Fundament: Audit mit IP/User-Agent, Mail-Absender je Mandant
- Migration audit_request_context: ip_address, user_agent an audit_logs (Spec §26) - writeAuditLog/writePlatformAudit erfassen IP (X-Forwarded-For) und User-Agent aus dem Request; außerhalb eines Requests (Worker/Skripte) null - Audit-Viewer liest die neuen Spalten statt Heuristik aus before/after - deliverMail nutzt Anzeigename und Reply-To aus TenantSettings (Spec §33.2); Absenderadresse bleibt Plattform-Domain (SPF/DKIM), Header-Injection bereinigt Gate: tsc, lint, build, 24/24 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+30
-3
@@ -1,20 +1,42 @@
|
||||
import { prisma } from "./db";
|
||||
|
||||
/**
|
||||
* Audit trail (SPEC §5 AuditLog, §10): every writing action creates an entry.
|
||||
* 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: "create" | "update" | "delete" | "login" | "logout" | "export" | "import" | "denied";
|
||||
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,
|
||||
@@ -25,6 +47,8 @@ export async function writeAuditLog(entry: {
|
||||
entityId: entry.entityId,
|
||||
before: entry.before as object | undefined,
|
||||
after: entry.after as object | undefined,
|
||||
ipAddress: ctx.ipAddress,
|
||||
userAgent: ctx.userAgent,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -35,12 +59,13 @@ export async function writeAuditLog(entry: {
|
||||
*/
|
||||
export async function writePlatformAudit(entry: {
|
||||
actorId?: string;
|
||||
action: "create" | "update" | "delete" | "login" | "logout" | "export" | "import" | "denied";
|
||||
action: AuditAction;
|
||||
entity: string;
|
||||
entityId?: string;
|
||||
before?: unknown;
|
||||
after?: unknown;
|
||||
}) {
|
||||
const ctx = await requestContext();
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
tenantId: null,
|
||||
@@ -51,6 +76,8 @@ export async function writePlatformAudit(entry: {
|
||||
entityId: entry.entityId,
|
||||
before: entry.before as object | undefined,
|
||||
after: entry.after as object | undefined,
|
||||
ipAddress: ctx.ipAddress,
|
||||
userAgent: ctx.userAgent,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user