import pg from 'pg'; import { PrismaPg } from '@prisma/adapter-pg'; import { PrismaClient } from '$lib/generated/prisma/client'; import { env } from '$env/dynamic/private'; import { getAuditContext } from '$lib/server/audit-context'; import { describeAction, entityIdFor, sanitizeAuditValue, shouldAudit } from '$lib/server/audit'; let _prisma: PrismaClient | undefined; let _base: PrismaClient | undefined; let _pool: pg.Pool | undefined; function getBase(): { base: PrismaClient; pool: pg.Pool } { if (!_base || !_pool) { _pool = new pg.Pool({ connectionString: env.DATABASE_URL }); const adapter = new PrismaPg(_pool); _base = new PrismaClient({ adapter }); } return { base: _base, pool: _pool }; } const roleNameCache = new Map(); async function resolveRoleName(base: PrismaClient, roleId: number | null): Promise { if (roleId === null) return null; const cached = roleNameCache.get(roleId); if (cached) return cached; try { const role = await base.userRole.findUnique({ where: { id: roleId }, select: { name: true } }); if (role?.name) { roleNameCache.set(roleId, role.name); return role.name; } } catch (e) { console.error('Failed to resolve role name for audit log:', e); } return null; } type Delegate = { findFirst?: (args: unknown) => Promise; findMany?: (args: unknown) => Promise; }; function delegateFor(base: PrismaClient, model: string): Delegate | undefined { const key = model.charAt(0).toLowerCase() + model.slice(1); return (base as unknown as Record)[key] as Delegate | undefined; } async function readBefore( base: PrismaClient, model: string, operation: string, args: unknown ): Promise { if (operation.startsWith('create')) return null; const delegate = delegateFor(base, model); if (!delegate) return null; const where = (args as { where?: unknown } | null)?.where; try { if (operation === 'updateMany' || operation === 'deleteMany') { return delegate.findMany ? await delegate.findMany({ where, take: 50 }) : null; } return delegate.findFirst ? await delegate.findFirst({ where }) : null; } catch { return null; } } async function writeAuditRow( pool: pg.Pool, base: PrismaClient, entry: { userId: string | null; email: string | null; roleId: number | null; page: string; action: string; entity: string; entityId: string | null; before: unknown; after: unknown; ip: string | null; } ): Promise { const roleName = await resolveRoleName(base, entry.roleId); await pool.query( `INSERT INTO "AuditLog" ("id", "createdAt", "userId", "email", "roleId", "roleName", "page", "action", "entity", "entityId", "before", "after", "ip") VALUES ($1, now(), $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb, $11::jsonb, $12)`, [ crypto.randomUUID(), entry.userId, entry.email, entry.roleId, roleName, entry.page, entry.action, entry.entity, entry.entityId, entry.before === null ? null : JSON.stringify(entry.before), entry.after === null ? null : JSON.stringify(entry.after), entry.ip ] ); } function getPrisma(): PrismaClient { if (_prisma) return _prisma; const { base, pool } = getBase(); const extended = base.$extends({ query: { $allModels: { async $allOperations({ model, operation, args, query }) { if (!shouldAudit(model, operation)) { return query(args); } const before = await readBefore(base, model, operation, args); const result = await query(args); try { const ctx = getAuditContext(); const res = result as { id?: unknown; email?: string; roleId?: number | null } | null; let userId = ctx?.userId ?? null; let email = ctx?.email ?? null; let roleId = ctx?.roleId ?? null; // Self-signup: attribute the new user to itself since no session exists yet. if (!userId && model === 'User' && operation === 'create' && res) { userId = (res.id as string | undefined) ?? null; email = res.email ?? null; roleId = res.roleId ?? null; } const after = operation === 'updateMany' || operation === 'deleteMany' ? { count: (result as { count?: number } | null)?.count ?? null } : operation.startsWith('delete') ? null : result; await writeAuditRow(pool, base, { userId, email, roleId, page: ctx?.page ?? '/system', action: describeAction(operation), entity: model, entityId: entityIdFor(args, result, before), before: sanitizeAuditValue(before), after: sanitizeAuditValue(after), ip: ctx?.ip ?? null }); } catch (e) { console.error('Failed to write audit log:', e); } return result; } } } }); _prisma = extended as unknown as PrismaClient; return _prisma; } export const prisma = new Proxy({} as PrismaClient, { get(_, prop) { return (getPrisma() as unknown as Record)[prop]; } });