From 638042d1cb06a310ddddfe697a04e47bef9e1643 Mon Sep 17 00:00:00 2001 From: Saptarshi Mukherjee Date: Thu, 8 Jan 2026 21:30:13 +0530 Subject: [PATCH] feat: Add admin dashboard foundation with user statistics - Add adminMiddleware for role-based access control - Create GET /api/v1/admin/stats endpoint for admin statistics - Implement getAdminStats service to count users by role - Add admin dashboard UI with user statistics cards - Display total users, admins, regular users, and creators - Show recent 10 registered users in table format - Add proper error handling and authentication checks - Update routes to include admin dashboard path --- .../src/api/v1/user/user.constant.ts | 5 + .../src/api/v1/user/user.controller.ts | 11 + .../src/api/v1/user/user.middleware.ts | 25 ++ .../src/api/v1/user/user.routes.ts | 3 + .../src/api/v1/user/user.service.ts | 24 ++ .../src/app/routes/AdminRoutes.tsx | 13 + .../src/app/routes/AppRoutes.tsx | 4 + .../V1/Component/Pages/AdminDashboard.tsx | 283 ++++++++++++++++++ 8 files changed, 368 insertions(+) create mode 100644 LocalMind-Frontend/src/features/Admin/V1/Component/Pages/AdminDashboard.tsx diff --git a/LocalMind-Backend/src/api/v1/user/user.constant.ts b/LocalMind-Backend/src/api/v1/user/user.constant.ts index 2f90bde..405058e 100644 --- a/LocalMind-Backend/src/api/v1/user/user.constant.ts +++ b/LocalMind-Backend/src/api/v1/user/user.constant.ts @@ -87,6 +87,11 @@ enum UserConstant { API_KEY_REVEALED = 'API key revealed successfully', API_KEY_REVEAL_FAILED = 'Failed to reveal API key', + + // ✅ ADMIN OPERATIONS + ADMIN_STATS_SUCCESS = 'Admin statistics fetched successfully', + ADMIN_STATS_FAILED = 'Failed to fetch admin statistics', + ADMIN_ONLY = 'Access denied. Admin privileges required', } export default UserConstant diff --git a/LocalMind-Backend/src/api/v1/user/user.controller.ts b/LocalMind-Backend/src/api/v1/user/user.controller.ts index 1b92537..959ff97 100644 --- a/LocalMind-Backend/src/api/v1/user/user.controller.ts +++ b/LocalMind-Backend/src/api/v1/user/user.controller.ts @@ -15,6 +15,7 @@ class UserController { this.profile = this.profile.bind(this) this.apiEndPointCreater = this.apiEndPointCreater.bind(this) this.getApiKey = this.getApiKey.bind(this) + this.getAdminStats = this.getAdminStats.bind(this) } private setHeaderToken(res: Response, token: string): void { @@ -158,6 +159,16 @@ class UserController { SendResponse.error(res, err.message || UserConstant.SERVER_ERROR, 500, err) } } + + async getAdminStats(req: Request, res: Response): Promise { + try { + const stats = await userService.getAdminStats() + + SendResponse.success(res, UserConstant.ADMIN_STATS_SUCCESS, stats, StatusConstant.OK) + } catch (err: any) { + SendResponse.error(res, err.message || UserConstant.ADMIN_STATS_FAILED, StatusConstant.INTERNAL_SERVER_ERROR, err) + } + } } export default new UserController() diff --git a/LocalMind-Backend/src/api/v1/user/user.middleware.ts b/LocalMind-Backend/src/api/v1/user/user.middleware.ts index 9b32702..17c1f6c 100644 --- a/LocalMind-Backend/src/api/v1/user/user.middleware.ts +++ b/LocalMind-Backend/src/api/v1/user/user.middleware.ts @@ -24,6 +24,31 @@ class UserMiddleware { SendResponse.error(res, err.message || UserConstant.SERVER_ERROR, 500) } } + + async adminMiddleware(req: Request, res: Response, next: NextFunction): Promise { + try { + const token = req.headers.authorization?.split(' ')[1] || req.cookies?.token + + if (!token) { + throw new Error(UserConstant.TOKEN_MISSING) + } + + const decodedData: Partial | null = UserUtils.verifyToken(token) + + if (!decodedData) { + throw new Error(UserConstant.INVALID_TOKEN) + } + + if (decodedData.role !== 'admin') { + throw new Error(UserConstant.ADMIN_ONLY) + } + + req.user = decodedData + return next() + } catch (err: any) { + SendResponse.error(res, err.message || UserConstant.SERVER_ERROR, err.message === UserConstant.ADMIN_ONLY ? 403 : 500) + } + } } export default new UserMiddleware() diff --git a/LocalMind-Backend/src/api/v1/user/user.routes.ts b/LocalMind-Backend/src/api/v1/user/user.routes.ts index e108630..d48d040 100644 --- a/LocalMind-Backend/src/api/v1/user/user.routes.ts +++ b/LocalMind-Backend/src/api/v1/user/user.routes.ts @@ -12,6 +12,9 @@ router.get('/v1/auth/apiKey/generate', userMiddleware.middleware, userController router.get('/v1/auth/profile', userMiddleware.middleware, userController.profile) router.get('/v1/auth/apiKey', userMiddleware.middleware, userController.getApiKey) +// Admin routes +router.get('/v1/admin/stats', userMiddleware.adminMiddleware, userController.getAdminStats) + // router.post("v1/user/apikey/reveal", userMiddleware.middleware, UserController.revealApiKey); export { router as userRoutes } diff --git a/LocalMind-Backend/src/api/v1/user/user.service.ts b/LocalMind-Backend/src/api/v1/user/user.service.ts index 952b77a..b57cecf 100644 --- a/LocalMind-Backend/src/api/v1/user/user.service.ts +++ b/LocalMind-Backend/src/api/v1/user/user.service.ts @@ -50,5 +50,29 @@ class userService { throw new Error(err.message) } } + + async getAdminStats() { + try { + const totalUsers = await User.countDocuments() + const adminUsers = await User.countDocuments({ role: 'admin' }) + const regularUsers = await User.countDocuments({ role: 'user' }) + const creatorUsers = await User.countDocuments({ role: 'creator' }) + + const recentUsers = await User.find() + .select('firstName email role createdAt') + .sort({ createdAt: -1 }) + .limit(10) + + return { + totalUsers, + adminUsers, + regularUsers, + creatorUsers, + recentUsers, + } + } catch (err: any) { + throw new Error(err.message) + } + } } export default new userService() diff --git a/LocalMind-Frontend/src/app/routes/AdminRoutes.tsx b/LocalMind-Frontend/src/app/routes/AdminRoutes.tsx index e69de29..cc377d0 100644 --- a/LocalMind-Frontend/src/app/routes/AdminRoutes.tsx +++ b/LocalMind-Frontend/src/app/routes/AdminRoutes.tsx @@ -0,0 +1,13 @@ +import React from 'react' +import { Route, Routes } from 'react-router-dom' +import AdminDashboard from '../../features/Admin/V1/Component/Pages/AdminDashboard' + +const AdminRoutes: React.FC = () => { + return ( + + } /> + + ) +} + +export default AdminRoutes diff --git a/LocalMind-Frontend/src/app/routes/AppRoutes.tsx b/LocalMind-Frontend/src/app/routes/AppRoutes.tsx index af480f1..d938b0e 100644 --- a/LocalMind-Frontend/src/app/routes/AppRoutes.tsx +++ b/LocalMind-Frontend/src/app/routes/AppRoutes.tsx @@ -2,6 +2,7 @@ import React from 'react' import { Route, Routes } from 'react-router-dom' import HomePage from '../../features/Dashboard/V1/Component/Pages/HomePage' import LoginPage from '../../shared/component/v1/LoginPage' +import AdminDashboard from '../../features/Admin/V1/Component/Pages/AdminDashboard' const AppRoutes: React.FC = () => { return ( @@ -18,6 +19,9 @@ const AppRoutes: React.FC = () => { {/* Forgot Password Page - TODO: Create ForgotPasswordPage component */} } /> + {/* Admin Dashboard */} + } /> + {/* Chat Page */} ) diff --git a/LocalMind-Frontend/src/features/Admin/V1/Component/Pages/AdminDashboard.tsx b/LocalMind-Frontend/src/features/Admin/V1/Component/Pages/AdminDashboard.tsx new file mode 100644 index 0000000..51e865b --- /dev/null +++ b/LocalMind-Frontend/src/features/Admin/V1/Component/Pages/AdminDashboard.tsx @@ -0,0 +1,283 @@ +import React, { useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' + +interface AdminStats { + totalUsers: number + adminUsers: number + regularUsers: number + creatorUsers: number + recentUsers: Array<{ + _id: string + firstName: string + email: string + role: string + createdAt: string + }> +} + +const AdminDashboard: React.FC = () => { + const navigate = useNavigate() + const [stats, setStats] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + fetchAdminStats() + }, []) + + const fetchAdminStats = async () => { + try { + setLoading(true) + const token = + localStorage.getItem('token') || document.cookie.split('token=')[1]?.split(';')[0] + + if (!token) { + setError('Authentication required') + navigate('/login') + return + } + + const response = await fetch('http://localhost:8000/api/v1/admin/stats', { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }) + + if (!response.ok) { + if (response.status === 403) { + setError('Access denied. Admin privileges required.') + } else if (response.status === 401) { + setError('Authentication failed') + navigate('/login') + } else { + setError('Failed to fetch admin statistics') + } + return + } + + const data = await response.json() + setStats(data.data) + setError(null) + } catch (err) { + setError('An error occurred while fetching statistics') + console.error('Error fetching admin stats:', err) + } finally { + setLoading(false) + } + } + + const formatDate = (dateString: string) => { + const date = new Date(dateString) + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) + } + + if (loading) { + return ( +
+
Loading...
+
+ ) + } + + if (error) { + return ( +
+
+

Error

+

{error}

+ +
+
+ ) + } + + return ( +
+
+ {/* Header */} +
+

Admin Dashboard

+

System usage and user statistics

+
+ + {/* Stats Cards */} +
+
+
+
+

Total Users

+

{stats?.totalUsers || 0}

+
+
+ + + +
+
+
+ +
+
+
+

Admin Users

+

{stats?.adminUsers || 0}

+
+
+ + + +
+
+
+ +
+
+
+

Regular Users

+

{stats?.regularUsers || 0}

+
+
+ + + +
+
+
+ +
+
+
+

Creator Users

+

{stats?.creatorUsers || 0}

+
+
+ + + +
+
+
+
+ + {/* Recent Users Table */} +
+
+

Recent Users

+

Last 10 registered users

+
+
+ + + + + + + + + + + {stats?.recentUsers && stats.recentUsers.length > 0 ? ( + stats.recentUsers.map(user => ( + + + + + + + )) + ) : ( + + + + )} + +
+ Name + + Email + + Role + + Joined +
+
{user.firstName}
+
+
{user.email}
+
+ + {user.role} + + +
{formatDate(user.createdAt)}
+
+ No users found +
+
+
+
+
+ ) +} + +export default AdminDashboard