diff --git a/backend/controllers/activityLogController.js b/backend/controllers/activityLogController.js new file mode 100644 index 0000000..bb27f64 --- /dev/null +++ b/backend/controllers/activityLogController.js @@ -0,0 +1,181 @@ +const ActivityLog = require("../schemas/activityLogModel"); + +const ALLOWED_ACTIONS = new Set(["login", "logout"]); +const ALLOWED_ROLES = new Set(["admin", "student", "teacher"]); +const ALLOWED_SORTS = new Set(["newest", "oldest"]); + +const escapeRegex = (value = "") => + value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + +const parsePositiveInteger = (value, fallback, max) => { + const parsed = Number.parseInt(value, 10); + + if (!Number.isFinite(parsed) || parsed < 1) { + return fallback; + } + + return Math.min(parsed, max); +}; + +const parseDateBoundary = (value, endOfDay = false) => { + if (!value) return null; + + const parsed = new Date(value); + + if (Number.isNaN(parsed.getTime())) { + return null; + } + + if (endOfDay && /^\d{4}-\d{2}-\d{2}$/.test(value)) { + parsed.setHours(23, 59, 59, 999); + } + + return parsed; +}; + +const getActivityLogsController = async (req, res) => { + try { + const page = parsePositiveInteger(req.query.page, 1, 100000); + const limit = parsePositiveInteger(req.query.limit, 10, 50); + + const search = String(req.query.search || "").trim().slice(0, 120); + const action = String(req.query.activity || "").trim().toLowerCase(); + const role = String(req.query.role || "").trim().toLowerCase(); + const sort = String(req.query.sort || "newest").trim().toLowerCase(); + + if (action && !ALLOWED_ACTIONS.has(action)) { + return res.status(400).send({ + success: false, + message: "Invalid activity filter.", + }); + } + + if (role && !ALLOWED_ROLES.has(role)) { + return res.status(400).send({ + success: false, + message: "Invalid role filter.", + }); + } + + if (!ALLOWED_SORTS.has(sort)) { + return res.status(400).send({ + success: false, + message: "Invalid sort option.", + }); + } + + const startDate = parseDateBoundary(req.query.startDate); + const endDate = parseDateBoundary(req.query.endDate, true); + + if (req.query.startDate && !startDate) { + return res.status(400).send({ + success: false, + message: "Invalid start date.", + }); + } + + if (req.query.endDate && !endDate) { + return res.status(400).send({ + success: false, + message: "Invalid end date.", + }); + } + + if (startDate && endDate && startDate > endDate) { + return res.status(400).send({ + success: false, + message: "Start date cannot be after end date.", + }); + } + + const query = {}; + + if (action) { + query.action = action; + } + + if (role) { + query.role = new RegExp(`^${escapeRegex(role)}$`, "i"); + } + + if (startDate || endDate) { + query.timestamp = {}; + + if (startDate) query.timestamp.$gte = startDate; + if (endDate) query.timestamp.$lte = endDate; + } + + if (search) { + const searchRegex = new RegExp(escapeRegex(search), "i"); + + query.$or = [ + { email: searchRegex }, + { role: searchRegex }, + { action: searchRegex }, + { ipAddress: searchRegex }, + { userAgent: searchRegex }, + ]; + } + + const totalItems = await ActivityLog.countDocuments(query); + const totalPages = Math.max(1, Math.ceil(totalItems / limit)); + const safePage = Math.min(page, totalPages); + const skip = (safePage - 1) * limit; + + const logs = await ActivityLog.find(query) + .select( + "userId action timestamp role email ipAddress userAgent createdAt", + ) + .populate("userId", "name email type") + .sort({ timestamp: sort === "oldest" ? 1 : -1 }) + .skip(skip) + .limit(limit) + .lean(); + + const sanitizedLogs = logs.map((log) => ({ + id: String(log._id), + user: { + id: log.userId?._id ? String(log.userId._id) : null, + name: log.userId?.name || null, + email: log.email || log.userId?.email || "Unknown", + role: log.role || log.userId?.type || "Unknown", + }, + activity: log.action, + timestamp: log.timestamp || log.createdAt, + ipAddress: log.ipAddress || null, + userAgent: log.userAgent || null, + })); + + return res.status(200).send({ + success: true, + data: sanitizedLogs, + pagination: { + page: safePage, + limit, + totalItems, + totalPages, + hasPreviousPage: safePage > 1, + hasNextPage: safePage < totalPages, + }, + filters: { + search, + activity: action, + role, + startDate: startDate?.toISOString() || null, + endDate: endDate?.toISOString() || null, + sort, + }, + }); + } catch (error) { + console.error("Unable to retrieve activity logs:", error); + + return res.status(500).send({ + success: false, + message: "Unable to retrieve activity logs.", + }); + } +}; + +module.exports = { + getActivityLogsController, +}; diff --git a/backend/routers/adminRoutes.js b/backend/routers/adminRoutes.js index b88e1ef..f001b12 100644 --- a/backend/routers/adminRoutes.js +++ b/backend/routers/adminRoutes.js @@ -1,32 +1,81 @@ const express = require("express"); + const authMiddleware = require("../middlewares/authMiddleware"); +const checkRole = require("../middlewares/roleMiddleware"); + const { - getAllUsersController, - getAllCoursesController, + adminLoginController, + adminResetPasswordController, deleteCourseController, deleteUserController, + getAllCoursesController, + getAllEnrolledCoursesController, + getAllPaymentsController, + getAllUsersController, } = require("../controllers/adminController"); -const checkRole = require("../middlewares/roleMiddleware"); +const { + getActivityLogsController, +} = require("../controllers/activityLogController"); const router = express.Router(); -// Admin login route (no auth middleware) -const { adminLoginController } = require("../controllers/adminController"); router.post("/login", adminLoginController); +router.get( + "/getallusers", + authMiddleware, + checkRole(["admin"]), + getAllUsersController, +); + +router.get( + "/enrolled-courses", + authMiddleware, + checkRole(["admin"]), + getAllEnrolledCoursesController, +); -router.get("/getallusers", authMiddleware, checkRole(["admin"]), getAllUsersController); -router.get("/enrolled-courses", authMiddleware, checkRole(["admin"]), require("../controllers/adminController").getAllEnrolledCoursesController); -router.get("/payments", authMiddleware, checkRole(["admin"]), require("../controllers/adminController").getAllPaymentsController); +router.get( + "/payments", + authMiddleware, + checkRole(["admin"]), + getAllPaymentsController, +); -router.get("/getallcourses", authMiddleware, checkRole(["admin"]), getAllCoursesController); +router.get( + "/activity-logs", + authMiddleware, + checkRole(["admin"]), + getActivityLogsController, +); -router.delete('/deletecourse/:courseid', authMiddleware, checkRole(["admin"]), deleteCourseController) +router.get( + "/getallcourses", + authMiddleware, + checkRole(["admin"]), + getAllCoursesController, +); -router.delete('/deleteuser/:cuserid', authMiddleware, checkRole(["admin"]), deleteUserController) +router.delete( + "/deletecourse/:courseid", + authMiddleware, + checkRole(["admin"]), + deleteCourseController, +); + +router.delete( + "/deleteuser/:cuserid", + authMiddleware, + checkRole(["admin"]), + deleteUserController, +); -// Admin reset user password -router.post('/reset-password/:userid', authMiddleware, checkRole(["admin"]), require("../controllers/adminController").adminResetPasswordController) +router.post( + "/reset-password/:userid", + authMiddleware, + checkRole(["admin"]), + adminResetPasswordController, +); module.exports = router; diff --git a/backend/schemas/activityLogModel.js b/backend/schemas/activityLogModel.js index 2332f99..25b8af5 100644 --- a/backend/schemas/activityLogModel.js +++ b/backend/schemas/activityLogModel.js @@ -1,12 +1,51 @@ -const mongoose = require('mongoose'); +const mongoose = require("mongoose"); +const activityLogSchema = new mongoose.Schema( + { + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: false, + index: true, + }, + action: { + type: String, + enum: ["login", "logout"], + required: true, + index: true, + }, + timestamp: { + type: Date, + default: Date.now, + index: true, + }, + role: { + type: String, + trim: true, + index: true, + }, + email: { + type: String, + trim: true, + lowercase: true, + index: true, + }, + ipAddress: { + type: String, + trim: true, + default: null, + }, + userAgent: { + type: String, + trim: true, + default: null, + }, + }, + { + versionKey: false, + }, +); -const activityLogSchema = new mongoose.Schema({ - userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: false }, - action: { type: String, enum: ['login', 'logout'], required: true }, - timestamp: { type: Date, default: Date.now }, - role: { type: String }, - email: { type: String } -}); +activityLogSchema.index({ timestamp: -1, role: 1, action: 1 }); -module.exports = mongoose.model('ActivityLog', activityLogSchema); +module.exports = mongoose.model("ActivityLog", activityLogSchema); diff --git a/docs/admin-activity-logs.md b/docs/admin-activity-logs.md new file mode 100644 index 0000000..a863cc8 --- /dev/null +++ b/docs/admin-activity-logs.md @@ -0,0 +1,89 @@ +# Admin Activity Logs + +Issue: `feat: add admin activity log viewer with filters and pagination` + +## API + +`GET /api/admin/activity-logs` + +Requires: + +```http +Authorization: Bearer +``` + +Supported query parameters: + +| Parameter | Values | +|---|---| +| `page` | Positive integer | +| `limit` | `1`–`50` | +| `search` | Email, role, activity, IP, or user-agent text | +| `role` | `admin`, `teacher`, `student` | +| `activity` | `login`, `logout` | +| `startDate` | ISO date or `YYYY-MM-DD` | +| `endDate` | ISO date or `YYYY-MM-DD` | +| `sort` | `newest`, `oldest` | + +Example: + +```text +/api/admin/activity-logs?page=1&limit=20&role=student&activity=login&sort=newest +``` + +## Security + +- Authentication is enforced with `authMiddleware`. +- Authorization is enforced with `checkRole(["admin"])`. +- API responses expose only safe activity fields. +- Passwords, JWTs, card details, and other credentials are not selected or returned. +- Regex search input is escaped. +- Pagination and filter values are bounded and validated. + +## Older records + +The original activity schema did not store IP address or user agent. Older records therefore show `Not recorded`. The new schema fields are optional, so no data migration is required. + +To record these fields in future login/logout handlers: + +```js +await ActivityLog.create({ + userId: user._id, + action: "login", + role: user.type, + email: user.email, + ipAddress: req.ip, + userAgent: req.get("user-agent"), +}); +``` + +## Frontend + +`AdminHome.jsx` now contains two sections: + +- Users +- Activity Logs + +The Activity Logs UI supports: + +- Search +- Role and activity filters +- Date range +- Newest/oldest sorting +- Page size +- Pagination +- Refresh +- Loading, empty, and error states +- Responsive mobile cards + +## Testing + +1. Sign in as admin. +2. Open Admin Dashboard → Activity Logs. +3. Verify logs load. +4. Test each filter separately and in combination. +5. Enter an invalid date range and verify validation. +6. Test previous/next pagination. +7. Remove or alter the token and verify the API returns `401`. +8. Sign in as a non-admin user and verify the API returns `403`. +9. Check the browser response and confirm no sensitive fields are returned. diff --git a/frontend/src/components/admin/ActivityLogs.css b/frontend/src/components/admin/ActivityLogs.css new file mode 100644 index 0000000..d51ee1c --- /dev/null +++ b/frontend/src/components/admin/ActivityLogs.css @@ -0,0 +1,405 @@ +.activity-log-page { + --activity-ink: #17201d; + --activity-muted: #66716c; + --activity-paper: #f5f1e8; + --activity-surface: #fffdf8; + --activity-line: rgba(23, 32, 29, 0.15); + --activity-coral: #ef6a4b; + --activity-blue: #315bd6; + --activity-green: #18865f; + + min-height: 100%; + padding: clamp(20px, 4vw, 48px); + color: var(--activity-ink); + background: + radial-gradient(circle at 95% 0%, rgba(49, 91, 214, 0.11), transparent 28rem), + var(--activity-paper); +} + +.activity-log-header { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 24px; + margin-bottom: 32px; +} + +.activity-log-eyebrow { + margin: 0 0 8px; + color: var(--activity-blue); + font-size: 0.72rem; + font-weight: 800; + letter-spacing: 0.16em; +} + +.activity-log-header h1 { + margin: 0; + font-size: clamp(2.4rem, 6vw, 4.8rem); + font-weight: 800; + letter-spacing: -0.065em; + line-height: 0.95; +} + +.activity-log-header p:last-child { + max-width: 620px; + margin: 18px 0 0; + color: var(--activity-muted); + line-height: 1.65; +} + +.activity-refresh-button, +.activity-primary-button, +.activity-secondary-button, +.activity-error-state button, +.activity-pagination button { + min-height: 42px; + padding: 0 16px; + border: 1px solid var(--activity-line); + border-radius: 999px; + font-weight: 700; + cursor: pointer; +} + +.activity-refresh-button, +.activity-primary-button { + border-color: var(--activity-ink); + color: white; + background: var(--activity-ink); +} + +.activity-secondary-button, +.activity-pagination button { + color: var(--activity-ink); + background: var(--activity-surface); +} + +.activity-filter-panel { + display: grid; + grid-template-columns: minmax(220px, 1.6fr) repeat(6, minmax(120px, 0.7fr)); + gap: 10px; + padding: 12px; + border: 1px solid var(--activity-line); + border-radius: 20px; + background: var(--activity-surface); +} + +.activity-filter-panel label { + min-width: 0; + padding: 9px 12px; + display: flex; + flex-direction: column; + justify-content: center; + border-radius: 12px; + background: var(--activity-paper); +} + +.activity-filter-panel label span { + margin-bottom: 4px; + color: var(--activity-muted); + font-size: 0.64rem; + font-weight: 800; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.activity-filter-panel input, +.activity-filter-panel select { + width: 100%; + min-height: 28px; + border: 0; + outline: 0; + color: var(--activity-ink); + background: transparent; +} + +.activity-filter-actions { + grid-column: 1 / -1; + display: flex; + justify-content: flex-end; + gap: 8px; + padding-top: 4px; +} + +.activity-error-state, +.activity-empty-state { + margin-top: 24px; + padding: 44px 24px; + border: 1px dashed var(--activity-line); + border-radius: 20px; + text-align: center; + background: rgba(255, 253, 248, 0.72); +} + +.activity-error-state { + border-color: rgba(190, 48, 48, 0.35); + color: #8f2525; + background: #fff1ef; +} + +.activity-error-state p, +.activity-empty-state p { + margin: 7px 0 18px; +} + +.activity-empty-state > span { + font-size: 2.5rem; + color: var(--activity-blue); +} + +.activity-empty-state h2 { + margin: 10px 0 0; +} + +.activity-skeleton-list { + margin-top: 24px; + overflow: hidden; + border: 1px solid var(--activity-line); + border-radius: 20px; + background: var(--activity-surface); +} + +.activity-skeleton-row { + height: 68px; + border-bottom: 1px solid var(--activity-line); + background: + linear-gradient( + 90deg, + transparent, + rgba(49, 91, 214, 0.07), + transparent + ); + background-size: 240% 100%; + animation: activityShimmer 1.5s linear infinite; +} + +.activity-table-summary { + margin: 26px 2px 10px; + display: flex; + justify-content: space-between; + gap: 16px; + color: var(--activity-muted); + font-size: 0.82rem; +} + +.activity-table-wrapper { + overflow-x: auto; + border: 1px solid var(--activity-line); + border-radius: 20px; + background: var(--activity-surface); +} + +.activity-table { + width: 100%; + border-collapse: collapse; + min-width: 880px; +} + +.activity-table th, +.activity-table td { + padding: 16px 18px; + border-bottom: 1px solid var(--activity-line); + text-align: left; + vertical-align: middle; +} + +.activity-table th { + color: var(--activity-muted); + background: #eee8da; + font-size: 0.67rem; + letter-spacing: 0.11em; + text-transform: uppercase; +} + +.activity-table td { + font-size: 0.86rem; +} + +.activity-table tbody tr:hover { + background: rgba(49, 91, 214, 0.035); +} + +.activity-table td:first-child { + display: table-cell; +} + +.activity-table td:first-child strong, +.activity-table td:first-child small { + display: block; +} + +.activity-table td:first-child small { + margin-top: 4px; + color: var(--activity-muted); +} + +.activity-role-badge, +.activity-action-badge { + display: inline-flex; + align-items: center; + padding: 6px 9px; + border-radius: 999px; + font-size: 0.7rem; + font-weight: 800; +} + +.activity-role-badge { + color: var(--activity-blue); + background: rgba(49, 91, 214, 0.1); +} + +.activity-action-badge { + text-transform: capitalize; +} + +.activity-action-login { + color: var(--activity-green); + background: rgba(24, 134, 95, 0.1); +} + +.activity-action-logout { + color: #9b4d1f; + background: rgba(239, 106, 75, 0.12); +} + +.activity-pagination { + margin-top: 18px; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 14px; +} + +.activity-pagination button:disabled, +.activity-refresh-button:disabled { + cursor: not-allowed; + opacity: 0.48; +} + +.sr-only { + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + position: absolute; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +@keyframes activityShimmer { + to { + background-position: -240% 0; + } +} + +@media (max-width: 1160px) { + .activity-filter-panel { + grid-template-columns: repeat(3, 1fr); + } + + .activity-search-field { + grid-column: span 2; + } +} + +@media (max-width: 720px) { + .activity-log-header { + align-items: flex-start; + flex-direction: column; + } + + .activity-filter-panel { + grid-template-columns: 1fr 1fr; + } + + .activity-search-field { + grid-column: 1 / -1; + } + + .activity-table-summary { + align-items: flex-start; + flex-direction: column; + } + + .activity-table-wrapper { + overflow: visible; + border: 0; + background: transparent; + } + + .activity-table { + min-width: 0; + } + + .activity-table thead { + display: none; + } + + .activity-table, + .activity-table tbody, + .activity-table tr, + .activity-table td { + display: block; + width: 100%; + } + + .activity-table tr { + margin-bottom: 12px; + padding: 8px 14px; + border: 1px solid var(--activity-line); + border-radius: 16px; + background: var(--activity-surface); + } + + .activity-table td, + .activity-table td:first-child { + padding: 11px 0; + display: grid; + grid-template-columns: 105px 1fr; + gap: 12px; + border-bottom: 1px solid var(--activity-line); + } + + .activity-table td:last-child { + border-bottom: 0; + } + + .activity-table td::before { + content: attr(data-label); + color: var(--activity-muted); + font-size: 0.64rem; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; + } +} + +@media (max-width: 480px) { + .activity-log-page { + padding: 18px 14px 34px; + } + + .activity-filter-panel { + grid-template-columns: 1fr; + } + + .activity-search-field { + grid-column: auto; + } + + .activity-filter-actions { + align-items: stretch; + flex-direction: column; + } + + .activity-pagination { + justify-content: space-between; + } +} + +@media (prefers-reduced-motion: reduce) { + .activity-skeleton-row { + animation: none; + } +} diff --git a/frontend/src/components/admin/ActivityLogs.jsx b/frontend/src/components/admin/ActivityLogs.jsx new file mode 100644 index 0000000..b14afd5 --- /dev/null +++ b/frontend/src/components/admin/ActivityLogs.jsx @@ -0,0 +1,393 @@ +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import axiosInstance from "../common/AxiosInstance"; +import "./ActivityLogs.css"; + +const initialFilters = { + search: "", + role: "", + activity: "", + startDate: "", + endDate: "", + sort: "newest", + limit: "10", +}; + +const formatDateTime = (value) => { + if (!value) return "Not recorded"; + + const date = new Date(value); + + if (Number.isNaN(date.getTime())) { + return "Not recorded"; + } + + return new Intl.DateTimeFormat(undefined, { + dateStyle: "medium", + timeStyle: "short", + }).format(date); +}; + +const formatRole = (role) => { + if (!role) return "Unknown"; + return role.charAt(0).toUpperCase() + role.slice(1).toLowerCase(); +}; + +const deviceLabel = (userAgent) => { + if (!userAgent) return "Not recorded"; + + if (/mobile|android|iphone|ipad/i.test(userAgent)) { + return "Mobile device"; + } + + if (/windows/i.test(userAgent)) return "Windows device"; + if (/macintosh|mac os/i.test(userAgent)) return "macOS device"; + if (/linux/i.test(userAgent)) return "Linux device"; + + return "Web browser"; +}; + +const ActivityLogs = () => { + const [draftFilters, setDraftFilters] = useState(initialFilters); + const [appliedFilters, setAppliedFilters] = useState(initialFilters); + const [logs, setLogs] = useState([]); + const [pagination, setPagination] = useState({ + page: 1, + limit: 10, + totalItems: 0, + totalPages: 1, + hasPreviousPage: false, + hasNextPage: false, + }); + const [page, setPage] = useState(1); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [error, setError] = useState(""); + + const queryString = useMemo(() => { + const params = new URLSearchParams({ + page: String(page), + limit: appliedFilters.limit, + sort: appliedFilters.sort, + }); + + Object.entries(appliedFilters).forEach(([key, value]) => { + if (value && !["limit", "sort"].includes(key)) { + params.set(key, value); + } + }); + + return params.toString(); + }, [appliedFilters, page]); + + const loadLogs = useCallback( + async (isRefresh = false) => { + isRefresh ? setRefreshing(true) : setLoading(true); + setError(""); + + try { + const response = await axiosInstance.get( + `api/admin/activity-logs?${queryString}`, + { + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + }, + ); + + if (!response.data.success) { + throw new Error(response.data.message); + } + + setLogs(response.data.data || []); + setPagination(response.data.pagination); + } catch (requestError) { + const message = + requestError.response?.data?.message || + "Activity logs could not be loaded. Please try again."; + + setError(message); + } finally { + setLoading(false); + setRefreshing(false); + } + }, + [queryString], + ); + + useEffect(() => { + loadLogs(); + }, [loadLogs]); + + const handleFilterChange = (event) => { + const { name, value } = event.target; + + setDraftFilters((current) => ({ + ...current, + [name]: value, + })); + }; + + const applyFilters = (event) => { + event.preventDefault(); + + if ( + draftFilters.startDate && + draftFilters.endDate && + draftFilters.startDate > draftFilters.endDate + ) { + setError("Start date cannot be after end date."); + return; + } + + setError(""); + setPage(1); + setAppliedFilters(draftFilters); + }; + + const clearFilters = () => { + setDraftFilters(initialFilters); + setAppliedFilters(initialFilters); + setPage(1); + }; + + const startItem = + pagination.totalItems === 0 + ? 0 + : (pagination.page - 1) * pagination.limit + 1; + const endItem = Math.min( + pagination.page * pagination.limit, + pagination.totalItems, + ); + + return ( +
+
+
+

ADMIN SECURITY

+

Activity logs

+

+ Review authentication activity without opening MongoDB or exposing + credentials. +

+
+ + +
+ +
+ + + + + + + + + + + + + + +
+ + +
+
+ + {error ? ( +
+ Unable to show activity +

{error}

+ +
+ ) : null} + + {loading ? ( +
+ Loading activity logs + {Array.from({ length: 6 }).map((_, index) => ( +
+ ))} +
+ ) : logs.length === 0 && !error ? ( +
+ +

No activity logs found

+

Try clearing filters or signing in with a demo account.

+
+ ) : !error ? ( + <> +
+ + Showing {startItem}{endItem} of{" "} + {pagination.totalItems} + + + Page {pagination.page} of {pagination.totalPages} + +
+ +
+ + + + + + + + + + + + + + {logs.map((log) => ( + + + + + + + + + ))} + +
+ Administrative authentication activity +
UserRoleActivityDate and timeIP addressDevice
+ {log.user.name || log.user.email} + {log.user.name ? {log.user.email} : null} + + + {formatRole(log.user.role)} + + + + {log.activity} + + + {formatDateTime(log.timestamp)} + + {log.ipAddress || "Not recorded"} + + {deviceLabel(log.userAgent)} +
+
+ + + + ) : null} +
+ ); +}; + +export default ActivityLogs; diff --git a/frontend/src/components/admin/AdminHome.jsx b/frontend/src/components/admin/AdminHome.jsx index c2517af..1a38063 100644 --- a/frontend/src/components/admin/AdminHome.jsx +++ b/frontend/src/components/admin/AdminHome.jsx @@ -1,119 +1,200 @@ -import React, { useState, useEffect } from 'react' -import { Button, styled, TableRow, TableHead, TableContainer, Paper, Table, TableBody, TableCell, tableCellClasses } from '@mui/material' -import axiosInstance from '../common/AxiosInstance' - - +import React, { useEffect, useState } from "react"; +import { + Button, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + styled, + tableCellClasses, +} from "@mui/material"; +import axiosInstance from "../common/AxiosInstance"; +import ActivityLogs from "./ActivityLogs"; const StyledTableCell = styled(TableCell)(({ theme }) => ({ - [`&.${tableCellClasses.head}`]: { - backgroundColor: theme.palette.common.black, - color: theme.palette.common.white, - }, - [`&.${tableCellClasses.body}`]: { - fontSize: 14, - }, + [`&.${tableCellClasses.head}`]: { + backgroundColor: theme.palette.common.black, + color: theme.palette.common.white, + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 14, + }, })); const StyledTableRow = styled(TableRow)(({ theme }) => ({ - '&:nth-of-type(odd)': { - backgroundColor: theme.palette.action.hover, - }, - // hide last border - '&:last-child td, &:last-child th': { - border: 0, - }, + "&:nth-of-type(odd)": { + backgroundColor: theme.palette.action.hover, + }, + "&:last-child td, &:last-child th": { + border: 0, + }, })); - const AdminHome = () => { - const [allUsers, setAllUsers] = useState([]) - - const allUsersList = async () => { - try { - const res = await axiosInstance.get('api/admin/getallusers', { - headers: { - "Authorization": `Bearer ${localStorage.getItem("token")}` - } - }) - if (res.data.success) { - setAllUsers(res.data.data) - } - else { - alert(res.data.message) - } - } catch (error) { - console.log(error); - } - } + const [activeSection, setActiveSection] = useState("users"); + const [allUsers, setAllUsers] = useState([]); + const [usersLoading, setUsersLoading] = useState(true); + const [usersError, setUsersError] = useState(""); - useEffect(() => { - allUsersList() - }, []) + const allUsersList = async () => { + setUsersLoading(true); + setUsersError(""); - const deleteUser = async (userId) => { - const confirmation = confirm('Are you sure you want to delete') - if (!confirmation) { - return; + try { + const response = await axiosInstance.get("api/admin/getallusers", { + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + }); + + if (response.data.success) { + setAllUsers(response.data.data || []); + } else { + setUsersError(response.data.message || "Unable to load users."); } - try { - const res = await axiosInstance.delete(`api/user/deleteuser/${userId}`, { - headers: { - Authorization: `Bearer ${localStorage.getItem('token')}`, - }, - }) - if (res.data.success) { - alert(res.data.message) - allUsersList() - } else { - alert("Failed to delete the user") - } - } catch (error) { - console.log('An error occurred:', error); + } catch (error) { + setUsersError( + error.response?.data?.message || "Unable to load users.", + ); + } finally { + setUsersLoading(false); + } + }; + + useEffect(() => { + allUsersList(); + }, []); + + const deleteUser = async (userId) => { + const confirmation = window.confirm( + "Are you sure you want to delete this user?", + ); + + if (!confirmation) return; + + try { + const response = await axiosInstance.delete( + `api/user/deleteuser/${userId}`, + { + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + }, + ); + + if (response.data.success) { + await allUsersList(); + } else { + window.alert(response.data.message || "Failed to delete the user."); } - } - - return ( - - - - - User ID - User Name - Email - Type - Action - - - - { - allUsers.length > 0 ? ( - allUsers.map((user) => ( - - - {user._id} - - - {user.name} - - - {user.email} - - - {user.type} - - - - {/* */} - - - ))) - : - (

No users found

) - } -
-
-
- ) -} - -export default AdminHome + } catch (error) { + window.alert( + error.response?.data?.message || "Failed to delete the user.", + ); + } + }; + + return ( +
+ + + {activeSection === "activity-logs" ? ( + + ) : ( +
+

+ Registered users +

+ + {usersError ? ( +
+

{usersError}

+ +
+ ) : null} + + {usersLoading ? ( +

Loading users…

+ ) : ( + + + + + User ID + User Name + Email + Type + Action + + + + {allUsers.length > 0 ? ( + allUsers.map((user) => ( + + + {user._id} + + {user.name} + {user.email} + {user.type} + + + + + )) + ) : ( + + + No users found + + + )} + +
+
+ )} +
+ )} +
+ ); +}; + +export default AdminHome;