68 lines
2.3 KiB
TypeScript
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);
|
|
}
|
|
}
|