forked from Deen-Bridge/dnb-backend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAuditLog.js
More file actions
169 lines (147 loc) · 5.64 KB
/
Copy pathAuditLog.js
File metadata and controls
169 lines (147 loc) · 5.64 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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
// models/AuditLog.js
//
// Append-only audit log for security- and financial-sensitive actions.
//
// RETENTION POLICY
// ─────────────────
// Financial rows (action prefix "payment.*", "payout.*") are retained
// indefinitely by default. No TTL index is set. Auth/wallet rows follow
// the same policy for now. A future ops decision may archive rows older
// than N years to cold storage; update this comment and add an index at
// that time — do NOT add auto-expiry to financial rows without an explicit
// compliance sign-off.
import mongoose from "mongoose";
// ── Action enum ────────────────────────────────────────────────────────────
// New categories should be added here (and mirrored in auditService.js)
// before instrumenting new controllers.
export const AUDIT_ACTIONS = Object.freeze({
// Auth
AUTH_REGISTER_SUCCESS: "auth.register.success",
AUTH_REGISTER_FAILURE: "auth.register.failure",
AUTH_LOGIN_SUCCESS: "auth.login.success",
AUTH_LOGIN_FAILURE: "auth.login.failure",
AUTH_LOGOUT: "auth.logout",
AUTH_PASSWORD_RESET_REQUEST: "auth.password_reset.request",
AUTH_PASSWORD_RESET_COMPLETE: "auth.password_reset.complete",
AUTH_PASSWORD_CHANGE: "auth.password_change",
// Wallet
WALLET_CONNECT_SUCCESS: "wallet.connect.success",
WALLET_CONNECT_FAILURE: "wallet.connect.failure",
WALLET_DISCONNECT: "wallet.disconnect",
WALLET_REASSIGN_ATTEMPT: "wallet.reassign.attempt",
// Payments
PAYMENT_INITIALIZE: "payment.initialize",
PAYMENT_SUBMIT_CONFIRMED: "payment.submit.confirmed",
PAYMENT_SUBMIT_FAILED: "payment.submit.failed",
PAYMENT_CANCEL: "payment.cancel",
// Entitlements (access grants)
ENTITLEMENT_GRANT: "entitlement.grant",
// Extension points — to be instrumented with issue #20 / #28
PAYOUT_BATCH_INITIATED: "payout.batch.initiated",
PAYOUT_BATCH_CONFIRMED: "payout.batch.confirmed",
ROLE_CHANGE: "role.change",
});
const ACTION_VALUES = Object.values(AUDIT_ACTIONS);
// ── Schema ─────────────────────────────────────────────────────────────────
const auditLogSchema = new mongoose.Schema(
{
/** The security/financial action that occurred. */
action: {
type: String,
enum: ACTION_VALUES,
required: [true, "action is required"],
},
/** The authenticated user who performed the action.
* Null for pre-authentication failures (e.g. login with unknown email). */
actor: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
default: null,
},
/** Client IP address at the time of the action. */
actorIp: {
type: String,
default: null,
},
/** Raw User-Agent string. */
actorUserAgent: {
type: String,
default: null,
},
/** The kind of resource affected (e.g. "User", "Transaction", "Wallet"). */
targetType: {
type: String,
default: null,
},
/**
* The identifier of the affected resource (Mongo ObjectId as string,
* email address, public key, etc.).
*/
targetId: {
type: String,
default: null,
},
/** Whether the action succeeded or failed. */
status: {
type: String,
enum: ["success", "failure"],
required: [true, "status is required"],
},
/**
* Structured context for the event. Only an explicit allowlist of keys
* is persisted (enforced in auditService.js) — no secrets, passwords,
* tokens, or full wallet balances reach this field.
*/
metadata: {
type: mongoose.Schema.Types.Mixed,
default: null,
},
/** Correlates to the X-Request-Id header / req.id set in app.js. */
requestId: {
type: String,
default: null,
},
},
{
// Only createdAt is meaningful; updatedAt would imply mutability.
timestamps: { createdAt: true, updatedAt: false },
versionKey: false,
// Collection name is explicit so it never clashes with any future model.
collection: "auditlogs",
}
);
// ── Indexes ────────────────────────────────────────────────────────────────
auditLogSchema.index({ actor: 1, createdAt: -1 });
auditLogSchema.index({ action: 1, createdAt: -1 });
auditLogSchema.index({ targetType: 1, targetId: 1 });
// ── Append-only enforcement ─────────────────────────────────────────────────
// These hooks ensure no document can be mutated or deleted at the model
// layer regardless of how the model is imported.
const MUTATION_ERROR =
"AuditLog is append-only: update and delete operations are forbidden.";
// Block save() on existing documents
auditLogSchema.pre("save", function (next) {
if (!this.isNew) {
return next(new Error(MUTATION_ERROR));
}
next();
});
// Block query-based updates
for (const hook of [
"updateOne",
"updateMany",
"findOneAndUpdate",
"findByIdAndUpdate",
"replaceOne",
]) {
auditLogSchema.pre(hook, function (next) {
next(new Error(MUTATION_ERROR));
});
}
// Block query-based deletes
for (const hook of ["deleteOne", "deleteMany", "findOneAndDelete", "findByIdAndDelete"]) {
auditLogSchema.pre(hook, function (next) {
next(new Error(MUTATION_ERROR));
});
}
export default mongoose.model("AuditLog", auditLogSchema);