import { prisma } from "./db"; /** * Audit trail (spec §26): every writing action creates an entry. * Uses the raw client on purpose — audit writes must never be silently * filtered, and tenantId is passed explicitly by the caller. */ type AuditAction = "create" | "update" | "delete" | "login" | "logout" | "export" | "import" | "denied"; /** * IP address and user agent of the current request, if there is one. * Outside a request scope (workers, scripts, tests) `headers()` throws → both null. * Behind the Coolify/Traefik proxy the client IP is the first X-Forwarded-For hop. */ async function requestContext(): Promise<{ ipAddress: string | null; userAgent: string | null }> { try { const { headers } = await import("next/headers"); const h = await headers(); const forwarded = h.get("x-forwarded-for")?.split(",")[0]?.trim(); const ip = forwarded || h.get("x-real-ip")?.trim() || null; const ua = h.get("user-agent"); return { ipAddress: ip ? ip.slice(0, 64) : null, userAgent: ua ? ua.slice(0, 512) : null }; } catch { return { ipAddress: null, userAgent: null }; } } export async function writeAuditLog(entry: { tenantId: string; actorId?: string; action: AuditAction; scope?: "tenant" | "platform"; entity: string; entityId?: string; before?: unknown; after?: unknown; }) { const ctx = await requestContext(); await prisma.auditLog.create({ data: { tenantId: entry.tenantId, scope: entry.scope ?? "tenant", actorId: entry.actorId, action: entry.action, entity: entry.entity, entityId: entry.entityId, before: entry.before as object | undefined, after: entry.after as object | undefined, ipAddress: ctx.ipAddress, userAgent: ctx.userAgent, }, }); } /** * Audit-Eintrag der Plattform-Ebene (kein Mandantenbezug, scope="platform"). * Für Superadmin-Anmeldungen und -Aktionen (Phase-1-Härtung Paket 2). */ export async function writePlatformAudit(entry: { actorId?: string; action: AuditAction; entity: string; entityId?: string; before?: unknown; after?: unknown; }) { const ctx = await requestContext(); await prisma.auditLog.create({ data: { tenantId: null, scope: "platform", actorId: entry.actorId, action: entry.action, entity: entry.entity, entityId: entry.entityId, before: entry.before as object | undefined, after: entry.after as object | undefined, ipAddress: ctx.ipAddress, userAgent: ctx.userAgent, }, }); }