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

65 lines
2.3 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
* `@hivegrid/db/audit-events.server` which already handles redaction.
*/
import { createAuditEvent } from "@hivegrid/db/audit-events.server";
import { appendPreauthAdminSsoAuditEvent } from "@hivegrid/db/admin-sso.server";
import { getRlsExecutionContext } from "@hivegrid/db/rls-context.server";
import type { Prisma } from "@hivegrid/db/prisma-browser";
import { getTrustedClientIpOrNull } from "~/utils/server/IpUtils";
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 ip = getTrustedClientIpOrNull(request);
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 {
const auditInput = {
action: input.action,
actorType: "admin",
actorId: input.actorAdminUserId ?? null,
sourceModule: "admin-sso",
outcome: input.action.endsWith(".failure") ? "failure" : "success",
dataSensitivity: "internal",
metadata: {
ssoSubjectId: input.ssoSubjectId ?? null,
ip,
userAgent,
...input.meta,
} as unknown as Prisma.InputJsonValue,
} as const;
if (getRlsExecutionContext()?.userId) {
await createAuditEvent(auditInput);
} else {
await appendPreauthAdminSsoAuditEvent(auditInput);
}
} catch {
// Audit failures must never block the auth flow. The DB-level error
// already lands in Sentry via the audit-events boundary.
}
}