AI Implementation feature(820): Project Scaffold and Platform Foundations (#1)

This commit was merged in pull request #1.
This commit is contained in:
2026-07-21 14:23:53 +00:00
parent e55c9cda56
commit 03bcb6b156
131 changed files with 23657 additions and 0 deletions
@@ -0,0 +1,83 @@
import { Controller, Get, Sse, MessageEvent } from '@nestjs/common';
import { ApiTags, ApiOperation } from '@nestjs/swagger';
import { interval, map, merge, mergeMap, Observable, startWith, defer } from 'rxjs';
import { Public } from '../../common/decorators/public.decorator';
import { SystemService } from './system.service';
import { EventStatusService } from '../../common/services/event-status.service';
import { SseHubService } from '../../common/services/sse-hub.service';
@ApiTags('system')
@Controller('api/v1')
export class SystemController {
constructor(
private readonly system: SystemService,
private readonly statusSvc: EventStatusService,
private readonly hub: SseHubService,
) {}
@Public()
@Get('bootstrap')
@ApiOperation({ summary: 'Public bootstrap payload used by the SPA on startup' })
bootstrap() {
return this.system.bootstrap();
}
@Public()
@Get('event/status')
@ApiOperation({ summary: 'Event status derived from UTC timestamps' })
eventStatus() {
return this.statusSvc.getStatus();
}
/**
* Global event status + countdown stream.
* Emits a FLATTENED object every second (and whenever the hub pushes).
*/
@Public()
@Sse('event/stream')
eventStream(): Observable<MessageEvent> {
const tick$ = interval(1000).pipe(
startWith(0),
mergeMap(() =>
defer(() =>
this.statusSvc.getStatus().then((s) => ({
status: s.status,
countdownMs: s.countdownMs,
serverNowUtc: s.serverNowUtc,
startUtc: s.startUtc,
endUtc: s.endUtc,
})),
),
),
);
const hub$ = this.hub.event$().pipe(
map((payload: any) => ({
status: payload?.status,
countdownMs: payload?.countdownMs,
serverNowUtc: payload?.serverNowUtc,
startUtc: payload?.startUtc,
endUtc: payload?.endUtc,
})),
);
return merge(tick$, hub$).pipe(map((src) => ({ data: src }) as MessageEvent));
}
/**
* Live solves stream.
* Emits a FLATTENED solve payload whenever a new solve is published.
*/
@Public()
@Sse('scoreboard/stream')
scoreboardStream(): Observable<MessageEvent> {
return this.hub.scoreboard$().pipe(
map((s: any) => ({
challengeId: s?.challengeId,
userId: s?.userId,
pointsAwarded: s?.pointsAwarded,
rankBonus: s?.rankBonus,
solvedAt: s?.solvedAt,
})),
map((src) => ({ data: src }) as MessageEvent),
);
}
}
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserEntity } from '../../database/entities/user.entity';
import { SettingEntity } from '../../database/entities/setting.entity';
import { SystemController } from './system.controller';
import { SystemService } from './system.service';
@Module({
imports: [TypeOrmModule.forFeature([UserEntity, SettingEntity])],
controllers: [SystemController],
providers: [SystemService],
})
export class SystemModule {}
@@ -0,0 +1,45 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserEntity } from '../../database/entities/user.entity';
import { SettingEntity } from '../../database/entities/setting.entity';
import { ThemeLoaderService } from '../../common/utils/theme-loader.service';
import { SETTINGS_KEYS } from '../../config/env.schema';
export interface BootstrapPayload {
initialized: boolean;
pageTitle: string;
logo: string;
welcomeMarkdown: string;
theme: any;
defaultChallengeIp: string;
registrationsEnabled: boolean;
}
@Injectable()
export class SystemService {
constructor(
@InjectRepository(UserEntity) private readonly users: Repository<UserEntity>,
@InjectRepository(SettingEntity) private readonly settings: Repository<SettingEntity>,
private readonly themes: ThemeLoaderService,
) {}
async bootstrap(): Promise<BootstrapPayload> {
const adminCount = await this.users.count({ where: { role: 'admin' } });
const themeKey = await this.getSetting(SETTINGS_KEYS.THEME_KEY, 'classic');
return {
initialized: adminCount > 0,
pageTitle: await this.getSetting(SETTINGS_KEYS.PAGE_TITLE, 'HIPCTF'),
logo: await this.getSetting(SETTINGS_KEYS.LOGO, ''),
welcomeMarkdown: await this.getSetting(SETTINGS_KEYS.WELCOME_MARKDOWN, ''),
theme: this.themes.getTheme(themeKey),
defaultChallengeIp: await this.getSetting(SETTINGS_KEYS.DEFAULT_CHALLENGE_IP, '127.0.0.1'),
registrationsEnabled: (await this.getSetting(SETTINGS_KEYS.REGISTRATIONS_ENABLED, 'false')) === 'true',
};
}
private async getSetting(key: string, fallback: string): Promise<string> {
const row = await this.settings.findOne({ where: { key } });
return row?.value ?? fallback;
}
}