1import type { Request } from "express";
2import { Inject, Injectable } from "@nestjs/common";
3import type { AuthContext } from "./types.js";
4import { logger } from "./logger.js";
5import { PrismaService } from "./prisma.service.js";
6
7export type AuditActorType = "USER" | "API_KEY" | "SYSTEM" | "INTEGRATION";
8
9export interface AuditWrite {
10 actorId?: string;
11
14 actorType?: AuditActorType;
15 action: string;
16 resourceType: string;
17 resourceId: string;
18 workspaceId?: string;
19 pageId?: string;
20 metadata?: Record<string, unknown>;
21}
22
23
30@Injectable()
31export class AuditService {
32 constructor(@Inject(PrismaService) private readonly prisma: PrismaService) {}
33
34 async write(input: AuditWrite): Promise<void> {
35 try {
36 await this.prisma.auditLog.create({
37 data: {
38 actorType: input.actorId ? "USER" : "SYSTEM",
39 actorId: input.actorId ?? "system",
40 workspaceId: input.workspaceId,
41 pageId: input.pageId,
42 resourceType: input.resourceType,
43 resourceId: input.resourceId,
44 action: input.action,
45 metadata: (input.metadata ?? {}) as object,
46 },
47 });
48 } catch (err) {
49 logger.warn(`audit write failed (${input.action}): ${(err as Error).message}`);
50 }
51 }
52
53
58 async audit(
59 req: Request | undefined,
60 auth: AuthContext | undefined,
61 input: Omit<AuditWrite, "actorId" | "actorType"> & {
62 actorId?: string;
63 actorType?: AuditActorType;
64 },
65 ): Promise<void> {
66
67
68
69
70
71
72 const isKey = auth?.kind === "api";
73 const actorId =
74 input.actorId ?? (isKey ? `api:${auth.apiKeyId ?? "unknown"}` : (auth?.userId ?? undefined));
75 try {
76 await this.prisma.auditLog.create({
77 data: {
78 actorType: input.actorType ?? (isKey ? "API_KEY" : auth ? "USER" : "SYSTEM"),
79 actorId: actorId ?? "system",
80 workspaceId: input.workspaceId ?? auth?.workspaceId,
81 pageId: input.pageId,
82 resourceType: input.resourceType,
83 resourceId: input.resourceId,
84 action: input.action,
85 ipAddress: req?.ip ?? null,
86 userAgent: req?.headers["user-agent"] ?? null,
87 metadata: (input.metadata ?? {}) as object,
88 },
89 });
90 } catch (err) {
91 logger.warn(`audit write failed (${input.action}): ${(err as Error).message}`);
92 }
93 }
94}
95