Initial Claude Run
This commit is contained in:
529
backend/src/routes/admin.js
Normal file
529
backend/src/routes/admin.js
Normal 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;
|
||||
Reference in New Issue
Block a user