forked from Deen-Bridge/dnb-backend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathauditRoutes.js
More file actions
126 lines (111 loc) · 3.9 KB
/
Copy pathauditRoutes.js
File metadata and controls
126 lines (111 loc) · 3.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
// routes/admin/auditRoutes.js
//
// Read-only admin API for querying audit logs.
// Mounted at: /api/admin/audit
//
// Gate: protect (JWT) → authorizeRoles("admin")
// No create / update / delete endpoints are exposed — ever.
import express from "express";
import mongoose from "mongoose";
import AuditLog from "../../models/AuditLog.js";
import { protect, authorizeRoles } from "../../middlewares/authMiddleware.js";
import { catchAsync, APIError } from "../../middlewares/errorHandler.js";
const router = express.Router();
// Apply auth gate to every route in this file
router.use(protect, authorizeRoles("admin"));
/**
* GET /api/admin/audit
*
* Query params (all optional):
* actor — MongoDB ObjectId string
* action — exact action string (e.g. "auth.login.failure")
* targetType — e.g. "User", "Transaction", "Wallet"
* targetId — arbitrary string
* status — "success" | "failure"
* from — ISO date string (inclusive lower bound on createdAt)
* to — ISO date string (inclusive upper bound on createdAt)
* page — positive integer (default 1)
* limit — positive integer (default 20, max 100)
*/
router.get(
"/",
catchAsync(async (req, res) => {
const {
actor,
action,
targetType,
targetId,
status,
from,
to,
page = "1",
limit = "20",
} = req.query;
// ── Build filter ──────────────────────────────────────────────────────
const filter = {};
if (actor) {
if (!mongoose.Types.ObjectId.isValid(actor)) {
throw new APIError("Invalid actor ObjectId", 400);
}
filter.actor = new mongoose.Types.ObjectId(actor);
}
if (action) {
filter.action = action;
}
if (targetType) {
filter.targetType = targetType;
}
if (targetId) {
filter.targetId = targetId;
}
if (status) {
if (!["success", "failure"].includes(status)) {
throw new APIError("status must be 'success' or 'failure'", 400);
}
filter.status = status;
}
if (from || to) {
filter.createdAt = {};
if (from) {
const fromDate = new Date(from);
if (isNaN(fromDate.getTime())) throw new APIError("Invalid 'from' date", 400);
filter.createdAt.$gte = fromDate;
}
if (to) {
const toDate = new Date(to);
if (isNaN(toDate.getTime())) throw new APIError("Invalid 'to' date", 400);
filter.createdAt.$lte = toDate;
}
}
// ── Pagination ────────────────────────────────────────────────────────
const pageNum = Math.max(1, parseInt(page, 10) || 1);
const limitNum = Math.min(100, Math.max(1, parseInt(limit, 10) || 20));
const skip = (pageNum - 1) * limitNum;
// ── Query ─────────────────────────────────────────────────────────────
const [logs, total] = await Promise.all([
AuditLog.find(filter)
.sort({ createdAt: -1 })
.skip(skip)
.limit(limitNum)
.populate("actor", "name email role")
.lean(),
AuditLog.countDocuments(filter),
]);
res.status(200).json({
success: true,
logs,
pagination: {
page: pageNum,
limit: limitNum,
total,
pages: Math.ceil(total / limitNum),
},
});
})
);
// Catch-all: any non-GET method on any path under this router returns 405.
// Belt-and-suspenders on top of the model-layer append-only pre-hooks.
router.use((req, res) =>
res.status(405).json({ success: false, message: "Method not allowed on audit log" })
);
export default router;