import { Injectable } from '@nestjs/common'; import type { AuditLogDto, AuditLogListResponse } from '@mento/shared'; import { Prisma } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; import { buildSearchWhere, ci } from '../common/search'; export type AuditWriteInput = { organizationId: string; actorUserId?: string | null; action: string; method: string; path: string; entityType?: string | null; entityId?: string | null; summary: string; meta?: Record | null; ip?: string | null; }; @Injectable() export class AuditLogsService { constructor(private readonly prisma: PrismaService) {} async write(input: AuditWriteInput): Promise { try { await this.prisma.auditLog.create({ data: { organizationId: input.organizationId, actorUserId: input.actorUserId ?? null, action: input.action, method: input.method.slice(0, 16), path: input.path.slice(0, 400), entityType: input.entityType ?? null, entityId: input.entityId ?? null, summary: input.summary.slice(0, 500), meta: input.meta == null ? undefined : (input.meta as Prisma.InputJsonValue), ip: input.ip ?? null, }, }); } catch { // never break the main request because of audit write failures } } async list( organizationId: string, opts: { q?: string; actorUserId?: string; entityType?: string; action?: string; page?: number; limit?: number; }, ): Promise { const page = Math.max(1, opts.page ?? 1); const limit = Math.min(100, Math.max(1, opts.limit ?? 30)); const where: Prisma.AuditLogWhereInput = { organizationId, ...(opts.actorUserId ? { actorUserId: opts.actorUserId } : {}), ...(opts.entityType ? { entityType: opts.entityType } : {}), ...(opts.action ? { action: opts.action } : {}), }; const search = buildSearchWhere(opts.q, (t) => [ { summary: ci(t) }, { path: ci(t) }, { action: ci(t) }, { actor: { fullName: ci(t) } }, ]); if (search) where.AND = [search]; const [total, rows] = await Promise.all([ this.prisma.auditLog.count({ where }), this.prisma.auditLog.findMany({ where, include: { actor: { select: { id: true, fullName: true } } }, orderBy: { createdAt: 'desc' }, skip: (page - 1) * limit, take: limit, }), ]); return { total, page, limit, items: rows.map((row) => this.toDto(row)), }; } private toDto(row: { id: string; action: string; method: string; path: string; entityType: string | null; entityId: string | null; summary: string; meta: Prisma.JsonValue | null; ip: string | null; createdAt: Date; actor: { id: string; fullName: string } | null; }): AuditLogDto { const meta = row.meta && typeof row.meta === 'object' && !Array.isArray(row.meta) ? (row.meta as { label?: unknown; detail?: unknown }) : null; const label = typeof meta?.label === 'string' ? meta.label : null; const detail = typeof meta?.detail === 'string' ? meta.detail : null; return { id: row.id, action: row.action, method: row.method, path: row.path, entityType: row.entityType, entityId: row.entityId, summary: row.summary, label, detail, actor: row.actor, ip: row.ip, createdAt: row.createdAt.toISOString(), }; } }