feat: Authenticated Shell: Header, Quick Tabs and Change Password
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user