105 lines
3.6 KiB
TypeScript
105 lines
3.6 KiB
TypeScript
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<GeneralSettingsView> {
|
|
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<GeneralSettingsView> {
|
|
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<string>('THEMES_DIR', './themes'));
|
|
let present = new Set<string>();
|
|
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 }));
|
|
}
|
|
}
|