Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/modules/admin-sso/audit.server.ts
T

55 lines
1.9 KiB
TypeScript

/**
* Phase 09 Task #82 — Thin Audit-Event wrapper for admin-sso paths.
*
* Centralises action names + metadata sanitization for the login / logout
* / fallback / mfa-challenge paths. Sits on top of
* `@toolctrl/db/audit-events.server` which already handles redaction.
*/
import { createAuditEvent } from "@toolctrl/db/audit-events.server";
import type { Prisma } from "@toolctrl/db/prisma-browser";
export type AdminSsoAuditAction =
| "admin.login.sso.success"
| "admin.login.sso.failure"
| "admin.login.fallback.success"
| "admin.login.fallback.failure"
| "admin.logout.sso"
| "admin.mfa.app.challenge.success"
| "admin.mfa.app.challenge.failure";
export type AdminSsoAuditInput = {
readonly action: AdminSsoAuditAction;
readonly actorAdminUserId?: string | null;
readonly ssoSubjectId?: string | null;
readonly request?: Request;
readonly meta?: Record<string, unknown>;
};
function clientMeta(request: Request | undefined): { ip: string | null; userAgent: string | null } {
if (!request) return { ip: null, userAgent: null };
const fwd = request.headers.get("x-forwarded-for") ?? "";
const ip = fwd.split(",")[0]?.trim() || request.headers.get("x-real-ip") || null;
const userAgent = request.headers.get("user-agent") || null;
return { ip, userAgent };
}
export async function writeAdminSsoAuditEvent(input: AdminSsoAuditInput): Promise<void> {
const { ip, userAgent } = clientMeta(input.request);
try {
await createAuditEvent({
action: input.action,
actorType: "admin",
actorId: input.actorAdminUserId ?? null,
metadata: {
ssoSubjectId: input.ssoSubjectId ?? null,
ip,
userAgent,
...input.meta,
} as unknown as Prisma.InputJsonValue,
});
} catch {
// Audit failures must never block the auth flow. The DB-level error
// already lands in Sentry via the audit-events boundary.
}
}