feat(audit): 增加审计事件基础模块
This commit is contained in:
90
apps/server/src/modules/audit/audit.service.ts
Normal file
90
apps/server/src/modules/audit/audit.service.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import type { CurrentUser } from '../../common/auth/auth-context.service';
|
||||
import type { QueryAuditEventsDto } from './dto/query-audit-events.dto';
|
||||
|
||||
export interface AuditRecordInput {
|
||||
actor?: CurrentUser | null;
|
||||
action: string;
|
||||
entityType: string;
|
||||
entityId: string;
|
||||
productId?: string | null;
|
||||
projectId?: string | null;
|
||||
versionId?: string | null;
|
||||
scope?: unknown;
|
||||
before?: unknown;
|
||||
after?: unknown;
|
||||
metadata?: unknown;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuditService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
record(input: AuditRecordInput) {
|
||||
return this.prisma.auditEvent.create({
|
||||
data: {
|
||||
actorId: input.actor?.id ?? null,
|
||||
actorName: input.actor?.name ?? input.actor?.username ?? '',
|
||||
action: input.action,
|
||||
entityType: input.entityType,
|
||||
entityId: input.entityId,
|
||||
productId: input.productId ?? null,
|
||||
projectId: input.projectId ?? null,
|
||||
versionId: input.versionId ?? null,
|
||||
scope: toJson(input.scope ?? {}),
|
||||
before: input.before === undefined ? undefined : toJson(redactSensitive(input.before)),
|
||||
after: input.after === undefined ? undefined : toJson(redactSensitive(input.after)),
|
||||
metadata: toJson(redactSensitive(input.metadata ?? {})),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
query(query: QueryAuditEventsDto) {
|
||||
return this.prisma.auditEvent.findMany({
|
||||
where: buildWhere(query),
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: clampTake(query.take),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function buildWhere(query: QueryAuditEventsDto) {
|
||||
const where: Record<string, unknown> = {};
|
||||
for (const key of ['actorId', 'entityType', 'entityId', 'productId', 'projectId', 'versionId'] as const) {
|
||||
if (query[key]) where[key] = query[key];
|
||||
}
|
||||
|
||||
const dateRange: Record<string, Date> = {};
|
||||
if (query.dateFrom) dateRange.gte = new Date(query.dateFrom);
|
||||
if (query.dateTo) dateRange.lte = new Date(query.dateTo);
|
||||
if (Object.keys(dateRange).length > 0) where.createdAt = dateRange;
|
||||
return where;
|
||||
}
|
||||
|
||||
function clampTake(value: string | undefined): number {
|
||||
const parsed = Number(value ?? 50);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) return 50;
|
||||
return Math.min(100, Math.floor(parsed));
|
||||
}
|
||||
|
||||
function redactSensitive(value: unknown): unknown {
|
||||
if (Array.isArray(value)) return value.map((item) => redactSensitive(item));
|
||||
if (value instanceof Date) return value.toISOString();
|
||||
if (!value || typeof value !== 'object') return value;
|
||||
|
||||
return Object.fromEntries(Object.entries(value as Record<string, unknown>).map(([key, item]) => [
|
||||
key,
|
||||
isSensitiveKey(key) ? '[REDACTED]' : redactSensitive(item),
|
||||
]));
|
||||
}
|
||||
|
||||
function isSensitiveKey(key: string): boolean {
|
||||
const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
return ['password', 'token', 'secret', 'apikey', 'authorization'].some((sensitive) => normalized.includes(sensitive));
|
||||
}
|
||||
|
||||
function toJson(value: unknown): Prisma.InputJsonValue {
|
||||
return value as Prisma.InputJsonValue;
|
||||
}
|
||||
Reference in New Issue
Block a user