import { Injectable } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { PinoLogger } from 'nestjs-pino'; import { PrismaService } from '../prisma/prisma.service'; export interface AuditEvent { /** Stable dot-namespaced id, e.g. `grant.created` — the UI translates it. */ action: string; /** The acting user; null/undefined for anonymous events. */ actorId?: string | null; targetType?: string; targetId?: string; /** Small, structured context — never secrets, tokens, or page content. */ details?: Record; } /** * The persistent audit trail (issue #86, security.md §Logging): auth events * and admin actions land as queryable rows for the Site-Admin viewer AND as * the established `audit: …` stdout line. Content activity (pages, files, * exports, labels) intentionally stays log-only — the trail answers "who * changed access/configuration", not "who edited what". * * Recording must never break the audited operation: failures are logged and * swallowed (the stdout line still fired, so nothing is silently lost). */ @Injectable() export class AuditService { constructor( private readonly prisma: PrismaService, private readonly logger: PinoLogger, ) { this.logger.setContext(AuditService.name); } async record(event: AuditEvent): Promise { const { action, actorId, targetType, targetId, details } = event; this.logger.info( { actor: actorId ?? null, targetType, targetId, ...details }, `audit: ${action}`, ); try { await this.prisma.auditEntry.create({ data: { action, actorId: actorId ?? null, targetType, targetId, details: details ? (details as Prisma.InputJsonObject) : undefined, }, }); } catch (error) { this.logger.error({ action, err: error }, 'audit entry could not be persisted'); } } }