import { logger } from "./logger.js"; import { Request } from "express"; import { db, auditLogsTable } from "@workspace/db"; export type AuditAction = | "login_success" | "LOGIN_SUCCESS" | "login_failure" | "LOGIN_FAILURE" | "unauthorized_access" | "role_change" | "USER_ROLE_CHANGE" | "USER_CREATE" | "task_created" | "TASK_CREATE" | "task_updated" | "TASK_STATUS_CHANGE" | "task_deleted" | "client_created" | "CLIENT_CREATE" | "client_updated" | "CLIENT_UPDATE" | "CLIENT_TOGGLE_ACTIVE" | string; export interface AuditLogOptions { userId?: number | string; action: AuditAction; entityType?: string; entityId?: number; details?: Record; req?: Request; } function mapActionToEvent(action: string): string { const upper = action.toUpperCase(); if (upper === "LOGIN_SUCCESS") return "login_success"; if (upper === "LOGIN_FAILURE") return "login_failure"; if (upper === "UNAUTHORIZED_ACCESS") return "unauthorized_access"; if (upper === "USER_ROLE_CHANGE" || upper === "ROLE_CHANGE" || upper === "USER_CREATE") return "role_change"; if (upper === "TASK_CREATE" || upper === "TASK_CREATED") return "task_created"; if (upper === "TASK_STATUS_CHANGE" || upper === "TASK_UPDATED") return "task_updated"; if (upper === "TASK_DELETED") return "task_deleted"; if (upper === "CLIENT_CREATE" || upper === "CLIENT_CREATED") return "client_created"; if (upper === "CLIENT_UPDATE" || upper === "CLIENT_TOGGLE_ACTIVE" || upper === "CLIENT_UPDATED") return "client_updated"; return action.toLowerCase(); } function sanitizeDetails(details?: Record): Record | undefined { if (!details) return undefined; const sanitized: Record = {}; for (const [key, value] of Object.entries(details)) { const lowerKey = key.toLowerCase(); if ( lowerKey.includes("password") || lowerKey.includes("jwt") || lowerKey.includes("token") || lowerKey.includes("secret") || lowerKey.includes("authorization") ) { sanitized[key] = "[REDACTED]"; } else if (typeof value === "object" && value !== null) { sanitized[key] = sanitizeDetails(value); } else { sanitized[key] = value; } } return sanitized; } export function logAudit(options: AuditLogOptions) { const { userId, action, entityType, entityId, details, req } = options; const authReq = req as any; let ipAddress = req?.ip || req?.headers["x-forwarded-for"] || null; if (Array.isArray(ipAddress)) { ipAddress = ipAddress[0]; } const userAgent = req?.headers["user-agent"] || null; const event = mapActionToEvent(action); const supabaseUid = authReq?.userUid || details?.supabaseUid || (userId && String(userId).includes("-") ? String(userId) : null); const sanitizedDetails = sanitizeDetails({ ...details, supabaseUid, ipAddress, userAgent, }); // Prefer internal integer userId from req.userId or options.userId const internalUserId = authReq?.userId ? String(authReq.userId) : (userId && !String(userId).includes("-") ? String(userId) : "system"); logger.info({ audit: true, userId: internalUserId, event, entityType, entityId, details: sanitizedDetails, timestamp: new Date().toISOString() }, `[AUDIT] ${event} on ${entityType ?? "system"}${entityId ? ` #${entityId}` : ""} by user #${internalUserId}`); db.insert(auditLogsTable).values({ event, entityId: entityId ? Number(entityId) : null, entityType: entityType ? String(entityType) : null, performedById: internalUserId, details: sanitizedDetails ?? null, }).catch(err => { logger.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to write audit log to database"); }); }