Spaces:
Sleeping
Sleeping
| 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<string, any>; | |
| 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<string, any>): Record<string, any> | undefined { | |
| if (!details) return undefined; | |
| const sanitized: Record<string, any> = {}; | |
| 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"); | |
| }); | |
| } | |