feat: Phase 9 – Administration (Audit-UI, Systemeinstellungen, erweiterter Systemstatus)
This commit is contained in:
@@ -8,6 +8,7 @@ import { AuthModule } from './auth/auth.module';
|
||||
import { UsersModule } from './users/users.module';
|
||||
import { HealthModule } from './health/health.module';
|
||||
import { ModulesModule } from './modules/modules.module';
|
||||
import { SettingsModule } from './settings/settings.module';
|
||||
import { SessionGuard } from './auth/guards/session.guard';
|
||||
import { CsrfGuard } from './auth/guards/csrf.guard';
|
||||
import { RolesGuard } from './common/guards/roles.guard';
|
||||
@@ -25,6 +26,7 @@ import { RolesGuard } from './common/guards/roles.guard';
|
||||
UsersModule,
|
||||
HealthModule,
|
||||
ModulesModule,
|
||||
SettingsModule,
|
||||
],
|
||||
providers: [
|
||||
MigrationRunner,
|
||||
|
||||
99
apps/platform-backend/src/audit/audit-query.repository.ts
Normal file
99
apps/platform-backend/src/audit/audit-query.repository.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DatabaseService } from '../database/database.service';
|
||||
|
||||
/** Audit-Log-Eintrag für API-Antworten. */
|
||||
export interface AuditLogEntry {
|
||||
readonly id: number;
|
||||
readonly username: string;
|
||||
readonly action: string;
|
||||
readonly details: Record<string, unknown>;
|
||||
readonly ipAddress: string | null;
|
||||
readonly createdAt: string;
|
||||
}
|
||||
|
||||
interface AuditRow {
|
||||
id: number;
|
||||
username: string;
|
||||
action: string;
|
||||
details: Record<string, unknown>;
|
||||
ip_address: string | null;
|
||||
created_at: Date;
|
||||
}
|
||||
|
||||
/** Filter-Parameter für die Audit-Liste. */
|
||||
export interface AuditListFilter {
|
||||
readonly action?: string;
|
||||
readonly username?: string;
|
||||
readonly limit: number;
|
||||
readonly offset: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Audit-Log-Abfragen (Infrastructure): Durchsuchen und Filtern des
|
||||
* zentralen Audit-Logs (nur für Administratoren).
|
||||
*/
|
||||
@Injectable()
|
||||
export class AuditQueryRepository {
|
||||
constructor(private readonly database: DatabaseService) {}
|
||||
|
||||
/** Listet Audit-Einträge mit optionalen Filtern (neueste zuerst). */
|
||||
async list(filter: AuditListFilter): Promise<AuditLogEntry[]> {
|
||||
const conditions: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
|
||||
if (filter.action) {
|
||||
params.push(filter.action);
|
||||
conditions.push(`action = $${params.length}`);
|
||||
}
|
||||
if (filter.username) {
|
||||
params.push(`%${filter.username}%`);
|
||||
conditions.push(`username ILIKE $${params.length}`);
|
||||
}
|
||||
|
||||
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
params.push(filter.limit);
|
||||
const limitClause = `LIMIT $${params.length}`;
|
||||
params.push(filter.offset);
|
||||
const offsetClause = `OFFSET $${params.length}`;
|
||||
|
||||
const result = await this.database.query<AuditRow>(
|
||||
`SELECT id, username, action, details, ip_address, created_at
|
||||
FROM audit_logs
|
||||
${whereClause}
|
||||
ORDER BY created_at DESC
|
||||
${limitClause} ${offsetClause}`,
|
||||
params,
|
||||
);
|
||||
|
||||
return result.rows.map((row) => ({
|
||||
id: row.id,
|
||||
username: row.username,
|
||||
action: row.action,
|
||||
details: row.details ?? {},
|
||||
ipAddress: row.ip_address,
|
||||
createdAt: row.created_at.toISOString(),
|
||||
}));
|
||||
}
|
||||
|
||||
/** Gesamtzahl der Einträge (für Paginierung). */
|
||||
async count(filter: Pick<AuditListFilter, 'action' | 'username'>): Promise<number> {
|
||||
const conditions: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
|
||||
if (filter.action) {
|
||||
params.push(filter.action);
|
||||
conditions.push(`action = $${params.length}`);
|
||||
}
|
||||
if (filter.username) {
|
||||
params.push(`%${filter.username}%`);
|
||||
conditions.push(`username ILIKE $${params.length}`);
|
||||
}
|
||||
|
||||
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
const result = await this.database.query<{ count: number }>(
|
||||
`SELECT count(*)::int AS count FROM audit_logs ${whereClause}`,
|
||||
params,
|
||||
);
|
||||
return result.rows[0]?.count ?? 0;
|
||||
}
|
||||
}
|
||||
38
apps/platform-backend/src/audit/audit.controller.ts
Normal file
38
apps/platform-backend/src/audit/audit.controller.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Controller, Get, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Roles } from '../common/decorators/roles.decorator';
|
||||
import { ZodValidationPipe } from '../common/zod-validation.pipe';
|
||||
import { z } from 'zod';
|
||||
import type { AuditLogEntry } from './audit-query.repository';
|
||||
import { AuditQueryRepository } from './audit-query.repository';
|
||||
|
||||
/** Abfrage-Parameter für die Audit-Liste (Zod-Validierung). */
|
||||
export const auditListQuerySchema = z.object({
|
||||
action: z.string().trim().max(100).optional(),
|
||||
username: z.string().trim().max(100).optional(),
|
||||
limit: z.coerce.number().int().min(1).max(100).default(50),
|
||||
offset: z.coerce.number().int().min(0).default(0),
|
||||
});
|
||||
type AuditListQuery = z.infer<typeof auditListQuerySchema>;
|
||||
|
||||
/**
|
||||
* Audit-Log (nur Administratoren): Durchsuchen und Filtern der
|
||||
* sicherheitsrelevanten Ereignisse der Plattform.
|
||||
*/
|
||||
@ApiTags('Audit')
|
||||
@Roles('ADMIN')
|
||||
@Controller({ path: 'api/v1/audit' })
|
||||
export class AuditController {
|
||||
constructor(private readonly auditQueryRepository: AuditQueryRepository) {}
|
||||
|
||||
@Get()
|
||||
async list(
|
||||
@Query(new ZodValidationPipe(auditListQuerySchema)) query: AuditListQuery,
|
||||
): Promise<{ entries: AuditLogEntry[]; total: number }> {
|
||||
const [entries, total] = await Promise.all([
|
||||
this.auditQueryRepository.list(query),
|
||||
this.auditQueryRepository.count(query),
|
||||
]);
|
||||
return { entries, total };
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { DatabaseModule } from '../database/database.module';
|
||||
import { AuditController } from './audit.controller';
|
||||
import { AuditQueryRepository } from './audit-query.repository';
|
||||
import { AuditService } from './audit.service';
|
||||
|
||||
/** Global verfügbares Audit-Logging. */
|
||||
/** Global verfügbares Audit-Logging mit Admin-Abfragen. */
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [DatabaseModule],
|
||||
providers: [AuditService],
|
||||
exports: [AuditService],
|
||||
controllers: [AuditController],
|
||||
providers: [AuditService, AuditQueryRepository],
|
||||
exports: [AuditService, AuditQueryRepository],
|
||||
})
|
||||
export class AuditModule {}
|
||||
@@ -23,6 +23,7 @@ export const AUDIT_ACTIONS = {
|
||||
MODULE_DISABLED: 'MODULE_DISABLED',
|
||||
PERMISSION_GRANTED: 'PERMISSION_GRANTED',
|
||||
PERMISSION_REVOKED: 'PERMISSION_REVOKED',
|
||||
SETTINGS_CHANGED: 'SETTINGS_CHANGED',
|
||||
} as const;
|
||||
|
||||
export type AuditAction = (typeof AUDIT_ACTIONS)[keyof typeof AUDIT_ACTIONS];
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { migration001CoreSchema } from './001-core-schema';
|
||||
import { migration002Modules } from '../../modules/migrations/002-modules';
|
||||
import { migration003ModulePermissions } from '../../modules/migrations/003-module-permissions';
|
||||
import { migration004SystemSettings } from '../../settings/migrations/004-system-settings';
|
||||
|
||||
/** Registrierte Migrationen in aufsteigender Reihenfolge. */
|
||||
export const MIGRATIONS = [
|
||||
migration001CoreSchema,
|
||||
migration002Modules,
|
||||
migration003ModulePermissions,
|
||||
migration004SystemSettings,
|
||||
];
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DatabaseModule } from '../database/database.module';
|
||||
import { AuditModule } from '../audit/audit.module';
|
||||
import { ModulesModule } from '../modules/modules.module';
|
||||
import { HealthController } from './health.controller';
|
||||
import { SystemStatusController } from './system-status.controller';
|
||||
|
||||
/** Health-Endpoints (Monitoring). */
|
||||
@Module({
|
||||
imports: [DatabaseModule],
|
||||
controllers: [HealthController],
|
||||
imports: [DatabaseModule, AuditModule, ModulesModule],
|
||||
controllers: [HealthController, SystemStatusController],
|
||||
})
|
||||
export class HealthModule {}
|
||||
96
apps/platform-backend/src/health/system-status.controller.ts
Normal file
96
apps/platform-backend/src/health/system-status.controller.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Roles } from '../common/decorators/roles.decorator';
|
||||
import { DatabaseService } from '../database/database.service';
|
||||
import { ModuleRepository } from '../modules/module.repository';
|
||||
import { ModuleHealthChecker } from '../modules/module-health-checker';
|
||||
import type { ModuleRecord } from '../modules/manifest.types';
|
||||
|
||||
/** Komponenten-Status für das Admin-Dashboard. */
|
||||
export interface ComponentStatus {
|
||||
readonly name: string;
|
||||
readonly status: 'HEALTHY' | 'DEGRADED' | 'UNHEALTHY' | 'STOPPED';
|
||||
readonly detail: string;
|
||||
}
|
||||
|
||||
/** Erweiterter Systemstatus (Phase 9). */
|
||||
export interface SystemStatusResponse {
|
||||
readonly overall: 'HEALTHY' | 'DEGRADED' | 'UNHEALTHY';
|
||||
readonly components: readonly ComponentStatus[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Systemstatus (nur Administratoren): Zustand aller Plattform-Komponenten
|
||||
* inklusive aller installierten Module.
|
||||
*/
|
||||
@ApiTags('System')
|
||||
@Roles('ADMIN')
|
||||
@Controller({ path: 'api/v1/system/status' })
|
||||
export class SystemStatusController {
|
||||
constructor(
|
||||
private readonly database: DatabaseService,
|
||||
private readonly moduleRepository: ModuleRepository,
|
||||
private readonly healthChecker: ModuleHealthChecker,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
async getStatus(): Promise<SystemStatusResponse> {
|
||||
const components: ComponentStatus[] = [];
|
||||
|
||||
// Management-Backend (dieser Prozess läuft, wenn die Antwort entsteht)
|
||||
components.push({
|
||||
name: 'Management',
|
||||
status: 'HEALTHY',
|
||||
detail: 'Backend erreichbar',
|
||||
});
|
||||
|
||||
// PostgreSQL
|
||||
try {
|
||||
const latencyMs = Math.round(await this.database.ping());
|
||||
components.push({
|
||||
name: 'Datenbank',
|
||||
status: latencyMs < 500 ? 'HEALTHY' : 'DEGRADED',
|
||||
detail: `${latencyMs} ms`,
|
||||
});
|
||||
} catch {
|
||||
components.push({
|
||||
name: 'Datenbank',
|
||||
status: 'UNHEALTHY',
|
||||
detail: 'nicht erreichbar',
|
||||
});
|
||||
}
|
||||
|
||||
// Module: Healthchecks parallel, Fehler isoliert pro Modul
|
||||
const modules = await this.moduleRepository.list();
|
||||
const moduleHealths = await Promise.all(
|
||||
modules.map(async (module): Promise<{ module: ModuleRecord; healthy: boolean; detail: string }> => {
|
||||
if (module.status !== 'RUNNING') {
|
||||
return { module, healthy: false, detail: module.status };
|
||||
}
|
||||
const health = await this.healthChecker.check(module);
|
||||
return { module, healthy: health.healthy, detail: health.detail };
|
||||
}),
|
||||
);
|
||||
|
||||
for (const { module, healthy, detail } of moduleHealths) {
|
||||
components.push({
|
||||
name: module.name,
|
||||
status: !module.enabled
|
||||
? 'STOPPED'
|
||||
: healthy
|
||||
? 'HEALTHY'
|
||||
: module.status === 'RUNNING'
|
||||
? 'UNHEALTHY'
|
||||
: 'STOPPED',
|
||||
detail: `${module.status} · ${detail}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Gesamtstatus: schlechtester Komponenten-Status
|
||||
const hasUnhealthy = components.some((component) => component.status === 'UNHEALTHY');
|
||||
const hasDegraded = components.some((component) => component.status === 'DEGRADED');
|
||||
const overall = hasUnhealthy ? 'UNHEALTHY' : hasDegraded ? 'DEGRADED' : 'HEALTHY';
|
||||
|
||||
return { overall, components };
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ import { ModulesService } from './modules.service';
|
||||
ModulePermissionRepository,
|
||||
ModulePermissionsService,
|
||||
],
|
||||
exports: [ModuleRepository, ModulesService, ModulePermissionsService],
|
||||
exports: [ModuleRepository, ModulesService, ModulePermissionsService, ModuleHealthChecker],
|
||||
})
|
||||
export class ModulesModule implements NestModule {
|
||||
/** Gateway-Middleware für alle /api/v1/gateway/* Pfade. */
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { Migration } from '../../database/migration.types';
|
||||
|
||||
/** Phase 9: Systemeinstellungen der Plattform. */
|
||||
export const migration004SystemSettings: Migration = {
|
||||
id: '004-system-settings',
|
||||
description: 'Systemeinstellungen (system_settings) anlegen',
|
||||
up: async (client) => {
|
||||
await client.query(`
|
||||
CREATE TABLE system_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_by TEXT
|
||||
)
|
||||
`);
|
||||
},
|
||||
};
|
||||
44
apps/platform-backend/src/settings/settings.controller.ts
Normal file
44
apps/platform-backend/src/settings/settings.controller.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Body, Controller, Get, Param, Patch, Req } from '@nestjs/common';
|
||||
import type { Request } from 'express';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import type { AuthenticatedRequest } from '../auth/authenticated-request';
|
||||
import { CurrentUser } from '../common/decorators/current-user.decorator';
|
||||
import { Roles } from '../common/decorators/roles.decorator';
|
||||
import { ZodValidationPipe } from '../common/zod-validation.pipe';
|
||||
import { z } from 'zod';
|
||||
import type { AuthUser } from '../users/user.types';
|
||||
import type { SettingResponse } from './settings.service';
|
||||
import { SettingsService } from './settings.service';
|
||||
|
||||
/** Einstellung aktualisieren (Zod-Validierung). */
|
||||
export const updateSettingSchema = z.object({
|
||||
value: z.string().min(1, 'Wert ist erforderlich').max(500),
|
||||
});
|
||||
type UpdateSettingDto = z.infer<typeof updateSettingSchema>;
|
||||
|
||||
/**
|
||||
* Systemeinstellungen (nur Administratoren).
|
||||
*/
|
||||
@ApiTags('Settings')
|
||||
@Roles('ADMIN')
|
||||
@Controller({ path: 'api/v1/settings' })
|
||||
export class SettingsController {
|
||||
constructor(private readonly settingsService: SettingsService) {}
|
||||
|
||||
@Get()
|
||||
async list(): Promise<{ settings: SettingResponse[] }> {
|
||||
const settings = await this.settingsService.list();
|
||||
return { settings };
|
||||
}
|
||||
|
||||
@Patch(':key')
|
||||
async update(
|
||||
@Param('key') key: string,
|
||||
@Body(new ZodValidationPipe(updateSettingSchema)) body: UpdateSettingDto,
|
||||
@CurrentUser() actor: AuthUser,
|
||||
@Req() request: AuthenticatedRequest & Request,
|
||||
): Promise<{ setting: SettingResponse }> {
|
||||
const setting = await this.settingsService.set(key, body.value, actor, request.ip ?? null);
|
||||
return { setting };
|
||||
}
|
||||
}
|
||||
15
apps/platform-backend/src/settings/settings.module.ts
Normal file
15
apps/platform-backend/src/settings/settings.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DatabaseModule } from '../database/database.module';
|
||||
import { AuditModule } from '../audit/audit.module';
|
||||
import { SettingsController } from './settings.controller';
|
||||
import { SettingsRepository } from './settings.repository';
|
||||
import { SettingsService } from './settings.service';
|
||||
|
||||
/** Systemeinstellungen (Phase 9). */
|
||||
@Module({
|
||||
imports: [DatabaseModule, AuditModule],
|
||||
controllers: [SettingsController],
|
||||
providers: [SettingsRepository, SettingsService],
|
||||
exports: [SettingsService],
|
||||
})
|
||||
export class SettingsModule {}
|
||||
78
apps/platform-backend/src/settings/settings.repository.ts
Normal file
78
apps/platform-backend/src/settings/settings.repository.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { DatabaseService } from '../database/database.service';
|
||||
|
||||
/** Systemeinstellung-Datensatz. */
|
||||
export interface SystemSettingRecord {
|
||||
readonly key: string;
|
||||
readonly value: string;
|
||||
readonly updatedAt: Date;
|
||||
readonly updatedBy: string | null;
|
||||
}
|
||||
|
||||
interface SettingRow {
|
||||
key: string;
|
||||
value: string;
|
||||
updated_at: Date;
|
||||
updated_by: string | null;
|
||||
}
|
||||
|
||||
/** Erlaubte Einstellungs-Schlüssel (Whitelist, verhindert Magic Strings). */
|
||||
export const SETTING_KEYS = {
|
||||
PLATFORM_NAME: 'platform.name',
|
||||
PLATFORM_DESCRIPTION: 'platform.description',
|
||||
MAINTENANCE_MODE: 'platform.maintenance_mode',
|
||||
} as const;
|
||||
export type SettingKey = (typeof SETTING_KEYS)[keyof typeof SETTING_KEYS];
|
||||
|
||||
/**
|
||||
* Systemeinstellungen (Infrastructure): Schlüssel-Werte-Store für
|
||||
* Plattform-Einstellungen. Nur whitelisted Schlüssel sind schreibbar.
|
||||
*/
|
||||
@Injectable()
|
||||
export class SettingsRepository {
|
||||
constructor(private readonly database: DatabaseService) {}
|
||||
|
||||
async list(): Promise<SystemSettingRecord[]> {
|
||||
const result = await this.database.query<SettingRow>(
|
||||
'SELECT key, value, updated_at, updated_by FROM system_settings ORDER BY key ASC',
|
||||
);
|
||||
return result.rows.map((row) => ({
|
||||
key: row.key,
|
||||
value: row.value,
|
||||
updatedAt: row.updated_at,
|
||||
updatedBy: row.updated_by,
|
||||
}));
|
||||
}
|
||||
|
||||
async get(key: string): Promise<SystemSettingRecord | null> {
|
||||
const result = await this.database.query<SettingRow>(
|
||||
'SELECT key, value, updated_at, updated_by FROM system_settings WHERE key = $1',
|
||||
[key],
|
||||
);
|
||||
const row = result.rows[0];
|
||||
return row ? { key: row.key, value: row.value, updatedAt: row.updated_at, updatedBy: row.updated_by } : null;
|
||||
}
|
||||
|
||||
/** Setzt oder aktualisiert eine Einstellung (Upsert). */
|
||||
async set(key: string, value: string, updatedBy: string): Promise<SystemSettingRecord> {
|
||||
const result = await this.database.query<SettingRow>(
|
||||
`INSERT INTO system_settings (key, value, updated_by)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (key)
|
||||
DO UPDATE SET value = $2, updated_at = now(), updated_by = $3
|
||||
RETURNING key, value, updated_at, updated_by`,
|
||||
[key, value, updatedBy],
|
||||
);
|
||||
const row = result.rows[0];
|
||||
return { key: row.key, value: row.value, updatedAt: row.updated_at, updatedBy: row.updated_by };
|
||||
}
|
||||
|
||||
/** Liest eine Einstellung oder wirft NotFoundException. */
|
||||
async getRequired(key: string): Promise<SystemSettingRecord> {
|
||||
const setting = await this.get(key);
|
||||
if (!setting) {
|
||||
throw new NotFoundException(`Einstellung "${key}" nicht gefunden`);
|
||||
}
|
||||
return setting;
|
||||
}
|
||||
}
|
||||
60
apps/platform-backend/src/settings/settings.service.ts
Normal file
60
apps/platform-backend/src/settings/settings.service.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { AUDIT_ACTIONS, AuditService } from '../audit/audit.service';
|
||||
import type { ActingUser } from '../users/users.service';
|
||||
import { SETTING_KEYS, SettingsRepository, type SystemSettingRecord } from './settings.repository';
|
||||
|
||||
/** Öffentliche Einstellungs-Daten in API-Antworten. */
|
||||
export interface SettingResponse {
|
||||
readonly key: string;
|
||||
readonly value: string;
|
||||
readonly updatedAt: string;
|
||||
readonly updatedBy: string | null;
|
||||
}
|
||||
|
||||
function toSettingResponse(setting: SystemSettingRecord): SettingResponse {
|
||||
return {
|
||||
key: setting.key,
|
||||
value: setting.value,
|
||||
updatedAt: setting.updatedAt.toISOString(),
|
||||
updatedBy: setting.updatedBy,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Systemeinstellungen (Application-Layer): Lesen und Schreiben von
|
||||
* Plattform-Einstellungen mit Whitelist-Prüfung und Audit-Logging.
|
||||
*/
|
||||
@Injectable()
|
||||
export class SettingsService {
|
||||
constructor(
|
||||
private readonly settingsRepository: SettingsRepository,
|
||||
private readonly auditService: AuditService,
|
||||
) {}
|
||||
|
||||
async list(): Promise<SettingResponse[]> {
|
||||
const settings = await this.settingsRepository.list();
|
||||
return settings.map(toSettingResponse);
|
||||
}
|
||||
|
||||
/** Aktualisiert eine Einstellung (nur whitelisted Schlüssel). */
|
||||
async set(
|
||||
key: string,
|
||||
value: string,
|
||||
actor: ActingUser,
|
||||
ipAddress: string | null,
|
||||
): Promise<SettingResponse> {
|
||||
if (!Object.values(SETTING_KEYS).includes(key as (typeof SETTING_KEYS)[keyof typeof SETTING_KEYS])) {
|
||||
throw new BadRequestException(`Unbekannter Einstellungs-Schlüssel: ${key}`);
|
||||
}
|
||||
|
||||
const setting = await this.settingsRepository.set(key, value, actor.username);
|
||||
await this.auditService.record({
|
||||
userId: actor.id,
|
||||
username: actor.username,
|
||||
action: AUDIT_ACTIONS.SETTINGS_CHANGED,
|
||||
details: { key },
|
||||
ipAddress,
|
||||
});
|
||||
return toSettingResponse(setting);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user