wulf-pulse/lib/services/audit.ts

198 lines
5.1 KiB
TypeScript
Raw Normal View History

import { Pool } from "pg";
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
export interface AuditLogEntry {
userId?: string;
userEmail?: string;
action: string;
resource: string;
resourceId?: string;
details?: Record<string, unknown>;
ipAddress?: string;
userAgent?: string;
}
/**
* Log an audit event
*/
export async function log(entry: AuditLogEntry): Promise<void> {
try {
const id = crypto.randomUUID();
await pool.query(
`INSERT INTO "audit_log"
(id, user_id, user_email, action, resource, resource_id, details, ip_address, user_agent)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
[
id,
entry.userId || null,
entry.userEmail || null,
entry.action,
entry.resource,
entry.resourceId || null,
entry.details ? JSON.stringify(entry.details) : null,
entry.ipAddress || null,
entry.userAgent || null,
]
);
} catch (error) {
console.error("Failed to write audit log:", error);
// Don't throw - audit logging should not break the main flow
}
}
// Convenience functions for common audit events
export const audit = {
log,
// Auth events
signIn: (userId: string, email: string, ipAddress?: string, userAgent?: string) =>
log({
userId,
userEmail: email,
action: "sign_in",
resource: "auth",
ipAddress,
userAgent,
}),
signOut: (userId: string, email: string, ipAddress?: string) =>
log({
userId,
userEmail: email,
action: "sign_out",
resource: "auth",
ipAddress,
}),
signInFailed: (email: string, reason: string, ipAddress?: string) =>
log({
userEmail: email,
action: "sign_in_failed",
resource: "auth",
details: { reason },
ipAddress,
}),
// User management events
userCreated: (actorId: string, actorEmail: string, targetUserId: string, targetEmail: string) =>
log({
userId: actorId,
userEmail: actorEmail,
action: "create",
resource: "user",
resourceId: targetUserId,
details: { targetEmail },
}),
userUpdated: (actorId: string, actorEmail: string, targetUserId: string, changes: Record<string, unknown>) =>
log({
userId: actorId,
userEmail: actorEmail,
action: "update",
resource: "user",
resourceId: targetUserId,
details: { changes },
}),
userDeleted: (actorId: string, actorEmail: string, targetUserId: string, targetEmail: string) =>
log({
userId: actorId,
userEmail: actorEmail,
action: "delete",
resource: "user",
resourceId: targetUserId,
details: { targetEmail },
}),
userBanned: (actorId: string, actorEmail: string, targetUserId: string, reason?: string) =>
log({
userId: actorId,
userEmail: actorEmail,
action: "ban",
resource: "user",
resourceId: targetUserId,
details: { reason },
}),
userUnbanned: (actorId: string, actorEmail: string, targetUserId: string) =>
log({
userId: actorId,
userEmail: actorEmail,
action: "unban",
resource: "user",
resourceId: targetUserId,
}),
userRoleChanged: (actorId: string, actorEmail: string, targetUserId: string, oldRole: string, newRole: string) =>
log({
userId: actorId,
userEmail: actorEmail,
action: "role_change",
resource: "user",
resourceId: targetUserId,
details: { oldRole, newRole },
}),
// Role management events
roleCreated: (actorId: string, actorEmail: string, roleId: string, roleName: string) =>
log({
userId: actorId,
userEmail: actorEmail,
action: "create",
resource: "role",
resourceId: roleId,
details: { roleName },
}),
roleUpdated: (actorId: string, actorEmail: string, roleId: string, changes: Record<string, unknown>) =>
log({
userId: actorId,
userEmail: actorEmail,
action: "update",
resource: "role",
resourceId: roleId,
details: { changes },
}),
roleDeleted: (actorId: string, actorEmail: string, roleId: string, roleName: string) =>
log({
userId: actorId,
userEmail: actorEmail,
action: "delete",
resource: "role",
resourceId: roleId,
details: { roleName },
}),
// Settings events
settingsUpdated: (actorId: string, actorEmail: string, setting: string, oldValue: unknown, newValue: unknown) =>
log({
userId: actorId,
userEmail: actorEmail,
action: "update",
resource: "settings",
resourceId: setting,
details: { oldValue, newValue },
}),
};
/**
* Purge old audit logs based on retention policy
*/
export async function purgeOldLogs(retentionDays?: number): Promise<number> {
const days = retentionDays || parseInt(process.env.AUDIT_LOG_RETENTION_DAYS || "90");
try {
const result = await pool.query(
`DELETE FROM "audit_log" WHERE timestamp < NOW() - INTERVAL '${days} days'`
);
return result.rowCount || 0;
} catch (error) {
console.error("Failed to purge audit logs:", error);
return 0;
}
}