Files
lehuo/apps/api/src/audit/audit.service.ts
T
2026-08-12 14:35:12 +08:00

68 lines
2.3 KiB
TypeScript

import { Inject, Injectable } from '@nestjs/common';
import type { Prisma } from '../generated/prisma/client';
import type { AuditResult } from '../generated/prisma/enums';
import type { AuthContext, RequestContext } from '../common/request-context';
import { PrismaService } from '../database/prisma.service';
import { SensitiveDataService } from '../security/sensitive-data.service';
const SENSITIVE_METADATA_KEY =
/(password|token|secret|openid|unionid|mobile|phone|address|description|content)/i;
export interface AuditEvent {
action: string;
result: AuditResult;
actor?: AuthContext;
actorUserId?: bigint;
actorRole?: string;
resourceType?: string;
resourcePublicId?: string;
metadata?: Record<string, string | number | boolean | null>;
}
@Injectable()
export class AuditService {
constructor(
@Inject(PrismaService) private readonly prisma: PrismaService,
@Inject(SensitiveDataService) private readonly sensitiveData: SensitiveDataService,
) {}
async record(
request: RequestContext,
event: AuditEvent,
client: Prisma.TransactionClient | PrismaService = this.prisma,
): Promise<void> {
const metadata = this.safeMetadata(event.metadata);
const userAgent =
request
.header('user-agent')
?.replace(/[\r\n\t]/g, ' ')
.slice(0, 255) || null;
await client.auditLog.create({
data: {
requestId: request.requestId,
actorUserId: event.actor?.userId ?? event.actorUserId ?? null,
actorRole: event.actor?.roles[0] ?? event.actorRole ?? null,
action: event.action,
resourceType: event.resourceType ?? null,
resourcePublicId: event.resourcePublicId ?? null,
result: event.result,
metadataJson: metadata ?? undefined,
ipHash: request.ip ? this.sensitiveData.auditIpHash(request.ip) : null,
userAgentSummary: userAgent,
},
});
}
safeMetadata(metadata: AuditEvent['metadata']): Prisma.InputJsonObject | undefined {
if (!metadata) return undefined;
const entries = Object.entries(metadata).map(([key, value]) => {
if (SENSITIVE_METADATA_KEY.test(key)) {
throw new Error(`Sensitive audit metadata key is not allowed: ${key}`);
}
return [key, value] as const;
});
return Object.fromEntries(entries);
}
}