feat: M0-04 Audit — async BullMQ writes + riskLevel + reason + SecurityEvent
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 39s
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 39s
This commit is contained in:
parent
a1ac07bf88
commit
b5a983dc6b
@ -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;
|
||||
@ -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])
|
||||
}
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {}
|
||||
|
||||
26
src/modules/admin-audit-log/audit-log.processor.ts
Normal file
26
src/modules/admin-audit-log/audit-log.processor.ts
Normal file
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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,15 +10,17 @@ 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: {
|
||||
await this.queue.add('audit-logs', {
|
||||
adminUserId: input.adminUserId,
|
||||
action: input.action,
|
||||
resourceType: input.resourceType ?? null,
|
||||
@ -27,7 +29,15 @@ export class AdminAuditService {
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user