Initial Claude Run

This commit is contained in:
Jake Kasper
2025-08-21 07:06:36 -05:00
parent 5ead64afcd
commit 2a46f7261e
53 changed files with 7633 additions and 2 deletions

529
backend/src/routes/admin.js Normal file
View File

@@ -0,0 +1,529 @@
const express = require('express');
const pool = require('../config/database');
const { validateRequest, validateParams } = require('../utils/validation');
const { productSchema, idParamSchema } = require('../utils/validation');
const { requireAdmin } = require('../middleware/auth');
const { AppError } = require('../middleware/errorHandler');
const router = express.Router();
// Apply admin middleware to all routes
router.use(requireAdmin);
// @route GET /api/admin/dashboard
// @desc Get admin dashboard statistics
// @access Private (Admin)
router.get('/dashboard', async (req, res, next) => {
try {
const statsQuery = `
SELECT
(SELECT COUNT(*) FROM users) as total_users,
(SELECT COUNT(*) FROM users WHERE role = 'admin') as admin_users,
(SELECT COUNT(*) FROM users WHERE created_at >= CURRENT_DATE - INTERVAL '30 days') as new_users_30d,
(SELECT COUNT(*) FROM properties) as total_properties,
(SELECT COUNT(*) FROM user_equipment) as total_equipment,
(SELECT COUNT(*) FROM products) as total_products,
(SELECT COUNT(*) FROM user_products) as custom_products,
(SELECT COUNT(*) FROM application_plans) as total_plans,
(SELECT COUNT(*) FROM application_logs) as total_applications,
(SELECT COUNT(*) FROM application_logs WHERE application_date >= CURRENT_DATE - INTERVAL '7 days') as recent_applications
`;
const statsResult = await pool.query(statsQuery);
const stats = statsResult.rows[0];
// Get user activity (users with recent activity)
const userActivityQuery = `
SELECT
DATE_TRUNC('day', created_at) as date,
COUNT(*) as new_registrations
FROM users
WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY DATE_TRUNC('day', created_at)
ORDER BY date DESC
`;
const activityResult = await pool.query(userActivityQuery);
// Get application activity
const applicationActivityQuery = `
SELECT
DATE_TRUNC('day', application_date) as date,
COUNT(*) as applications,
COUNT(DISTINCT user_id) as active_users
FROM application_logs
WHERE application_date >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY DATE_TRUNC('day', application_date)
ORDER BY date DESC
`;
const appActivityResult = await pool.query(applicationActivityQuery);
res.json({
success: true,
data: {
stats: {
totalUsers: parseInt(stats.total_users),
adminUsers: parseInt(stats.admin_users),
newUsers30d: parseInt(stats.new_users_30d),
totalProperties: parseInt(stats.total_properties),
totalEquipment: parseInt(stats.total_equipment),
totalProducts: parseInt(stats.total_products),
customProducts: parseInt(stats.custom_products),
totalPlans: parseInt(stats.total_plans),
totalApplications: parseInt(stats.total_applications),
recentApplications: parseInt(stats.recent_applications)
},
userActivity: activityResult.rows.map(row => ({
date: row.date,
newRegistrations: parseInt(row.new_registrations)
})),
applicationActivity: appActivityResult.rows.map(row => ({
date: row.date,
applications: parseInt(row.applications),
activeUsers: parseInt(row.active_users)
}))
}
});
} catch (error) {
next(error);
}
});
// @route GET /api/admin/users
// @desc Get all users with pagination
// @access Private (Admin)
router.get('/users', async (req, res, next) => {
try {
const { page = 1, limit = 20, search, role } = req.query;
const offset = (page - 1) * limit;
let whereConditions = [];
let queryParams = [];
let paramCount = 0;
if (search) {
paramCount++;
whereConditions.push(`(first_name ILIKE $${paramCount} OR last_name ILIKE $${paramCount} OR email ILIKE $${paramCount})`);
queryParams.push(`%${search}%`);
}
if (role) {
paramCount++;
whereConditions.push(`role = $${paramCount}`);
queryParams.push(role);
}
const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : '';
// Get total count
const countQuery = `SELECT COUNT(*) as total FROM users ${whereClause}`;
const countResult = await pool.query(countQuery, queryParams);
const totalUsers = parseInt(countResult.rows[0].total);
// Get users with stats
paramCount++;
queryParams.push(limit);
paramCount++;
queryParams.push(offset);
const usersQuery = `
SELECT u.id, u.email, u.first_name, u.last_name, u.role, u.oauth_provider,
u.created_at, u.updated_at,
COUNT(DISTINCT p.id) as property_count,
COUNT(DISTINCT ap.id) as application_count,
MAX(al.application_date) as last_application
FROM users u
LEFT JOIN properties p ON u.id = p.user_id
LEFT JOIN application_plans ap ON u.id = ap.user_id
LEFT JOIN application_logs al ON u.id = al.user_id
${whereClause}
GROUP BY u.id
ORDER BY u.created_at DESC
LIMIT $${paramCount - 1} OFFSET $${paramCount}
`;
const usersResult = await pool.query(usersQuery, queryParams);
res.json({
success: true,
data: {
users: usersResult.rows.map(user => ({
id: user.id,
email: user.email,
firstName: user.first_name,
lastName: user.last_name,
role: user.role,
oauthProvider: user.oauth_provider,
propertyCount: parseInt(user.property_count),
applicationCount: parseInt(user.application_count),
lastApplication: user.last_application,
createdAt: user.created_at,
updatedAt: user.updated_at
})),
pagination: {
currentPage: parseInt(page),
totalPages: Math.ceil(totalUsers / limit),
totalUsers,
hasNext: (page * limit) < totalUsers,
hasPrev: page > 1
}
}
});
} catch (error) {
next(error);
}
});
// @route PUT /api/admin/users/:id/role
// @desc Update user role
// @access Private (Admin)
router.put('/users/:id/role', validateParams(idParamSchema), async (req, res, next) => {
try {
const userId = req.params.id;
const { role } = req.body;
if (!['admin', 'user'].includes(role)) {
throw new AppError('Invalid role', 400);
}
// Prevent removing admin role from yourself
if (parseInt(userId) === req.user.id && role !== 'admin') {
throw new AppError('Cannot remove admin role from yourself', 400);
}
// Check if user exists
const userCheck = await pool.query(
'SELECT id, role FROM users WHERE id = $1',
[userId]
);
if (userCheck.rows.length === 0) {
throw new AppError('User not found', 404);
}
// Update role
const result = await pool.query(
'UPDATE users SET role = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2 RETURNING id, email, role',
[role, userId]
);
const user = result.rows[0];
res.json({
success: true,
message: 'User role updated successfully',
data: {
user: {
id: user.id,
email: user.email,
role: user.role
}
}
});
} catch (error) {
next(error);
}
});
// @route DELETE /api/admin/users/:id
// @desc Delete user account
// @access Private (Admin)
router.delete('/users/:id', validateParams(idParamSchema), async (req, res, next) => {
try {
const userId = req.params.id;
// Prevent deleting yourself
if (parseInt(userId) === req.user.id) {
throw new AppError('Cannot delete your own account', 400);
}
// Check if user exists
const userCheck = await pool.query(
'SELECT id, email FROM users WHERE id = $1',
[userId]
);
if (userCheck.rows.length === 0) {
throw new AppError('User not found', 404);
}
const user = userCheck.rows[0];
// Delete user (cascading will handle related records)
await pool.query('DELETE FROM users WHERE id = $1', [userId]);
res.json({
success: true,
message: `User ${user.email} deleted successfully`
});
} catch (error) {
next(error);
}
});
// @route GET /api/admin/products
// @desc Get all products for management
// @access Private (Admin)
router.get('/products', async (req, res, next) => {
try {
const { category, search } = req.query;
let whereConditions = [];
let queryParams = [];
let paramCount = 0;
if (category) {
paramCount++;
whereConditions.push(`p.category_id = $${paramCount}`);
queryParams.push(category);
}
if (search) {
paramCount++;
whereConditions.push(`(p.name ILIKE $${paramCount} OR p.brand ILIKE $${paramCount})`);
queryParams.push(`%${search}%`);
}
const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : '';
const result = await pool.query(
`SELECT p.*, pc.name as category_name,
COUNT(pr.id) as rate_count,
COUNT(up.id) as usage_count
FROM products p
JOIN product_categories pc ON p.category_id = pc.id
LEFT JOIN product_rates pr ON p.id = pr.product_id
LEFT JOIN user_products up ON p.id = up.product_id
${whereClause}
GROUP BY p.id, pc.name
ORDER BY p.name`,
queryParams
);
res.json({
success: true,
data: {
products: result.rows.map(product => ({
id: product.id,
name: product.name,
brand: product.brand,
categoryName: product.category_name,
productType: product.product_type,
activeIngredients: product.active_ingredients,
description: product.description,
rateCount: parseInt(product.rate_count),
usageCount: parseInt(product.usage_count),
createdAt: product.created_at
}))
}
});
} catch (error) {
next(error);
}
});
// @route POST /api/admin/products
// @desc Create new product
// @access Private (Admin)
router.post('/products', validateRequest(productSchema), async (req, res, next) => {
try {
const { name, brand, categoryId, productType, activeIngredients, description } = req.body;
// Check if category exists
const categoryCheck = await pool.query(
'SELECT id FROM product_categories WHERE id = $1',
[categoryId]
);
if (categoryCheck.rows.length === 0) {
throw new AppError('Product category not found', 404);
}
const result = await pool.query(
`INSERT INTO products (name, brand, category_id, product_type, active_ingredients, description)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *`,
[name, brand, categoryId, productType, activeIngredients, description]
);
const product = result.rows[0];
res.status(201).json({
success: true,
message: 'Product created successfully',
data: {
product: {
id: product.id,
name: product.name,
brand: product.brand,
categoryId: product.category_id,
productType: product.product_type,
activeIngredients: product.active_ingredients,
description: product.description,
createdAt: product.created_at
}
}
});
} catch (error) {
next(error);
}
});
// @route PUT /api/admin/products/:id
// @desc Update product
// @access Private (Admin)
router.put('/products/:id', validateParams(idParamSchema), validateRequest(productSchema), async (req, res, next) => {
try {
const productId = req.params.id;
const { name, brand, categoryId, productType, activeIngredients, description } = req.body;
// Check if product exists
const productCheck = await pool.query(
'SELECT id FROM products WHERE id = $1',
[productId]
);
if (productCheck.rows.length === 0) {
throw new AppError('Product not found', 404);
}
// Check if category exists
const categoryCheck = await pool.query(
'SELECT id FROM product_categories WHERE id = $1',
[categoryId]
);
if (categoryCheck.rows.length === 0) {
throw new AppError('Product category not found', 404);
}
const result = await pool.query(
`UPDATE products
SET name = $1, brand = $2, category_id = $3, product_type = $4,
active_ingredients = $5, description = $6
WHERE id = $7
RETURNING *`,
[name, brand, categoryId, productType, activeIngredients, description, productId]
);
const product = result.rows[0];
res.json({
success: true,
message: 'Product updated successfully',
data: {
product: {
id: product.id,
name: product.name,
brand: product.brand,
categoryId: product.category_id,
productType: product.product_type,
activeIngredients: product.active_ingredients,
description: product.description,
createdAt: product.created_at
}
}
});
} catch (error) {
next(error);
}
});
// @route DELETE /api/admin/products/:id
// @desc Delete product
// @access Private (Admin)
router.delete('/products/:id', validateParams(idParamSchema), async (req, res, next) => {
try {
const productId = req.params.id;
// Check if product exists
const productCheck = await pool.query(
'SELECT id, name FROM products WHERE id = $1',
[productId]
);
if (productCheck.rows.length === 0) {
throw new AppError('Product not found', 404);
}
const product = productCheck.rows[0];
// Check if product is used in any user products or applications
const usageCheck = await pool.query(
`SELECT
(SELECT COUNT(*) FROM user_products WHERE product_id = $1) +
(SELECT COUNT(*) FROM application_plan_products WHERE product_id = $1) +
(SELECT COUNT(*) FROM application_log_products WHERE product_id = $1) as usage_count`,
[productId]
);
if (parseInt(usageCheck.rows[0].usage_count) > 0) {
throw new AppError('Cannot delete product that is being used by users', 400);
}
await pool.query('DELETE FROM products WHERE id = $1', [productId]);
res.json({
success: true,
message: `Product "${product.name}" deleted successfully`
});
} catch (error) {
next(error);
}
});
// @route GET /api/admin/system/health
// @desc Get system health information
// @access Private (Admin)
router.get('/system/health', async (req, res, next) => {
try {
// Database connection test
const dbResult = await pool.query('SELECT NOW() as timestamp, version() as version');
// Get database statistics
const dbStats = await pool.query(`
SELECT
pg_size_pretty(pg_database_size(current_database())) as database_size,
(SELECT count(*) FROM pg_stat_activity WHERE state = 'active') as active_connections,
(SELECT setting FROM pg_settings WHERE name = 'max_connections') as max_connections
`);
const stats = dbStats.rows[0];
res.json({
success: true,
data: {
system: {
status: 'healthy',
timestamp: new Date(),
uptime: process.uptime(),
nodeVersion: process.version,
environment: process.env.NODE_ENV || 'development'
},
database: {
status: 'connected',
version: dbResult.rows[0].version,
size: stats.database_size,
activeConnections: parseInt(stats.active_connections),
maxConnections: parseInt(stats.max_connections),
timestamp: dbResult.rows[0].timestamp
},
services: {
weatherApi: {
configured: !!process.env.WEATHER_API_KEY,
status: process.env.WEATHER_API_KEY ? 'available' : 'not configured'
},
googleOAuth: {
configured: !!(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET),
status: (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) ? 'available' : 'not configured'
}
}
}
});
} catch (error) {
next(error);
}
});
module.exports = router;