From 090b38ab3bfeba7b4d75807178c80a455daa11fa Mon Sep 17 00:00:00 2001 From: Madhavi1108 Date: Sun, 21 Jun 2026 16:38:17 +0530 Subject: [PATCH 1/5] Implement admin dashboard with user management and analytics --- backend/controllers/admin.controller.js | 79 +++++++++ backend/routes/adminRoutes.js | 15 ++ backend/scripts/admin_dashboard_schema.sql | 19 +++ backend/server.js | 3 +- backend/services/admin.service.js | 168 +++++++++++++++++++ frontend/admin.html | 81 +++++++-- frontend/scripts/admin.js | 185 ++++++++++++++++++++- 7 files changed, 534 insertions(+), 16 deletions(-) create mode 100644 backend/controllers/admin.controller.js create mode 100644 backend/routes/adminRoutes.js create mode 100644 backend/scripts/admin_dashboard_schema.sql create mode 100644 backend/services/admin.service.js diff --git a/backend/controllers/admin.controller.js b/backend/controllers/admin.controller.js new file mode 100644 index 0000000..3df001b --- /dev/null +++ b/backend/controllers/admin.controller.js @@ -0,0 +1,79 @@ +const adminService = require("../services/admin.service"); +const { safeArray, safeNumber, sanitizeString, getPagination, buildPaginationMeta } = require("../utils/helpers"); + +const getDashboardStats = async (req, res) => { + try { + const data = await adminService.getDashboardStats(); + return res.status(200).json({ success: true, data }); + } catch (error) { + console.error("ADMIN DASHBOARD ERROR:", error); + return res.status(500).json({ success: false, message: "Server error" }); + } +}; + +const getUsers = async (req, res) => { + try { + const { page, limit } = getPagination(req.query.page, req.query.limit, 50); + const filters = { + search: sanitizeString(req.query.search), + status: sanitizeString(req.query.status), + role: sanitizeString(req.query.role) + }; + + const result = await adminService.getUsers(filters, page, limit); + + return res.status(200).json({ + success: true, + users: result.users, + ...buildPaginationMeta(result.total, page, limit) + }); + } catch (error) { + console.error("ADMIN GET USERS ERROR:", error); + return res.status(500).json({ success: false, message: "Server error" }); + } +}; + +const updateUserStatus = async (req, res) => { + try { + const targetId = safeNumber(req.params.id); + const status = sanitizeString(req.body.status); // 'active' or 'blocked' + + if (!targetId || !['active', 'blocked'].includes(status)) { + return res.status(400).json({ success: false, message: "Invalid payload" }); + } + + if (targetId === req.user.id) { + return res.status(400).json({ success: false, message: "Cannot modify own status" }); + } + + await adminService.updateUserStatus(req.user.id, targetId, status, req.ip, req.headers['user-agent']); + return res.status(200).json({ success: true, message: `User ${status === 'active' ? 'unblocked' : 'blocked'} successfully` }); + } catch (error) { + console.error("ADMIN UPDATE USER ERROR:", error); + return res.status(500).json({ success: false, message: "Server error" }); + } +}; + +const bulkUpdateUserStatus = async (req, res) => { + try { + const targetIds = safeArray(req.body.userIds).map(id => safeNumber(id)).filter(id => id > 0 && id !== req.user.id); + const status = sanitizeString(req.body.status); // 'active' or 'blocked' + + if (!targetIds.length || !['active', 'blocked'].includes(status)) { + return res.status(400).json({ success: false, message: "Invalid payload or users" }); + } + + await adminService.bulkUpdateUserStatus(req.user.id, targetIds, status, req.ip, req.headers['user-agent']); + return res.status(200).json({ success: true, message: `Users ${status === 'active' ? 'unblocked' : 'blocked'} successfully` }); + } catch (error) { + console.error("ADMIN BULK UPDATE ERROR:", error); + return res.status(500).json({ success: false, message: "Server error" }); + } +}; + +module.exports = { + getDashboardStats, + getUsers, + updateUserStatus, + bulkUpdateUserStatus +}; diff --git a/backend/routes/adminRoutes.js b/backend/routes/adminRoutes.js new file mode 100644 index 0000000..35f54a9 --- /dev/null +++ b/backend/routes/adminRoutes.js @@ -0,0 +1,15 @@ +const express = require("express"); +const router = express.Router(); +const { getDashboardStats, getUsers, updateUserStatus, bulkUpdateUserStatus } = require("../controllers/admin.controller"); +const { authMiddleware, authorizeRoles } = require("../middleware/authMiddleware"); + +// All admin routes are protected and require 'admin' role +router.use(authMiddleware); +router.use(authorizeRoles("admin")); + +router.get("/dashboard", getDashboardStats); +router.get("/users", getUsers); +router.patch("/users/:id/status", updateUserStatus); +router.post("/users/bulk-status", bulkUpdateUserStatus); + +module.exports = router; diff --git a/backend/scripts/admin_dashboard_schema.sql b/backend/scripts/admin_dashboard_schema.sql new file mode 100644 index 0000000..4e53abb --- /dev/null +++ b/backend/scripts/admin_dashboard_schema.sql @@ -0,0 +1,19 @@ +-- backend/scripts/admin_dashboard_schema.sql + +CREATE TABLE IF NOT EXISTS user_audit_logs ( + id INT AUTO_INCREMENT PRIMARY KEY, + admin_id INT NOT NULL, + target_user_id INT, + action VARCHAR(100) NOT NULL, + metadata JSON, + ip_address VARCHAR(45), + user_agent VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (admin_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (target_user_id) REFERENCES users(id) ON DELETE SET NULL +); + +-- Index for fast queries +CREATE INDEX idx_audit_admin ON user_audit_logs(admin_id); +CREATE INDEX idx_audit_action ON user_audit_logs(action); +CREATE INDEX idx_audit_created ON user_audit_logs(created_at); diff --git a/backend/server.js b/backend/server.js index 8a70038..e57e59a 100644 --- a/backend/server.js +++ b/backend/server.js @@ -44,6 +44,7 @@ const authRoutes = require("./routes/authRoutes"); const orderRoutes = require("./routes/orderRoutes"); const promoRoutes = require("./routes/promoRoutes"); +const adminRoutes = require("./routes/adminRoutes"); const wishlistRoutes = require( @@ -256,8 +257,8 @@ app.use("/api/products", productRoutes); app.use("/api/auth", authRoutes); app.use("/api/orders", orderRoutes); - app.use("/api/promos", promoRoutes); +app.use("/api/admin", adminRoutes); app.use( "/api/wishlist", diff --git a/backend/services/admin.service.js b/backend/services/admin.service.js new file mode 100644 index 0000000..42da7bf --- /dev/null +++ b/backend/services/admin.service.js @@ -0,0 +1,168 @@ +const db = require("../config/db"); +const { safeArray, safeNumber, sanitizeString } = require("../utils/helpers"); + +const logAudit = async (connection, adminId, targetUserId, action, metadata, ip, userAgent) => { + const query = ` + INSERT INTO user_audit_logs (admin_id, target_user_id, action, metadata, ip_address, user_agent) + VALUES (?, ?, ?, ?, ?, ?) + `; + await connection.query(query, [adminId, targetUserId, action, JSON.stringify(metadata || {}), ip, userAgent]); +}; + +const getDashboardStats = async () => { + // Collect basic statistics + const [userStats] = await db.query(` + SELECT + COUNT(*) as totalUsers, + SUM(CASE WHEN is_active = 1 THEN 1 ELSE 0 END) as activeUsers, + SUM(CASE WHEN is_active = 0 THEN 1 ELSE 0 END) as blockedUsers, + SUM(CASE WHEN MONTH(created_at) = MONTH(CURRENT_DATE()) AND YEAR(created_at) = YEAR(CURRENT_DATE()) THEN 1 ELSE 0 END) as newUsersThisMonth + FROM users + `); + + const [orderStats] = await db.query(` + SELECT + COUNT(*) as totalOrders, + SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) as completedOrders, + SUM(CASE WHEN status = 'pending' OR status = 'processing' THEN 1 ELSE 0 END) as pendingOrders, + SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelledOrders, + SUM(final_amount) as totalRevenue, + SUM(CASE WHEN MONTH(created_at) = MONTH(CURRENT_DATE()) AND YEAR(created_at) = YEAR(CURRENT_DATE()) THEN final_amount ELSE 0 END) as revenueThisMonth + FROM orders + `); + + const [productStats] = await db.query(`SELECT COUNT(*) as totalProducts FROM products`); + + // Analytics: Revenue over the last 30 days (simplified) + const [revenueAnalytics] = await db.query(` + SELECT DATE(created_at) as date, SUM(final_amount) as revenue + FROM orders + WHERE created_at >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY) + GROUP BY DATE(created_at) + ORDER BY date ASC + `); + + // Order status distribution + const [orderStatusDistribution] = await db.query(` + SELECT status, COUNT(*) as count + FROM orders + GROUP BY status + `); + + return { + stats: { + ...userStats[0], + ...orderStats[0], + totalProducts: productStats[0].totalProducts, + averageOrderValue: orderStats[0].totalOrders ? (orderStats[0].totalRevenue / orderStats[0].totalOrders).toFixed(2) : 0 + }, + charts: { + revenue: safeArray(revenueAnalytics), + orderStatus: safeArray(orderStatusDistribution) + } + }; +}; + +const getUsers = async (filters, page, limit) => { + const offset = (page - 1) * limit; + let query = `SELECT id, name, email, role, is_active, created_at, updated_at FROM users WHERE 1=1`; + const params = []; + + if (filters.search) { + query += ` AND (name LIKE ? OR email LIKE ?)`; + params.push(`%${filters.search}%`, `%${filters.search}%`); + } + + if (filters.status) { + query += ` AND is_active = ?`; + params.push(filters.status === 'active' ? 1 : 0); + } + + if (filters.role) { + query += ` AND role = ?`; + params.push(filters.role); + } + + // Get total count + const countQuery = `SELECT COUNT(*) as total FROM (${query}) as t`; + const [countResult] = await db.query(countQuery, params); + + // Apply pagination + query += ` ORDER BY created_at DESC LIMIT ? OFFSET ?`; + params.push(limit, offset); + + const [users] = await db.query(query, params); + + return { + users: safeArray(users), + total: countResult[0].total, + page, + limit + }; +}; + +const updateUserStatus = async (adminId, targetId, status, ip, userAgent) => { + const connection = await db.getConnection(); + try { + await connection.beginTransaction(); + + await connection.query(`UPDATE users SET is_active = ? WHERE id = ?`, [status === 'active' ? 1 : 0, targetId]); + + await logAudit( + connection, + adminId, + targetId, + status === 'active' ? 'USER_UNBLOCKED' : 'USER_BLOCKED', + {}, + ip, + userAgent + ); + + await connection.commit(); + return true; + } catch (e) { + await connection.rollback(); + throw e; + } finally { + connection.release(); + } +}; + +const bulkUpdateUserStatus = async (adminId, targetIds, status, ip, userAgent) => { + const connection = await db.getConnection(); + try { + await connection.beginTransaction(); + + if (safeArray(targetIds).length > 0) { + const placeholders = targetIds.map(() => '?').join(','); + await connection.query(`UPDATE users SET is_active = ? WHERE id IN (${placeholders})`, [status === 'active' ? 1 : 0, ...targetIds]); + + for (const id of targetIds) { + await logAudit( + connection, + adminId, + id, + status === 'active' ? 'BULK_UNBLOCK' : 'BULK_BLOCK', + {}, + ip, + userAgent + ); + } + } + + await connection.commit(); + return true; + } catch (e) { + await connection.rollback(); + throw e; + } finally { + connection.release(); + } +}; + +module.exports = { + getDashboardStats, + getUsers, + updateUserStatus, + bulkUpdateUserStatus +}; diff --git a/frontend/admin.html b/frontend/admin.html index 2490025..cb8cfee 100644 --- a/frontend/admin.html +++ b/frontend/admin.html @@ -82,12 +82,19 @@ > - - - + + + + + @@ -112,7 +119,7 @@