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 { HealthController } from './common/health.controller';
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
import { NodesModule } from './nodes/nodes.module';
|
import { NodesModule } from './nodes/nodes.module';
|
||||||
|
import { AuditModule } from './audit/audit.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forRoot(buildTypeOrmConfig()), AuthModule, NodesModule],
|
imports: [
|
||||||
|
TypeOrmModule.forRoot(buildTypeOrmConfig()),
|
||||||
|
AuditModule,
|
||||||
|
AuthModule,
|
||||||
|
NodesModule,
|
||||||
|
],
|
||||||
controllers: [HealthController],
|
controllers: [HealthController],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
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 { User } from '../entities/user.entity';
|
||||||
import { RegisterDto } from './dto.register.dto';
|
import { RegisterDto } from './dto.register.dto';
|
||||||
import { LoginDto } from './dto.login.dto';
|
import { LoginDto } from './dto.login.dto';
|
||||||
|
import { AuditService } from '../audit/audit.service';
|
||||||
|
|
||||||
function parseDurationToSeconds(input: string, fallbackSeconds: number): number {
|
function parseDurationToSeconds(input: string, fallbackSeconds: number): number {
|
||||||
const raw = input.trim();
|
const raw = input.trim();
|
||||||
@@ -44,6 +45,7 @@ export class AuthService {
|
|||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(User)
|
@InjectRepository(User)
|
||||||
private readonly userRepo: Repository<User>,
|
private readonly userRepo: Repository<User>,
|
||||||
|
private readonly audit: AuditService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async register(input: RegisterDto) {
|
async register(input: RegisterDto) {
|
||||||
@@ -60,6 +62,14 @@ export class AuthService {
|
|||||||
});
|
});
|
||||||
const saved = await this.userRepo.save(user);
|
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 {
|
return {
|
||||||
id: saved.id,
|
id: saved.id,
|
||||||
email: saved.email,
|
email: saved.email,
|
||||||
@@ -79,6 +89,14 @@ export class AuthService {
|
|||||||
user.refreshTokenHash = await bcrypt.hash(refreshToken, 10);
|
user.refreshTokenHash = await bcrypt.hash(refreshToken, 10);
|
||||||
await this.userRepo.save(user);
|
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 {
|
return {
|
||||||
accessToken,
|
accessToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
@@ -112,6 +130,14 @@ export class AuthService {
|
|||||||
user.refreshTokenHash = await bcrypt.hash(newRefreshToken, 10);
|
user.refreshTokenHash = await bcrypt.hash(newRefreshToken, 10);
|
||||||
await this.userRepo.save(user);
|
await this.userRepo.save(user);
|
||||||
|
|
||||||
|
await this.audit.write({
|
||||||
|
action: 'auth.refresh',
|
||||||
|
actorId: user.id,
|
||||||
|
targetType: 'user',
|
||||||
|
targetId: user.id,
|
||||||
|
detail: null,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessToken: newAccessToken,
|
accessToken: newAccessToken,
|
||||||
refreshToken: newRefreshToken,
|
refreshToken: newRefreshToken,
|
||||||
@@ -135,6 +161,13 @@ export class AuthService {
|
|||||||
|
|
||||||
user.refreshTokenHash = null;
|
user.refreshTokenHash = null;
|
||||||
await this.userRepo.save(user);
|
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' };
|
return { status: 'ok' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
||||||
import { User } from './entities/user.entity';
|
import { User } from './entities/user.entity';
|
||||||
import { GuildNode } from './entities/guild-node.entity';
|
import { GuildNode } from './entities/guild-node.entity';
|
||||||
|
import { AuditLog } from './entities/audit-log.entity';
|
||||||
|
|
||||||
export const buildTypeOrmConfig = (): TypeOrmModuleOptions => ({
|
export const buildTypeOrmConfig = (): TypeOrmModuleOptions => ({
|
||||||
type: 'mysql',
|
type: 'mysql',
|
||||||
@@ -9,7 +10,7 @@ export const buildTypeOrmConfig = (): TypeOrmModuleOptions => ({
|
|||||||
username: process.env.DB_USER ?? 'fabric',
|
username: process.env.DB_USER ?? 'fabric',
|
||||||
password: process.env.DB_PASSWORD ?? 'fabric',
|
password: process.env.DB_PASSWORD ?? 'fabric',
|
||||||
database: process.env.DB_NAME ?? 'fabric_center',
|
database: process.env.DB_NAME ?? 'fabric_center',
|
||||||
entities: [User, GuildNode],
|
entities: [User, GuildNode, AuditLog],
|
||||||
synchronize: (process.env.DB_SYNC ?? 'true') === 'true',
|
synchronize: (process.env.DB_SYNC ?? 'true') === 'true',
|
||||||
logging: (process.env.DB_LOGGING ?? 'false') === '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 { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { GuildNode } from '../entities/guild-node.entity';
|
import { GuildNode } from '../entities/guild-node.entity';
|
||||||
|
import { AuditService } from '../audit/audit.service';
|
||||||
import { RegisterNodeDto } from './dto.register-node.dto';
|
import { RegisterNodeDto } from './dto.register-node.dto';
|
||||||
import { UpdateNodeStatusDto } from './dto.update-node-status.dto';
|
import { UpdateNodeStatusDto } from './dto.update-node-status.dto';
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ export class NodesController {
|
|||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(GuildNode)
|
@InjectRepository(GuildNode)
|
||||||
private readonly nodeRepo: Repository<GuildNode>,
|
private readonly nodeRepo: Repository<GuildNode>,
|
||||||
|
private readonly audit: AuditService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post('register')
|
@Post('register')
|
||||||
@@ -52,6 +54,12 @@ export class NodesController {
|
|||||||
status: 'active',
|
status: 'active',
|
||||||
});
|
});
|
||||||
const saved = await this.nodeRepo.save(node);
|
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 {
|
return {
|
||||||
status: 'accepted',
|
status: 'accepted',
|
||||||
@@ -78,6 +86,12 @@ export class NodesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const saved = await this.nodeRepo.save(node);
|
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 {
|
return {
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
nodeId: saved.nodeId,
|
nodeId: saved.nodeId,
|
||||||
@@ -98,6 +112,12 @@ export class NodesController {
|
|||||||
|
|
||||||
node.status = body.status;
|
node.status = body.status;
|
||||||
const saved = await this.nodeRepo.save(node);
|
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 {
|
return {
|
||||||
id: saved.id,
|
id: saved.id,
|
||||||
nodeId: saved.nodeId,
|
nodeId: saved.nodeId,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
- [x] node 心跳接口(可选)
|
- [x] node 心跳接口(可选)
|
||||||
|
|
||||||
### 1.3 Center 运维能力
|
### 1.3 Center 运维能力
|
||||||
- [ ] 审计日志(auth/node 关键操作)
|
- [x] 审计日志(auth/node 关键操作)
|
||||||
- [ ] 健康检查深化(DB ready)
|
- [ ] 健康检查深化(DB ready)
|
||||||
- [x] 配置校验(启动时必填项检查)
|
- [x] 配置校验(启动时必填项检查)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user