Skip to content

Commit fe20023

Browse files
authored
feat: merge secure admin payment dashboard (#17)
Merge PR #17 from Jidnyasa-P Add a secure admin payment dashboard with payment management features. Highlights: - Add admin payment records dashboard - Sanitize API responses to protect sensitive payment data - Add search, filtering, sorting, and pagination - Display payment summary metrics - Support responsive desktop and mobile layouts - Add accessible transaction details dialog - Protect endpoints with admin authentication and authorization - Add documentation for the payment records API - Include testing validation and build verification Closes #9.
1 parent 426f297 commit fe20023

7 files changed

Lines changed: 1366 additions & 129 deletions

File tree

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
const ActivityLog = require("../schemas/activityLogModel");
2+
3+
const ALLOWED_ACTIONS = new Set(["login", "logout"]);
4+
const ALLOWED_ROLES = new Set(["admin", "student", "teacher"]);
5+
const ALLOWED_SORTS = new Set(["newest", "oldest"]);
6+
7+
const escapeRegex = (value = "") =>
8+
value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9+
10+
const parsePositiveInteger = (value, fallback, max) => {
11+
const parsed = Number.parseInt(value, 10);
12+
13+
if (!Number.isFinite(parsed) || parsed < 1) {
14+
return fallback;
15+
}
16+
17+
return Math.min(parsed, max);
18+
};
19+
20+
const parseDateBoundary = (value, endOfDay = false) => {
21+
if (!value) return null;
22+
23+
const parsed = new Date(value);
24+
25+
if (Number.isNaN(parsed.getTime())) {
26+
return null;
27+
}
28+
29+
if (endOfDay && /^\d{4}-\d{2}-\d{2}$/.test(value)) {
30+
parsed.setHours(23, 59, 59, 999);
31+
}
32+
33+
return parsed;
34+
};
35+
36+
const getActivityLogsController = async (req, res) => {
37+
try {
38+
const page = parsePositiveInteger(req.query.page, 1, 100000);
39+
const limit = parsePositiveInteger(req.query.limit, 10, 50);
40+
41+
const search = String(req.query.search || "").trim().slice(0, 120);
42+
const action = String(req.query.activity || "").trim().toLowerCase();
43+
const role = String(req.query.role || "").trim().toLowerCase();
44+
const sort = String(req.query.sort || "newest").trim().toLowerCase();
45+
46+
if (action && !ALLOWED_ACTIONS.has(action)) {
47+
return res.status(400).send({
48+
success: false,
49+
message: "Invalid activity filter.",
50+
});
51+
}
52+
53+
if (role && !ALLOWED_ROLES.has(role)) {
54+
return res.status(400).send({
55+
success: false,
56+
message: "Invalid role filter.",
57+
});
58+
}
59+
60+
if (!ALLOWED_SORTS.has(sort)) {
61+
return res.status(400).send({
62+
success: false,
63+
message: "Invalid sort option.",
64+
});
65+
}
66+
67+
const startDate = parseDateBoundary(req.query.startDate);
68+
const endDate = parseDateBoundary(req.query.endDate, true);
69+
70+
if (req.query.startDate && !startDate) {
71+
return res.status(400).send({
72+
success: false,
73+
message: "Invalid start date.",
74+
});
75+
}
76+
77+
if (req.query.endDate && !endDate) {
78+
return res.status(400).send({
79+
success: false,
80+
message: "Invalid end date.",
81+
});
82+
}
83+
84+
if (startDate && endDate && startDate > endDate) {
85+
return res.status(400).send({
86+
success: false,
87+
message: "Start date cannot be after end date.",
88+
});
89+
}
90+
91+
const query = {};
92+
93+
if (action) {
94+
query.action = action;
95+
}
96+
97+
if (role) {
98+
query.role = new RegExp(`^${escapeRegex(role)}$`, "i");
99+
}
100+
101+
if (startDate || endDate) {
102+
query.timestamp = {};
103+
104+
if (startDate) query.timestamp.$gte = startDate;
105+
if (endDate) query.timestamp.$lte = endDate;
106+
}
107+
108+
if (search) {
109+
const searchRegex = new RegExp(escapeRegex(search), "i");
110+
111+
query.$or = [
112+
{ email: searchRegex },
113+
{ role: searchRegex },
114+
{ action: searchRegex },
115+
{ ipAddress: searchRegex },
116+
{ userAgent: searchRegex },
117+
];
118+
}
119+
120+
const totalItems = await ActivityLog.countDocuments(query);
121+
const totalPages = Math.max(1, Math.ceil(totalItems / limit));
122+
const safePage = Math.min(page, totalPages);
123+
const skip = (safePage - 1) * limit;
124+
125+
const logs = await ActivityLog.find(query)
126+
.select(
127+
"userId action timestamp role email ipAddress userAgent createdAt",
128+
)
129+
.populate("userId", "name email type")
130+
.sort({ timestamp: sort === "oldest" ? 1 : -1 })
131+
.skip(skip)
132+
.limit(limit)
133+
.lean();
134+
135+
const sanitizedLogs = logs.map((log) => ({
136+
id: String(log._id),
137+
user: {
138+
id: log.userId?._id ? String(log.userId._id) : null,
139+
name: log.userId?.name || null,
140+
email: log.email || log.userId?.email || "Unknown",
141+
role: log.role || log.userId?.type || "Unknown",
142+
},
143+
activity: log.action,
144+
timestamp: log.timestamp || log.createdAt,
145+
ipAddress: log.ipAddress || null,
146+
userAgent: log.userAgent || null,
147+
}));
148+
149+
return res.status(200).send({
150+
success: true,
151+
data: sanitizedLogs,
152+
pagination: {
153+
page: safePage,
154+
limit,
155+
totalItems,
156+
totalPages,
157+
hasPreviousPage: safePage > 1,
158+
hasNextPage: safePage < totalPages,
159+
},
160+
filters: {
161+
search,
162+
activity: action,
163+
role,
164+
startDate: startDate?.toISOString() || null,
165+
endDate: endDate?.toISOString() || null,
166+
sort,
167+
},
168+
});
169+
} catch (error) {
170+
console.error("Unable to retrieve activity logs:", error);
171+
172+
return res.status(500).send({
173+
success: false,
174+
message: "Unable to retrieve activity logs.",
175+
});
176+
}
177+
};
178+
179+
module.exports = {
180+
getActivityLogsController,
181+
};

backend/routers/adminRoutes.js

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,81 @@
11
const express = require("express");
2+
23
const authMiddleware = require("../middlewares/authMiddleware");
4+
const checkRole = require("../middlewares/roleMiddleware");
5+
36
const {
4-
getAllUsersController,
5-
getAllCoursesController,
7+
adminLoginController,
8+
adminResetPasswordController,
69
deleteCourseController,
710
deleteUserController,
11+
getAllCoursesController,
12+
getAllEnrolledCoursesController,
13+
getAllPaymentsController,
14+
getAllUsersController,
815
} = require("../controllers/adminController");
916

10-
const checkRole = require("../middlewares/roleMiddleware");
17+
const {
18+
getActivityLogsController,
19+
} = require("../controllers/activityLogController");
1120

1221
const router = express.Router();
1322

14-
// Admin login route (no auth middleware)
15-
const { adminLoginController } = require("../controllers/adminController");
1623
router.post("/login", adminLoginController);
1724

25+
router.get(
26+
"/getallusers",
27+
authMiddleware,
28+
checkRole(["admin"]),
29+
getAllUsersController,
30+
);
31+
32+
router.get(
33+
"/enrolled-courses",
34+
authMiddleware,
35+
checkRole(["admin"]),
36+
getAllEnrolledCoursesController,
37+
);
1838

19-
router.get("/getallusers", authMiddleware, checkRole(["admin"]), getAllUsersController);
20-
router.get("/enrolled-courses", authMiddleware, checkRole(["admin"]), require("../controllers/adminController").getAllEnrolledCoursesController);
21-
router.get("/payments", authMiddleware, checkRole(["admin"]), require("../controllers/adminController").getAllPaymentsController);
39+
router.get(
40+
"/payments",
41+
authMiddleware,
42+
checkRole(["admin"]),
43+
getAllPaymentsController,
44+
);
2245

23-
router.get("/getallcourses", authMiddleware, checkRole(["admin"]), getAllCoursesController);
46+
router.get(
47+
"/activity-logs",
48+
authMiddleware,
49+
checkRole(["admin"]),
50+
getActivityLogsController,
51+
);
2452

25-
router.delete('/deletecourse/:courseid', authMiddleware, checkRole(["admin"]), deleteCourseController)
53+
router.get(
54+
"/getallcourses",
55+
authMiddleware,
56+
checkRole(["admin"]),
57+
getAllCoursesController,
58+
);
2659

27-
router.delete('/deleteuser/:cuserid', authMiddleware, checkRole(["admin"]), deleteUserController)
60+
router.delete(
61+
"/deletecourse/:courseid",
62+
authMiddleware,
63+
checkRole(["admin"]),
64+
deleteCourseController,
65+
);
66+
67+
router.delete(
68+
"/deleteuser/:cuserid",
69+
authMiddleware,
70+
checkRole(["admin"]),
71+
deleteUserController,
72+
);
2873

29-
// Admin reset user password
30-
router.post('/reset-password/:userid', authMiddleware, checkRole(["admin"]), require("../controllers/adminController").adminResetPasswordController)
74+
router.post(
75+
"/reset-password/:userid",
76+
authMiddleware,
77+
checkRole(["admin"]),
78+
adminResetPasswordController,
79+
);
3180

3281
module.exports = router;
Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,51 @@
1-
const mongoose = require('mongoose');
1+
const mongoose = require("mongoose");
22

3+
const activityLogSchema = new mongoose.Schema(
4+
{
5+
userId: {
6+
type: mongoose.Schema.Types.ObjectId,
7+
ref: "User",
8+
required: false,
9+
index: true,
10+
},
11+
action: {
12+
type: String,
13+
enum: ["login", "logout"],
14+
required: true,
15+
index: true,
16+
},
17+
timestamp: {
18+
type: Date,
19+
default: Date.now,
20+
index: true,
21+
},
22+
role: {
23+
type: String,
24+
trim: true,
25+
index: true,
26+
},
27+
email: {
28+
type: String,
29+
trim: true,
30+
lowercase: true,
31+
index: true,
32+
},
33+
ipAddress: {
34+
type: String,
35+
trim: true,
36+
default: null,
37+
},
38+
userAgent: {
39+
type: String,
40+
trim: true,
41+
default: null,
42+
},
43+
},
44+
{
45+
versionKey: false,
46+
},
47+
);
348

4-
const activityLogSchema = new mongoose.Schema({
5-
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: false },
6-
action: { type: String, enum: ['login', 'logout'], required: true },
7-
timestamp: { type: Date, default: Date.now },
8-
role: { type: String },
9-
email: { type: String }
10-
});
49+
activityLogSchema.index({ timestamp: -1, role: 1, action: 1 });
1150

12-
module.exports = mongoose.model('ActivityLog', activityLogSchema);
51+
module.exports = mongoose.model("ActivityLog", activityLogSchema);

0 commit comments

Comments
 (0)