From f0ea49e81b38ba30ec48026d55c720c74d85d53e Mon Sep 17 00:00:00 2001 From: hashbk Date: Sun, 23 Aug 2026 17:11:10 +0800 Subject: [PATCH] feat(settings): add audit retention days to general settings API Move the audit log retention days configuration into the existing GET/PUT /api/settings/general endpoint so administrators can configure it alongside other general settings without a separate API. Changes: - Add auditRetentionDays field to GeneralSettingsDto and UpdateGeneralSettingsDto with @IsInt and @Min(0) validation (0 disables auto-cleanup, which is the default) - Add getAuditRetentionDays() to GeneralSettingsService for AuditCleanupService to consume - Update AuditCleanupService to inject GeneralSettingsService instead of AuditSettingsService - Remove the now-obsolete AuditSettingsService and its registration in SettingsModule - Add tests for persistence, invalid value fallback, and DTO validation --- .../audit/services/audit-cleanup.service.ts | 7 +- .../settings/dto/general-settings.dto.ts | 6 ++ .../settings/general-settings.service.spec.ts | 66 ++++++++++++++++- .../services/audit-settings.service.ts | 72 ------------------- .../services/general-settings.service.ts | 24 +++++++ src/modules/settings/settings.module.ts | 13 ++-- 6 files changed, 103 insertions(+), 85 deletions(-) delete mode 100644 src/modules/settings/services/audit-settings.service.ts diff --git a/src/modules/audit/services/audit-cleanup.service.ts b/src/modules/audit/services/audit-cleanup.service.ts index 31bdd42..3fb1db7 100644 --- a/src/modules/audit/services/audit-cleanup.service.ts +++ b/src/modules/audit/services/audit-cleanup.service.ts @@ -5,7 +5,7 @@ import { Repository, LessThan } from 'typeorm'; import { ConnectionAudit } from '../entities/connection-audit.entity'; import { FileAudit } from '../entities/file-audit.entity'; import { AlarmAudit } from '../entities/alarm-audit.entity'; -import { AuditSettingsService } from '../../settings/services/audit-settings.service'; +import { GeneralSettingsService } from '../../settings/services/general-settings.service'; /** * 审计日志自动清理服务 @@ -23,13 +23,14 @@ export class AuditCleanupService { private fileAuditRepository: Repository, @InjectRepository(AlarmAudit) private alarmAuditRepository: Repository, - private readonly auditSettingsService: AuditSettingsService, + private readonly generalSettingsService: GeneralSettingsService, ) {} @Cron('0 0 * * *') async handleCleanupExpiredAudits() { try { - const retentionDays = await this.auditSettingsService.getRetentionDays(); + const retentionDays = + await this.generalSettingsService.getAuditRetentionDays(); if (retentionDays <= 0) { return; diff --git a/src/modules/settings/dto/general-settings.dto.ts b/src/modules/settings/dto/general-settings.dto.ts index 76f12c6..b4b092f 100644 --- a/src/modules/settings/dto/general-settings.dto.ts +++ b/src/modules/settings/dto/general-settings.dto.ts @@ -34,6 +34,7 @@ export class GeneralSettingsDto { watermarkEnabled: boolean; defaultLanguage: string; jwtExpiryDays: number; + auditRetentionDays: number; site: SiteSettingsDto; webauthn: WebAuthnSettingsDto; } @@ -60,6 +61,11 @@ export class UpdateGeneralSettingsDto { @Min(1) jwtExpiryDays?: number; + @IsOptional() + @IsInt() + @Min(0) + auditRetentionDays?: number; + @IsOptional() @ValidateNested() @Type(() => SiteSettingsDto) diff --git a/src/modules/settings/general-settings.service.spec.ts b/src/modules/settings/general-settings.service.spec.ts index d7fe391..380fce7 100644 --- a/src/modules/settings/general-settings.service.spec.ts +++ b/src/modules/settings/general-settings.service.spec.ts @@ -15,6 +15,7 @@ const DEFAULT_SETTINGS = { watermarkEnabled: true, defaultLanguage: 'en-US', jwtExpiryDays: 30, + auditRetentionDays: 0, site: { frontendUrl: '', backendUrl: '' }, webauthn: { enabled: true, rpName: 'RustDesk Console' }, }; @@ -70,7 +71,7 @@ describe('GeneralSettingsService', () => { ...DEFAULT_SETTINGS, watermarkEnabled: false, }); - await expect(repository.count()).resolves.toBe(7); + await expect(repository.count()).resolves.toBe(8); }); it('persists jwtExpiryDays and reads it back', async () => { @@ -104,6 +105,37 @@ describe('GeneralSettingsService', () => { await expect(service.getJwtExpiryDays()).resolves.toBe(30); }); + it('persists auditRetentionDays and reads it back', async () => { + await service.updateSettings({ + watermarkEnabled: true, + auditRetentionDays: 30, + }); + await expect(service.getSettings()).resolves.toMatchObject({ + auditRetentionDays: 30, + }); + await expect(service.getAuditRetentionDays()).resolves.toBe(30); + }); + + it('falls back to default auditRetentionDays for invalid values', async () => { + await repository.save([ + repository.create({ + key: 'general.auditRetentionDays', + value: 'not-a-number', + category: 'general', + }), + ]); + await expect(service.getAuditRetentionDays()).resolves.toBe(0); + + await repository.save([ + repository.create({ + key: 'general.auditRetentionDays', + value: '-5', + category: 'general', + }), + ]); + await expect(service.getAuditRetentionDays()).resolves.toBe(0); + }); + it('persists defaultLanguage and validates the format', async () => { await service.updateSettings({ watermarkEnabled: true, @@ -227,6 +259,38 @@ describe('general settings HTTP contract', () => { await expect(validate(invalidString)).resolves.not.toHaveLength(0); }); + it('validates auditRetentionDays must be a non-negative integer', async () => { + const valid = plainToInstance(UpdateGeneralSettingsDto, { + watermarkEnabled: true, + auditRetentionDays: 30, + }); + await expect(validate(valid)).resolves.toHaveLength(0); + + const validZero = plainToInstance(UpdateGeneralSettingsDto, { + watermarkEnabled: true, + auditRetentionDays: 0, + }); + await expect(validate(validZero)).resolves.toHaveLength(0); + + const invalidNegative = plainToInstance(UpdateGeneralSettingsDto, { + watermarkEnabled: true, + auditRetentionDays: -1, + }); + await expect(validate(invalidNegative)).resolves.not.toHaveLength(0); + + const invalidFloat = plainToInstance(UpdateGeneralSettingsDto, { + watermarkEnabled: true, + auditRetentionDays: 3.5, + }); + await expect(validate(invalidFloat)).resolves.not.toHaveLength(0); + + const invalidString = plainToInstance(UpdateGeneralSettingsDto, { + watermarkEnabled: true, + auditRetentionDays: '30', + }); + await expect(validate(invalidString)).resolves.not.toHaveLength(0); + }); + it('restricts general reads and updates to administrators', () => { const readHandler = Object.getOwnPropertyDescriptor( GeneralSettingsController.prototype, diff --git a/src/modules/settings/services/audit-settings.service.ts b/src/modules/settings/services/audit-settings.service.ts deleted file mode 100644 index 705a486..0000000 --- a/src/modules/settings/services/audit-settings.service.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { SystemSetting } from '../entities/system-setting.entity'; - -/** - * 审计日志保留配置服务 - * 使用通用 SystemSetting 表管理审计日志保留策略 - * - * 保留天数存储在 system_settings 表中: - * - key: 'audit.retentionDays' - * - value: 数字字符串,0 表示不自动清理 - * - category: 'audit' - */ -@Injectable() -export class AuditSettingsService { - private readonly logger = new Logger(AuditSettingsService.name); - - /** 设置分类 */ - private readonly CATEGORY = 'audit'; - - /** 默认保留天数(0 表示不自动清理) */ - private readonly DEFAULT_RETENTION_DAYS = 0; - - /** 审计设置键名 */ - private readonly AUDIT_KEY = 'audit.retentionDays'; - - constructor( - @InjectRepository(SystemSetting) - private settingRepository: Repository, - ) {} - - /** - * 获取审计日志保留天数 - * @returns 保留天数,0 表示不自动清理 - */ - async getRetentionDays(): Promise { - const setting = await this.settingRepository.findOne({ - where: { key: this.AUDIT_KEY }, - }); - - if (!setting) { - return this.DEFAULT_RETENTION_DAYS; - } - - const days = parseInt(setting.value, 10); - return isNaN(days) ? this.DEFAULT_RETENTION_DAYS : days; - } - - /** - * 设置审计日志保留天数 - * @param days 保留天数,0 表示不自动清理 - */ - async setRetentionDays(days: number): Promise { - let setting = await this.settingRepository.findOne({ - where: { key: this.AUDIT_KEY }, - }); - - if (setting) { - setting.value = String(days); - } else { - setting = this.settingRepository.create({ - key: this.AUDIT_KEY, - value: String(days), - category: this.CATEGORY, - }); - } - - await this.settingRepository.save(setting); - this.logger.log(`Audit retention days set to ${days}`); - } -} diff --git a/src/modules/settings/services/general-settings.service.ts b/src/modules/settings/services/general-settings.service.ts index d67b7cf..36639c1 100644 --- a/src/modules/settings/services/general-settings.service.ts +++ b/src/modules/settings/services/general-settings.service.ts @@ -12,6 +12,7 @@ const CATEGORY = 'general'; const WATERMARK_ENABLED_KEY = 'general.watermarkEnabled'; const DEFAULT_LANGUAGE_KEY = 'general.defaultLanguage'; const JWT_EXPIRY_DAYS_KEY = 'general.jwtExpiryDays'; +const AUDIT_RETENTION_DAYS_KEY = 'general.auditRetentionDays'; const SITE_FRONTEND_URL_KEY = 'general.siteFrontendUrl'; const SITE_BACKEND_URL_KEY = 'general.siteBackendUrl'; const WEBAUTHN_ENABLED_KEY = 'general.webauthnEnabled'; @@ -22,12 +23,14 @@ const DEFAULT_BACKEND_URL = ''; const DEFAULT_RP_NAME = 'RustDesk Console'; const DEFAULT_LANGUAGE = 'en-US'; const DEFAULT_JWT_EXPIRY_DAYS = 30; +const DEFAULT_AUDIT_RETENTION_DAYS = 0; const FALLBACK_URL = 'http://localhost:3000'; const ALL_KEYS = [ WATERMARK_ENABLED_KEY, DEFAULT_LANGUAGE_KEY, JWT_EXPIRY_DAYS_KEY, + AUDIT_RETENTION_DAYS_KEY, SITE_FRONTEND_URL_KEY, SITE_BACKEND_URL_KEY, WEBAUTHN_ENABLED_KEY, @@ -51,6 +54,9 @@ export class GeneralSettingsService { ), defaultLanguage: values.get(DEFAULT_LANGUAGE_KEY) ?? DEFAULT_LANGUAGE, jwtExpiryDays: this.readJwtExpiryDays(values.get(JWT_EXPIRY_DAYS_KEY)), + auditRetentionDays: this.readAuditRetentionDays( + values.get(AUDIT_RETENTION_DAYS_KEY), + ), site: { frontendUrl: values.get(SITE_FRONTEND_URL_KEY) ?? DEFAULT_FRONTEND_URL, backendUrl: values.get(SITE_BACKEND_URL_KEY) ?? DEFAULT_BACKEND_URL, @@ -130,6 +136,15 @@ export class GeneralSettingsService { return this.readJwtExpiryDays(values.get(JWT_EXPIRY_DAYS_KEY)); } + /** + * 获取审计日志保留天数(供 AuditCleanupService 消费) + * 默认 0 表示不自动清理,可通过 settings/general 接口修改 + */ + async getAuditRetentionDays(): Promise { + const values = await this.readValues([AUDIT_RETENTION_DAYS_KEY]); + return this.readAuditRetentionDays(values.get(AUDIT_RETENTION_DAYS_KEY)); + } + async updateSettings( dto: UpdateGeneralSettingsDto, ): Promise { @@ -138,6 +153,8 @@ export class GeneralSettingsService { watermarkEnabled: dto.watermarkEnabled, defaultLanguage: dto.defaultLanguage ?? current.defaultLanguage, jwtExpiryDays: dto.jwtExpiryDays ?? current.jwtExpiryDays, + auditRetentionDays: + dto.auditRetentionDays ?? current.auditRetentionDays, site: { frontendUrl: dto.site?.frontendUrl ?? current.site.frontendUrl, backendUrl: dto.site?.backendUrl ?? current.site.backendUrl, @@ -152,6 +169,7 @@ export class GeneralSettingsService { [WATERMARK_ENABLED_KEY, String(merged.watermarkEnabled)], [DEFAULT_LANGUAGE_KEY, merged.defaultLanguage ?? DEFAULT_LANGUAGE], [JWT_EXPIRY_DAYS_KEY, String(merged.jwtExpiryDays)], + [AUDIT_RETENTION_DAYS_KEY, String(merged.auditRetentionDays)], [SITE_FRONTEND_URL_KEY, merged.site.frontendUrl ?? ''], [SITE_BACKEND_URL_KEY, merged.site.backendUrl ?? ''], [WEBAUTHN_ENABLED_KEY, String(merged.webauthn.enabled)], @@ -201,4 +219,10 @@ export class GeneralSettingsService { if (Number.isNaN(parsed) || parsed < 1) return DEFAULT_JWT_EXPIRY_DAYS; return parsed; } + + private readAuditRetentionDays(value: string | undefined): number { + const parsed = parseInt(value ?? '', 10); + if (Number.isNaN(parsed) || parsed < 0) return DEFAULT_AUDIT_RETENTION_DAYS; + return parsed; + } } diff --git a/src/modules/settings/settings.module.ts b/src/modules/settings/settings.module.ts index 68277ac..b32907c 100644 --- a/src/modules/settings/settings.module.ts +++ b/src/modules/settings/settings.module.ts @@ -3,20 +3,19 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { SystemSetting } from './entities/system-setting.entity'; import { SettingsController } from './settings.controller'; import { SmtpSettingsService } from './services/smtp-settings.service'; -import { AuditSettingsService } from './services/audit-settings.service'; import { GeneralSettingsController } from './general-settings.controller'; import { FrontendSettingsController } from './frontend-settings.controller'; import { GeneralSettingsService } from './services/general-settings.service'; /** * 系统设置模块 - * 管理系统配置,包括 SMTP 配置、审计日志保留策略等 + * 管理系统配置,包括 SMTP 配置、通用设置等 * * 使用通用 SystemSetting 表存储各类设置项 * * 导出服务: * - SmtpSettingsService(供 EmailModule 等其他模块使用) - * - AuditSettingsService(供 AuditModule 等其他模块使用) + * - GeneralSettingsService(供 AuditModule / AuthModule 等其他模块使用) */ @Module({ imports: [TypeOrmModule.forFeature([SystemSetting])], @@ -25,11 +24,7 @@ import { GeneralSettingsService } from './services/general-settings.service'; GeneralSettingsController, FrontendSettingsController, ], - providers: [ - SmtpSettingsService, - AuditSettingsService, - GeneralSettingsService, - ], - exports: [SmtpSettingsService, AuditSettingsService, GeneralSettingsService], + providers: [SmtpSettingsService, GeneralSettingsService], + exports: [SmtpSettingsService, GeneralSettingsService], }) export class SettingsModule {}