import { Injectable } from '@nestjs/common'; import * as fs from 'fs'; import * as path from 'path'; import { ConfigService } from '@nestjs/config'; import { SettingsService } from '../settings/settings.module'; import { ThemeLoaderService } from '../../common/utils/theme-loader.service'; import { SseHubService } from '../../common/services/sse-hub.service'; import { SETTINGS_KEYS } from '../../config/env.schema'; import { GeneralSettingsPayload } from './dto/general.dto'; export interface GeneralSettingsView { pageTitle: string; logo: string; welcomeMarkdown: string; themeKey: string; eventStartUtc: string; eventEndUtc: string; defaultChallengeIp: string; registrationsEnabled: boolean; } export interface ThemeView { id: string; key: string; name: string; } @Injectable() export class AdminGeneralService { constructor( private readonly settings: SettingsService, private readonly themes: ThemeLoaderService, private readonly hub: SseHubService, private readonly config: ConfigService, ) {} async getSettings(): Promise { const [pageTitle, logo, welcomeMarkdown, themeKey, eventStartUtc, eventEndUtc, defaultChallengeIp, registrationsEnabled] = await Promise.all([ this.settings.get(SETTINGS_KEYS.PAGE_TITLE, 'HIPCTF'), this.settings.get(SETTINGS_KEYS.LOGO, ''), this.settings.get(SETTINGS_KEYS.WELCOME_MARKDOWN, ''), this.settings.get(SETTINGS_KEYS.THEME_KEY, 'classic'), this.settings.get(SETTINGS_KEYS.EVENT_START_UTC, ''), this.settings.get(SETTINGS_KEYS.EVENT_END_UTC, ''), this.settings.get(SETTINGS_KEYS.DEFAULT_CHALLENGE_IP, '127.0.0.1'), this.settings.get(SETTINGS_KEYS.REGISTRATIONS_ENABLED, 'false'), ]); return { pageTitle, logo, welcomeMarkdown, themeKey, eventStartUtc, eventEndUtc, defaultChallengeIp, registrationsEnabled: registrationsEnabled === 'true', }; } async updateSettings(payload: GeneralSettingsPayload): Promise { await Promise.all([ this.settings.set(SETTINGS_KEYS.PAGE_TITLE, payload.pageTitle), this.settings.set(SETTINGS_KEYS.LOGO, payload.logo), this.settings.set(SETTINGS_KEYS.WELCOME_MARKDOWN, payload.welcomeMarkdown), this.settings.set(SETTINGS_KEYS.THEME_KEY, payload.themeKey), this.settings.set(SETTINGS_KEYS.EVENT_START_UTC, payload.eventStartUtc), this.settings.set(SETTINGS_KEYS.EVENT_END_UTC, payload.eventEndUtc), this.settings.set(SETTINGS_KEYS.DEFAULT_CHALLENGE_IP, payload.defaultChallengeIp), this.settings.set(SETTINGS_KEYS.REGISTRATIONS_ENABLED, payload.registrationsEnabled ? 'true' : 'false'), ]); this.hub.emitEvent({ topic: 'general', themeKey: payload.themeKey, registrationsEnabled: payload.registrationsEnabled, }); return this.getSettings(); } listThemes(): ThemeView[] { const themesDir = path.resolve(this.config.get('THEMES_DIR', './themes')); let present = new Set(); try { if (fs.existsSync(themesDir)) { for (const f of fs.readdirSync(themesDir)) { if (!f.endsWith('.json')) continue; try { const raw = fs.readFileSync(path.join(themesDir, f), 'utf-8'); const parsed = JSON.parse(raw) as { id?: string }; if (parsed?.id) present.add(parsed.id); } catch { /* skip unreadable */ } } } } catch { /* swallow */ } return this.themes .listThemes() .filter((t) => present.has(t.id)) .map((t) => ({ id: t.id, key: t.id, name: t.name })); } }