diff --git a/Fabric.Backend.Center/src/app.module.ts b/Fabric.Backend.Center/src/app.module.ts index b512491..4a34b35 100644 --- a/Fabric.Backend.Center/src/app.module.ts +++ b/Fabric.Backend.Center/src/app.module.ts @@ -4,9 +4,15 @@ import { buildTypeOrmConfig } from './database.config'; import { HealthController } from './common/health.controller'; import { AuthModule } from './auth/auth.module'; import { NodesModule } from './nodes/nodes.module'; +import { AuditModule } from './audit/audit.module'; @Module({ - imports: [TypeOrmModule.forRoot(buildTypeOrmConfig()), AuthModule, NodesModule], + imports: [ + TypeOrmModule.forRoot(buildTypeOrmConfig()), + AuditModule, + AuthModule, + NodesModule, + ], controllers: [HealthController], }) export class AppModule {} diff --git a/Fabric.Backend.Center/src/audit/audit.module.ts b/Fabric.Backend.Center/src/audit/audit.module.ts new file mode 100644 index 0000000..d87290a --- /dev/null +++ b/Fabric.Backend.Center/src/audit/audit.module.ts @@ -0,0 +1,12 @@ +import { Global, Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AuditLog } from '../entities/audit-log.entity'; +import { AuditService } from './audit.service'; + +@Global() +@Module({ + imports: [TypeOrmModule.forFeature([AuditLog])], + providers: [AuditService], + exports: [AuditService], +}) +export class AuditModule {} diff --git a/Fabric.Backend.Center/src/audit/audit.service.ts b/Fabric.Backend.Center/src/audit/audit.service.ts new file mode 100644 index 0000000..c90d0e9 --- /dev/null +++ b/Fabric.Backend.Center/src/audit/audit.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { AuditLog } from '../entities/audit-log.entity'; + +export type AuditWriteInput = { + action: string; + actorId?: string | null; + targetType?: string | null; + targetId?: string | null; + detail?: string | null; +}; + +@Injectable() +export class AuditService { + constructor( + @InjectRepository(AuditLog) + private readonly auditRepo: Repository, + ) {} + + async write(input: AuditWriteInput): Promise { + const row = this.auditRepo.create({ + action: input.action, + actorId: input.actorId ?? null, + targetType: input.targetType ?? null, + targetId: input.targetId ?? null, + detail: input.detail ?? null, + }); + await this.auditRepo.save(row); + } +} diff --git a/Fabric.Backend.Center/src/auth/auth.service.ts b/Fabric.Backend.Center/src/auth/auth.service.ts index e228609..1923a67 100644 --- a/Fabric.Backend.Center/src/auth/auth.service.ts +++ b/Fabric.Backend.Center/src/auth/auth.service.ts @@ -10,6 +10,7 @@ import * as jwt from 'jsonwebtoken'; import { User } from '../entities/user.entity'; import { RegisterDto } from './dto.register.dto'; import { LoginDto } from './dto.login.dto'; +import { AuditService } from '../audit/audit.service'; function parseDurationToSeconds(input: string, fallbackSeconds: number): number { const raw = input.trim(); @@ -44,6 +45,7 @@ export class AuthService { constructor( @InjectRepository(User) private readonly userRepo: Repository, + private readonly audit: AuditService, ) {} async register(input: RegisterDto) { @@ -60,6 +62,14 @@ export class AuthService { }); const saved = await this.userRepo.save(user); + await this.audit.write({ + action: 'auth.register', + actorId: saved.id, + targetType: 'user', + targetId: saved.id, + detail: JSON.stringify({ email: saved.email }), + }); + return { id: saved.id, email: saved.email, @@ -79,6 +89,14 @@ export class AuthService { user.refreshTokenHash = await bcrypt.hash(refreshToken, 10); await this.userRepo.save(user); + await this.audit.write({ + action: 'auth.login', + actorId: user.id, + targetType: 'user', + targetId: user.id, + detail: JSON.stringify({ email: user.email }), + }); + return { accessToken, refreshToken, @@ -112,6 +130,14 @@ export class AuthService { user.refreshTokenHash = await bcrypt.hash(newRefreshToken, 10); await this.userRepo.save(user); + await this.audit.write({ + action: 'auth.refresh', + actorId: user.id, + targetType: 'user', + targetId: user.id, + detail: null, + }); + return { accessToken: newAccessToken, refreshToken: newRefreshToken, @@ -135,6 +161,13 @@ export class AuthService { user.refreshTokenHash = null; await this.userRepo.save(user); + await this.audit.write({ + action: 'auth.logout', + actorId: user.id, + targetType: 'user', + targetId: user.id, + detail: null, + }); return { status: 'ok' }; } } diff --git a/Fabric.Backend.Center/src/database.config.ts b/Fabric.Backend.Center/src/database.config.ts index 52699ba..7e285df 100644 --- a/Fabric.Backend.Center/src/database.config.ts +++ b/Fabric.Backend.Center/src/database.config.ts @@ -1,6 +1,7 @@ import { TypeOrmModuleOptions } from '@nestjs/typeorm'; import { User } from './entities/user.entity'; import { GuildNode } from './entities/guild-node.entity'; +import { AuditLog } from './entities/audit-log.entity'; export const buildTypeOrmConfig = (): TypeOrmModuleOptions => ({ type: 'mysql', @@ -9,7 +10,7 @@ export const buildTypeOrmConfig = (): TypeOrmModuleOptions => ({ username: process.env.DB_USER ?? 'fabric', password: process.env.DB_PASSWORD ?? 'fabric', database: process.env.DB_NAME ?? 'fabric_center', - entities: [User, GuildNode], + entities: [User, GuildNode, AuditLog], synchronize: (process.env.DB_SYNC ?? 'true') === 'true', logging: (process.env.DB_LOGGING ?? 'false') === 'true', }); diff --git a/Fabric.Backend.Center/src/entities/audit-log.entity.ts b/Fabric.Backend.Center/src/entities/audit-log.entity.ts new file mode 100644 index 0000000..0c273d0 --- /dev/null +++ b/Fabric.Backend.Center/src/entities/audit-log.entity.ts @@ -0,0 +1,25 @@ +import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity('audit_logs') +export class AuditLog { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column() + action!: string; + + @Column({ nullable: true }) + actorId!: string | null; + + @Column({ nullable: true }) + targetType!: string | null; + + @Column({ nullable: true }) + targetId!: string | null; + + @Column({ type: 'text', nullable: true }) + detail!: string | null; + + @CreateDateColumn() + createdAt!: Date; +} diff --git a/Fabric.Backend.Center/src/nodes/nodes.controller.ts b/Fabric.Backend.Center/src/nodes/nodes.controller.ts index 9bd9487..c126d92 100644 --- a/Fabric.Backend.Center/src/nodes/nodes.controller.ts +++ b/Fabric.Backend.Center/src/nodes/nodes.controller.ts @@ -15,6 +15,7 @@ import { import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { GuildNode } from '../entities/guild-node.entity'; +import { AuditService } from '../audit/audit.service'; import { RegisterNodeDto } from './dto.register-node.dto'; import { UpdateNodeStatusDto } from './dto.update-node-status.dto'; @@ -23,6 +24,7 @@ export class NodesController { constructor( @InjectRepository(GuildNode) private readonly nodeRepo: Repository, + private readonly audit: AuditService, ) {} @Post('register') @@ -52,6 +54,12 @@ export class NodesController { status: 'active', }); const saved = await this.nodeRepo.save(node); + await this.audit.write({ + action: 'node.register', + targetType: 'node', + targetId: saved.nodeId, + detail: JSON.stringify({ endpoint: saved.endpoint }), + }); return { status: 'accepted', @@ -78,6 +86,12 @@ export class NodesController { } const saved = await this.nodeRepo.save(node); + await this.audit.write({ + action: 'node.heartbeat', + targetType: 'node', + targetId: saved.nodeId, + detail: JSON.stringify({ status: saved.status }), + }); return { status: 'ok', nodeId: saved.nodeId, @@ -98,6 +112,12 @@ export class NodesController { node.status = body.status; const saved = await this.nodeRepo.save(node); + await this.audit.write({ + action: 'node.status.update', + targetType: 'node', + targetId: saved.nodeId, + detail: JSON.stringify({ status: saved.status }), + }); return { id: saved.id, nodeId: saved.nodeId, diff --git a/docs/TODO-backend-center-guild.md b/docs/TODO-backend-center-guild.md index 8b48e70..8975e91 100644 --- a/docs/TODO-backend-center-guild.md +++ b/docs/TODO-backend-center-guild.md @@ -30,7 +30,7 @@ - [x] node 心跳接口(可选) ### 1.3 Center 运维能力 -- [ ] 审计日志(auth/node 关键操作) +- [x] 审计日志(auth/node 关键操作) - [ ] 健康检查深化(DB ready) - [x] 配置校验(启动时必填项检查)