feat(center): add audit logs for auth and node operations

This commit is contained in:
nav
2026-05-12 08:57:34 +00:00
parent 7f68a09486
commit 7270256587
8 changed files with 131 additions and 3 deletions

View File

@@ -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 {}

View 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 {}

View 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);
}
}

View File

@@ -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' };
}
}

View File

@@ -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',
});

View 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;
}

View File

@@ -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,