feat(center): add audit logs for auth and node operations
This commit is contained in:
@@ -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 {}
|
||||
|
||||
12
Fabric.Backend.Center/src/audit/audit.module.ts
Normal file
12
Fabric.Backend.Center/src/audit/audit.module.ts
Normal file
@@ -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 {}
|
||||
31
Fabric.Backend.Center/src/audit/audit.service.ts
Normal file
31
Fabric.Backend.Center/src/audit/audit.service.ts
Normal file
@@ -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<AuditLog>,
|
||||
) {}
|
||||
|
||||
async write(input: AuditWriteInput): Promise<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<User>,
|
||||
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' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
25
Fabric.Backend.Center/src/entities/audit-log.entity.ts
Normal file
25
Fabric.Backend.Center/src/entities/audit-log.entity.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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<GuildNode>,
|
||||
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,
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
- [x] node 心跳接口(可选)
|
||||
|
||||
### 1.3 Center 运维能力
|
||||
- [ ] 审计日志(auth/node 关键操作)
|
||||
- [x] 审计日志(auth/node 关键操作)
|
||||
- [ ] 健康检查深化(DB ready)
|
||||
- [x] 配置校验(启动时必填项检查)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user