From b5a983dc6bfd7ba72e84e193ec6955d10daf75dd Mon Sep 17 00:00:00 2001 From: WangDL Date: Fri, 22 May 2026 23:03:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20M0-04=20Audit=20=E2=80=94=20async=20Bul?= =?UTF-8?q?lMQ=20writes=20+=20riskLevel=20+=20reason=20+=20SecurityEvent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migration.sql | 11 ++++++ prisma/schema.prisma | 20 +++++++++++ src/infrastructure/queue/queue.module.ts | 3 +- src/infrastructure/queue/queue.service.ts | 1 + .../admin-audit-log/admin-audit-log.module.ts | 8 +++-- .../admin-audit-log/audit-log.processor.ts | 26 ++++++++++++++ src/modules/admin-auth/admin-audit.service.ts | 36 ++++++++++++------- 7 files changed, 89 insertions(+), 16 deletions(-) create mode 100644 prisma/migrations/20260522230311_add_audit_risk_reason_security_event/migration.sql create mode 100644 src/modules/admin-audit-log/audit-log.processor.ts diff --git a/prisma/migrations/20260522230311_add_audit_risk_reason_security_event/migration.sql b/prisma/migrations/20260522230311_add_audit_risk_reason_security_event/migration.sql new file mode 100644 index 0000000..8f06c7e --- /dev/null +++ b/prisma/migrations/20260522230311_add_audit_risk_reason_security_event/migration.sql @@ -0,0 +1,11 @@ +ALTER TABLE AdminAuditLog ADD COLUMN riskLevel VARCHAR(16) NULL; +ALTER TABLE AdminAuditLog ADD COLUMN reason VARCHAR(500) NULL; +CREATE TABLE SecurityEvent ( + id VARCHAR(191) NOT NULL, userId VARCHAR(191), adminUserId VARCHAR(191), + eventType VARCHAR(64) NOT NULL, severity VARCHAR(16) NOT NULL DEFAULT 'low', + ip VARCHAR(45), userAgent VARCHAR(500), detail JSON, + handled BOOLEAN NOT NULL DEFAULT false, handledBy VARCHAR(100), + createdAt DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + INDEX SecurityEvent_userId_idx(userId), INDEX SecurityEvent_eventType_idx(eventType), + INDEX SecurityEvent_createdAt_idx(createdAt), PRIMARY KEY (id) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index fc9c8b8..51e4ea2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -766,6 +766,8 @@ model AdminAuditLog { afterJson Json? ip String? @db.VarChar(45) userAgent String? @db.VarChar(500) + riskLevel String? @db.VarChar(16) + reason String? @db.VarChar(500) createdAt DateTime @default(now()) adminUser AdminUser @relation(fields: [adminUserId], references: [id]) @@ -881,3 +883,21 @@ model ConfigChangeLog { @@index([entityType, entityId]) @@index([createdAt]) } + +model SecurityEvent { + id String @id @default(cuid()) + userId String? + adminUserId String? + eventType String @db.VarChar(64) + severity String @default("low") @db.VarChar(16) + ip String? @db.VarChar(45) + userAgent String? @db.VarChar(500) + detail Json? + handled Boolean @default(false) + handledBy String? @db.VarChar(100) + createdAt DateTime @default(now()) + + @@index([userId]) + @@index([eventType]) + @@index([createdAt]) +} diff --git a/src/infrastructure/queue/queue.module.ts b/src/infrastructure/queue/queue.module.ts index 365e846..6662a7f 100644 --- a/src/infrastructure/queue/queue.module.ts +++ b/src/infrastructure/queue/queue.module.ts @@ -1,7 +1,7 @@ import { Global, Module } from '@nestjs/common'; import { BullModule } from '@nestjs/bullmq'; import { ConfigService } from '@nestjs/config'; -import { QueueService, QUEUE_AI_ANALYSIS, QUEUE_DOCUMENT_IMPORT, QUEUE_NOTIFICATION } from './queue.service'; +import { QueueService, QUEUE_AI_ANALYSIS, QUEUE_AUDIT_LOG, QUEUE_DOCUMENT_IMPORT, QUEUE_NOTIFICATION } from './queue.service'; @Global() @Module({ @@ -27,6 +27,7 @@ import { QueueService, QUEUE_AI_ANALYSIS, QUEUE_DOCUMENT_IMPORT, QUEUE_NOTIFICAT { name: QUEUE_AI_ANALYSIS }, { name: QUEUE_DOCUMENT_IMPORT }, { name: QUEUE_NOTIFICATION }, + { name: QUEUE_AUDIT_LOG }, ), ], providers: [QueueService], diff --git a/src/infrastructure/queue/queue.service.ts b/src/infrastructure/queue/queue.service.ts index bf1acee..01c7d8c 100644 --- a/src/infrastructure/queue/queue.service.ts +++ b/src/infrastructure/queue/queue.service.ts @@ -5,6 +5,7 @@ import { Queue } from 'bullmq'; export const QUEUE_AI_ANALYSIS = 'ai-analysis'; export const QUEUE_DOCUMENT_IMPORT = 'document-import'; export const QUEUE_NOTIFICATION = 'notification'; +export const QUEUE_AUDIT_LOG = 'audit-logs'; @Injectable() export class QueueService { diff --git a/src/modules/admin-audit-log/admin-audit-log.module.ts b/src/modules/admin-audit-log/admin-audit-log.module.ts index 1aef88d..a7b4d75 100644 --- a/src/modules/admin-audit-log/admin-audit-log.module.ts +++ b/src/modules/admin-audit-log/admin-audit-log.module.ts @@ -1,11 +1,15 @@ import { Module } from '@nestjs/common'; +import { BullModule } from '@nestjs/bullmq'; +import { AuditLogProcessor } from './audit-log.processor'; +import { PrismaService } from '../../infrastructure/database/prisma.service'; +import { QUEUE_AUDIT_LOG } from '../../infrastructure/queue/queue.service'; import { AdminAuditLogController } from './admin-audit-log.controller'; import { AdminAuditLogService } from './admin-audit-log.service'; import { AdminAuthModule } from '../admin-auth/admin-auth.module'; @Module({ - imports: [AdminAuthModule], + imports: [AdminAuthModule, BullModule.registerQueue({ name: QUEUE_AUDIT_LOG })], controllers: [AdminAuditLogController], - providers: [AdminAuditLogService], + providers: [AuditLogProcessor, PrismaService,AdminAuditLogService], }) export class AdminAuditLogModule {} diff --git a/src/modules/admin-audit-log/audit-log.processor.ts b/src/modules/admin-audit-log/audit-log.processor.ts new file mode 100644 index 0000000..9a187ec --- /dev/null +++ b/src/modules/admin-audit-log/audit-log.processor.ts @@ -0,0 +1,26 @@ +import { Processor, WorkerHost } from '@nestjs/bullmq'; +import { Job } from 'bullmq'; +import { PrismaService } from '../../infrastructure/database/prisma.service'; +import { QUEUE_AUDIT_LOG } from '../../infrastructure/queue/queue.service'; + +@Processor(QUEUE_AUDIT_LOG) +export class AuditLogProcessor extends WorkerHost { + constructor(private readonly prisma: PrismaService) { super(); } + + async process(job: Job) { + await this.prisma.adminAuditLog.create({ + data: { + adminUserId: job.data.adminUserId, + action: job.data.action, + resourceType: job.data.resourceType, + resourceId: job.data.resourceId, + beforeJson: job.data.beforeJson, + afterJson: job.data.afterJson, + ip: job.data.ip, + userAgent: job.data.userAgent, + riskLevel: job.data.riskLevel, + reason: job.data.reason, + }, + }); + } +} diff --git a/src/modules/admin-auth/admin-audit.service.ts b/src/modules/admin-auth/admin-audit.service.ts index 5a3fb37..0f5871b 100644 --- a/src/modules/admin-auth/admin-audit.service.ts +++ b/src/modules/admin-auth/admin-audit.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { PrismaService } from '../../infrastructure/database/prisma.service'; +import { QueueService } from '../../infrastructure/queue/queue.service'; export interface AuditLogInput { adminUserId: string; @@ -10,24 +10,34 @@ export interface AuditLogInput { afterJson?: any; ip?: string; userAgent?: string; + riskLevel?: 'low' | 'medium' | 'high' | 'critical'; + reason?: string; } @Injectable() export class AdminAuditService { - constructor(private readonly prisma: PrismaService) {} + constructor(private readonly queue: QueueService) {} + /** Async audit log via BullMQ — never blocks the main operation */ async log(input: AuditLogInput) { - return this.prisma.adminAuditLog.create({ - data: { - adminUserId: input.adminUserId, - action: input.action, - resourceType: input.resourceType ?? null, - resourceId: input.resourceId ?? null, - beforeJson: input.beforeJson ?? null, - afterJson: input.afterJson ?? null, - ip: input.ip ?? null, - userAgent: input.userAgent ?? null, - }, + await this.queue.add('audit-logs', { + adminUserId: input.adminUserId, + action: input.action, + resourceType: input.resourceType ?? null, + resourceId: input.resourceId ?? null, + beforeJson: input.beforeJson ?? null, + afterJson: input.afterJson ?? null, + ip: input.ip ?? null, + userAgent: input.userAgent ?? null, + riskLevel: input.riskLevel ?? this.defaultRisk(input.action), + reason: input.reason ?? null, }); } + + private defaultRisk(action: string): string { + if (/delete|remove|purge|revoke|reset|transfer/i.test(action)) return 'critical'; + if (/update|edit|disable|block|freeze/i.test(action)) return 'high'; + if (/create|add|import|upload/i.test(action)) return 'medium'; + return 'low'; + } }