operation-cycle / api-server /src /lib /audit-logger.ts
o134's picture
Upload full TeamTasker system with all fixes
8314cf4 verified
Raw
History Blame Contribute Delete
3.77 kB
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");
});
}