File size: 3,772 Bytes
8314cf4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
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");
  });
}