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?
|
afterJson Json?
|
||||||
ip String? @db.VarChar(45)
|
ip String? @db.VarChar(45)
|
||||||
userAgent String? @db.VarChar(500)
|
userAgent String? @db.VarChar(500)
|
||||||
|
riskLevel String? @db.VarChar(16)
|
||||||
|
reason String? @db.VarChar(500)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
adminUser AdminUser @relation(fields: [adminUserId], references: [id])
|
adminUser AdminUser @relation(fields: [adminUserId], references: [id])
|
||||||
@ -881,3 +883,21 @@ model ConfigChangeLog {
|
|||||||
@@index([entityType, entityId])
|
@@index([entityType, entityId])
|
||||||
@@index([createdAt])
|
@@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 { Global, Module } from '@nestjs/common';
|
||||||
import { BullModule } from '@nestjs/bullmq';
|
import { BullModule } from '@nestjs/bullmq';
|
||||||
import { ConfigService } from '@nestjs/config';
|
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()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
@ -27,6 +27,7 @@ import { QueueService, QUEUE_AI_ANALYSIS, QUEUE_DOCUMENT_IMPORT, QUEUE_NOTIFICAT
|
|||||||
{ name: QUEUE_AI_ANALYSIS },
|
{ name: QUEUE_AI_ANALYSIS },
|
||||||
{ name: QUEUE_DOCUMENT_IMPORT },
|
{ name: QUEUE_DOCUMENT_IMPORT },
|
||||||
{ name: QUEUE_NOTIFICATION },
|
{ name: QUEUE_NOTIFICATION },
|
||||||
|
{ name: QUEUE_AUDIT_LOG },
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
providers: [QueueService],
|
providers: [QueueService],
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { Queue } from 'bullmq';
|
|||||||
export const QUEUE_AI_ANALYSIS = 'ai-analysis';
|
export const QUEUE_AI_ANALYSIS = 'ai-analysis';
|
||||||
export const QUEUE_DOCUMENT_IMPORT = 'document-import';
|
export const QUEUE_DOCUMENT_IMPORT = 'document-import';
|
||||||
export const QUEUE_NOTIFICATION = 'notification';
|
export const QUEUE_NOTIFICATION = 'notification';
|
||||||
|
export const QUEUE_AUDIT_LOG = 'audit-logs';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QueueService {
|
export class QueueService {
|
||||||
|
|||||||
@ -1,11 +1,15 @@
|
|||||||
import { Module } from '@nestjs/common';
|
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 { AdminAuditLogController } from './admin-audit-log.controller';
|
||||||
import { AdminAuditLogService } from './admin-audit-log.service';
|
import { AdminAuditLogService } from './admin-audit-log.service';
|
||||||
import { AdminAuthModule } from '../admin-auth/admin-auth.module';
|
import { AdminAuthModule } from '../admin-auth/admin-auth.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [AdminAuthModule],
|
imports: [AdminAuthModule, BullModule.registerQueue({ name: QUEUE_AUDIT_LOG })],
|
||||||
controllers: [AdminAuditLogController],
|
controllers: [AdminAuditLogController],
|
||||||
providers: [AdminAuditLogService],
|
providers: [AuditLogProcessor, PrismaService,AdminAuditLogService],
|
||||||
})
|
})
|
||||||
export class AdminAuditLogModule {}
|
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 { Injectable } from '@nestjs/common';
|
||||||
import { PrismaService } from '../../infrastructure/database/prisma.service';
|
import { QueueService } from '../../infrastructure/queue/queue.service';
|
||||||
|
|
||||||
export interface AuditLogInput {
|
export interface AuditLogInput {
|
||||||
adminUserId: string;
|
adminUserId: string;
|
||||||
@ -10,15 +10,17 @@ export interface AuditLogInput {
|
|||||||
afterJson?: any;
|
afterJson?: any;
|
||||||
ip?: string;
|
ip?: string;
|
||||||
userAgent?: string;
|
userAgent?: string;
|
||||||
|
riskLevel?: 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
reason?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminAuditService {
|
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) {
|
async log(input: AuditLogInput) {
|
||||||
return this.prisma.adminAuditLog.create({
|
await this.queue.add('audit-logs', {
|
||||||
data: {
|
|
||||||
adminUserId: input.adminUserId,
|
adminUserId: input.adminUserId,
|
||||||
action: input.action,
|
action: input.action,
|
||||||
resourceType: input.resourceType ?? null,
|
resourceType: input.resourceType ?? null,
|
||||||
@ -27,7 +29,15 @@ export class AdminAuditService {
|
|||||||
afterJson: input.afterJson ?? null,
|
afterJson: input.afterJson ?? null,
|
||||||
ip: input.ip ?? null,
|
ip: input.ip ?? null,
|
||||||
userAgent: input.userAgent ?? 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