feat: implement sensitive operation audit retention (TASK-WP6-04)
Dada P0-A isolated Windows CI / validate-and-package (push) Failing after 59s
Dada P0-A isolated Windows CI / validate-and-package (push) Failing after 59s
This commit is contained in:
@@ -4,6 +4,7 @@ import { createRequire } from "node:module";
|
||||
import type BetterSqlite3 from "better-sqlite3";
|
||||
|
||||
import {
|
||||
auditRetentionMilliseconds,
|
||||
ensureAdminOperationAuditSchema,
|
||||
ensurePrivateAccessAuditSchema,
|
||||
isSafeAuditRef,
|
||||
@@ -281,6 +282,39 @@ export class RegistrationService {
|
||||
return { code, inviteId };
|
||||
}
|
||||
|
||||
createAdminInvite(input: { actorId: string; expiresAt: number; maxUses: number }) {
|
||||
if (!Number.isSafeInteger(input.expiresAt) || !Number.isSafeInteger(input.maxUses) || input.maxUses < 1) {
|
||||
throw new Error("Invite request is invalid.");
|
||||
}
|
||||
const code = this.options.inviteCodeGenerator();
|
||||
const inviteId = randomUUID();
|
||||
const now = this.options.clock();
|
||||
this.runImmediate("invite_create", () => {
|
||||
const admin = this.database.prepare(`
|
||||
SELECT u.user_id FROM users u JOIN admin_access a ON a.user_id = u.user_id
|
||||
WHERE u.user_id = ? AND u.role = 'super_admin' AND u.status = 'active' AND a.allowed = 1
|
||||
`).get(input.actorId);
|
||||
if (!admin) throw new RegistrationError("AUTH_SESSION_INVALID", "session_invalid");
|
||||
this.database.prepare(`
|
||||
INSERT INTO invite_codes (
|
||||
invite_id, code_hmac, max_uses, used_count, expires_at, status, created_at
|
||||
) VALUES (?, ?, ?, 0, ?, 'enabled', ?)
|
||||
`).run(inviteId, this.inviteHmac(code), input.maxUses, input.expiresAt, now);
|
||||
this.recordAdminAudit({
|
||||
actorRef: input.actorId,
|
||||
actorType: "super_admin",
|
||||
afterSummary: { max_uses: input.maxUses, status: "enabled" },
|
||||
beforeSummary: null,
|
||||
operationType: "invite_create",
|
||||
result: "succeeded",
|
||||
targetRef: inviteId,
|
||||
targetType: "invite",
|
||||
}, now);
|
||||
return { outcome: "committed", value: undefined };
|
||||
});
|
||||
return { code, inviteId };
|
||||
}
|
||||
|
||||
async sendRegistrationCode(input: { email: string; inviteCode: string }): Promise<RegistrationSendResult> {
|
||||
const email = normalizeEmail(input.email);
|
||||
const inviteCode = normalizeProfileValue(input.inviteCode, 160);
|
||||
@@ -1241,14 +1275,35 @@ export class RegistrationService {
|
||||
return outcome;
|
||||
}
|
||||
|
||||
changeUserStatus(userId: string, status: "suspended" | "deleted") {
|
||||
changeUserStatus(userId: string, status: "suspended" | "deleted", actorId?: string) {
|
||||
const now = this.options.clock();
|
||||
this.runImmediate("session_revoke", () => {
|
||||
if (actorId) {
|
||||
const admin = this.database.prepare(`
|
||||
SELECT u.user_id FROM users u JOIN admin_access a ON a.user_id = u.user_id
|
||||
WHERE u.user_id = ? AND u.role = 'super_admin' AND u.status = 'active' AND a.allowed = 1
|
||||
`).get(actorId);
|
||||
if (!admin) throw new RegistrationError("AUTH_SESSION_INVALID", "session_invalid");
|
||||
}
|
||||
const before = this.database.prepare("SELECT status FROM users WHERE user_id = ? AND role = 'user'")
|
||||
.get(userId) as { status: "active" | "suspended" | "deleted" } | undefined;
|
||||
const changed = this.database.prepare("UPDATE users SET status = ? WHERE user_id = ? AND role = 'user'")
|
||||
.run(status, userId);
|
||||
if (changed.changes !== 1) throw new RegistrationError("AUTH_SESSION_INVALID", "session_invalid");
|
||||
this.database.prepare("UPDATE sessions SET revoked_at = ? WHERE user_id = ? AND revoked_at IS NULL")
|
||||
.run(now, userId);
|
||||
if (actorId) {
|
||||
this.recordAdminAudit({
|
||||
actorRef: actorId,
|
||||
actorType: "super_admin",
|
||||
afterSummary: { status },
|
||||
beforeSummary: { status: before?.status ?? "unknown" },
|
||||
operationType: "user_status_change",
|
||||
result: "succeeded",
|
||||
targetRef: userId,
|
||||
targetType: "user_account",
|
||||
}, now);
|
||||
}
|
||||
return { outcome: "committed", value: undefined };
|
||||
});
|
||||
}
|
||||
@@ -1772,7 +1827,7 @@ export class RegistrationService {
|
||||
serializeAuditSummary(input.beforeSummary),
|
||||
serializeAuditSummary(input.afterSummary),
|
||||
now,
|
||||
now + 180 * 24 * 60 * 60 * 1_000,
|
||||
now + auditRetentionMilliseconds,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user