feat: Authenticated Shell: Header, Quick Tabs and Change Password

This commit is contained in:
OpenVelo Agent
2026-07-21 22:26:44 +00:00
parent e5720c66dc
commit 1539a904c1
35 changed files with 1978 additions and 89 deletions
@@ -1,10 +1,11 @@
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 { interval, map, merge, mergeMap, Observable, startWith, defer, distinctUntilChanged } 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';
import { SettingsService } from '../settings/settings.module';
@ApiTags('system')
@Controller('api/v1')
@@ -13,6 +14,7 @@ export class SystemController {
private readonly system: SystemService,
private readonly statusSvc: EventStatusService,
private readonly hub: SseHubService,
private readonly settings: SettingsService,
) {}
@Public()
@@ -29,6 +31,15 @@ export class SystemController {
return this.statusSvc.getStatus();
}
@Public()
@Get('settings/event')
@ApiOperation({ summary: 'Public-readable event window timestamps (UTC)' })
async settingsEvent(): Promise<{ eventStartUtc: string | null; eventEndUtc: string | null }> {
const eventStartUtc = (await this.settings.get('eventStartUtc')) || null;
const eventEndUtc = (await this.settings.get('eventEndUtc')) || null;
return { eventStartUtc, eventEndUtc };
}
/**
* Global event status + countdown stream.
* Emits a FLATTENED object every second (and whenever the hub pushes).
@@ -62,6 +73,45 @@ export class SystemController {
return merge(tick$, hub$).pipe(map((src) => ({ data: src }) as MessageEvent));
}
/**
* Authenticated event status SSE.
* Path: GET /api/v1/events/status (note the plural 'events').
* Emits an initial state immediately on connect, then re-emits on transition
* and on a minimum 60s tick while in 'countdown' state.
* Protected by the global JwtAuthGuard (so unauthenticated clients get 401).
*/
@Sse('events/status')
eventsStatus(): Observable<MessageEvent> {
const flat = (s: any) => ({
state: s?.state,
serverNowUtc: s?.serverNowUtc,
eventStartUtc: s?.eventStartUtc,
eventEndUtc: s?.eventEndUtc,
secondsToStart: s?.secondsToStart,
secondsToEnd: s?.secondsToEnd,
});
const initial$ = defer(() =>
this.statusSvc.getState().then((s) => flat(s)),
);
const tick$ = interval(60_000).pipe(
startWith(0),
mergeMap(() =>
defer(() =>
this.statusSvc.getState().then((s) => flat(s)),
),
),
);
const hub$ = this.hub.event$().pipe(map((p) => flat(p)));
return merge(initial$, tick$, hub$).pipe(
distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)),
map((src) => ({ data: src }) as MessageEvent),
);
}
/**
* Live solves stream.
* Emits a FLATTENED solve payload whenever a new solve is published.
+2 -1
View File
@@ -4,9 +4,10 @@ import { UserEntity } from '../../database/entities/user.entity';
import { SettingEntity } from '../../database/entities/setting.entity';
import { SystemController } from './system.controller';
import { SystemService } from './system.service';
import { SettingsModule } from '../settings/settings.module';
@Module({
imports: [TypeOrmModule.forFeature([UserEntity, SettingEntity])],
imports: [TypeOrmModule.forFeature([UserEntity, SettingEntity]), SettingsModule],
controllers: [SystemController],
providers: [SystemService],
})