Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 181 additions & 0 deletions backend/controllers/activityLogController.js
Original file line number Diff line number Diff line change
@@ -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,
};
75 changes: 62 additions & 13 deletions backend/routers/adminRoutes.js
Original file line number Diff line number Diff line change
@@ -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;
57 changes: 48 additions & 9 deletions backend/schemas/activityLogModel.js
Original file line number Diff line number Diff line change
@@ -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);
Loading
Loading