From 18af97cea504dc7a5f7cd4607cdb5e619dddf3dc Mon Sep 17 00:00:00 2001 From: OpenVelo Agent Date: Tue, 21 Jul 2026 23:22:03 +0000 Subject: [PATCH 1/2] docs: update documentation to OKF v0.1 format --- docs/api/system.md | 5 +- docs/architecture/frontend-structure.md | 174 ++++++-------------- docs/architecture/key-files.md | 202 ++++-------------------- docs/guides/authenticated-shell.md | 185 +++++++--------------- docs/guides/event-window.md | 12 +- docs/index.md | 2 +- 6 files changed, 145 insertions(+), 435 deletions(-) diff --git a/docs/api/system.md b/docs/api/system.md index 5f1adc8..b175a49 100644 --- a/docs/api/system.md +++ b/docs/api/system.md @@ -114,7 +114,10 @@ Each frame: See `backend/src/common/services/event-status.service.ts` for the authoritative state machine; see [Event Window Guide](/guides/event-window.md) for the full diagram and -the SPA wiring. +the SPA wiring. The authenticated browser consumer uses the fetch-based +transport in `frontend/src/app/core/services/authenticated-event-source.service.ts` +so it can send the JWT `Authorization` header; native `EventSource` cannot set +that header. # `GET /api/v1/event/stream` (public SSE, legacy) diff --git a/docs/architecture/frontend-structure.md b/docs/architecture/frontend-structure.md index 59feb69..442c4d7 100644 --- a/docs/architecture/frontend-structure.md +++ b/docs/architecture/frontend-structure.md @@ -1,152 +1,66 @@ --- type: architecture title: Frontend Structure -description: Angular routes, components, services, guards, and interceptors. +description: Angular routes, components, services, guards, interceptors, and authenticated SSE transport. tags: [architecture, frontend, angular, shell, sse] -timestamp: 2026-07-21T22:19:08Z +timestamp: 2026-07-21T23:25:00Z --- # Routes Routes live in `frontend/src/app/app.routes.ts`: -| Path | Component | Guard | Notes | -|-----------------|----------------------------------|---------------|----------------------------------------| -| `/bootstrap` | `SetupCreateAdminComponent` | — | Lazy-loaded first-admin creation route. Non-dismissible modal overlay while `initialized === false`; successful creation navigates to `/admin`. | -| `/login` | `LandingComponent` | `landingGuard`| Public landing page that hosts the login (and registration, when enabled) modal. The `landingGuard` redirects to `/bootstrap` until the instance is initialized and to `/` if the user is already authenticated. | -| `/` | `HomeComponent` (shell) | `authGuard` | Requires auth + initialized. Hosts the header (LED + countdown + user menu), quick-jump tabs (Challenges/Scoreboard/Blog), and a `` for child routes. Default child route redirects to `/challenges`. | -| `/challenges` | `ChallengesPage` | inherits `authGuard` | Placeholder child of `/`; renders an empty page until the challenges Job lands. | -| `/scoreboard` | `ScoreboardPage` | inherits `authGuard` | Placeholder child of `/`; renders an empty page until the scoreboard Job lands. | -| `/blog` | `BlogPage` | inherits `authGuard` | Placeholder child of `/`; renders an empty page until the blog Job lands. | -| `/admin` | `AdminUsersComponent` | `adminGuard` | Child of `/`; requires `role === 'admin'`. | -| `**` | Redirect to `/` | — | Wildcard fallback. | +| Path | Component | Guard | Notes | +|---|---|---|---| +| `/bootstrap` | `SetupCreateAdminComponent` | — | First-admin creation flow. | +| `/login` | `LandingComponent` | `landingGuard` | Public landing and authentication modal. | +| `/` | `HomeComponent` | `authGuard` | Authenticated shell and child outlet. | +| `/challenges` | `ChallengesPage` | inherited | Placeholder challenges page. | +| `/scoreboard` | `ScoreboardPage` | inherited | Placeholder scoreboard page. | +| `/blog` | `BlogPage` | inherited | Placeholder blog page. | +| `/admin` | `AdminUsersComponent` | `adminGuard` | Admin-only child route. | -# Components +# Components and services -All components are standalone (no NgModules). Each component imports -`FormsModule` directly and uses Angular signals for state. +| Symbol | Path | Responsibility | +|---|---|---| +| `HomeComponent` | `frontend/src/app/features/home/home.component.ts` | Smart shell; owns navigation signals, user hydration, change-password flow, and event-stream lifecycle. | +| `ShellHeaderComponent` | `frontend/src/app/features/shell/header/shell-header.component.ts` | Renders title, active section, event LED/countdown, and user menu. | +| `QuickTabsComponent` | `frontend/src/app/features/shell/tabs/quick-tabs.component.ts` | Emits navigation events for Challenges, Scoreboard, and Blog. | +| `EventStatusStore` | `frontend/src/app/core/services/event-status.store.ts` | Applies event state frames and computes countdown text. | +| `AuthenticatedEventSourceService` | `frontend/src/app/core/services/authenticated-event-source.service.ts` | Opens authenticated SSE over `fetch`, parsing streamed frames into `EventSourceLike` events. | +| `AuthService` | `frontend/src/app/core/services/auth.service.ts` | Stores the access token used by the authenticated SSE transport. | +| `UserStore` | `frontend/src/app/core/services/user.store.ts` | Hydrates the signed-in user and rank data. | -| Component | Path | Purpose | -|------------------------|-----------------------------------------------------------------|-------------------------------------------------| -| `AppComponent` | `frontend/src/app/app.component.ts` | Root; renders ``, calls `BootstrapService.load()` then `AuthService.restoreSession()`. | -| `HomeComponent` | `frontend/src/app/features/home/home.component.ts` | Smart authenticated shell. Hosts the `ShellHeaderComponent`, `QuickTabsComponent`, the `ChangePasswordModalComponent`, and the `` for child routes. Owns the active section / active tab signals and the SSE lifecycle for `EventStatusStore`. | -| `ShellHeaderComponent` | `frontend/src/app/features/shell/header/shell-header.component.ts` | Dumb presentational header. Inputs: `pageTitle`, `activeSection`, `eventState`, `countdownText`, `username`, `rankText`, `canAccessAdmin`, `userMenuOpen`. Outputs: `titleClick`, `usernameMenuToggle`, `rankClick`, `changePasswordClick`, `adminClick`, `logoutClick`. Renders the LED (state class), `DD:HH:mm` countdown, and an inline user menu dropdown (`Change password`, optional `Admin area`, `Logout`). | -| `QuickTabsComponent` | `frontend/src/app/features/shell/tabs/quick-tabs.component.ts` | Dumb presentational tab strip. Inputs: `tabs: ShellTab[]`, `active: string`. Output: `tabChange: {id}`. | -| `ChangePasswordModalComponent` | `frontend/src/app/features/shell/change-password/change-password-modal.component.ts` | Dumb reactive-form modal with three fields (`oldPassword` only in `mode='self'`), `passwordMatchValidator` group validation, policy hint from `BootstrapService.passwordPolicy()`, and `errorMessage`/`submitting` inputs. Emits `submit(payload)` and `cancel()`. Closes on Escape and on overlay click. | -| `ChallengesPage` | `frontend/src/app/features/challenges/challenges.page.ts` | Placeholder child route. | -| `ScoreboardPage` | `frontend/src/app/features/scoreboard/scoreboard.page.ts` | Placeholder child route. | -| `BlogPage` | `frontend/src/app/features/blog/blog.page.ts` | Placeholder child route. | -| `AdminUsersComponent` | `frontend/src/app/features/admin/admin-users.component.ts` | Admin-only user list rendered inside the home shell. | -| `LandingComponent` | `frontend/src/app/features/landing/landing.component.{ts,html,css}` | Public landing page: logo, page title, rendered `welcomeMarkdown`, "Login" button, blog post list, and the modal that contains both the login and registration forms (mode toggled by a single signal). | -| `LandingService` | `frontend/src/app/features/landing/landing.service.ts` | Fetches `GET /api/v1/blog/posts`, exposes `posts`/`loading`/`error` signals, `refresh()`. | -| `LoginModalService` (pure helpers) | `frontend/src/app/features/landing/login-modal.service.ts` | Pure `buildLoginFailureMessage` that maps an API error envelope into user-facing copy (and extracts a `retryAfterSeconds` for `RATE_LIMITED`). | -| `SetupCreateAdminComponent` | `frontend/src/app/features/setup/setup-create-admin.component.ts` | First-admin bootstrap modal with typed reactive form, inline validation messages (via [the field-error helpers](/guides/setup-field-errors.md)), retry handling, session initialization, and redirect to `/admin`. | -| `HomeShell` (pure helpers) | `frontend/src/app/features/home/home.shell.ts` | Pure `shouldShowAdminNav({isAuthenticated, role})` + `deriveActiveSectionFromUrl(url)` predicates (unit-tested). | +# Authenticated SSE wiring -# Services +`HomeComponent.ngOnInit()` calls `EventStatusStore.start()` with a factory for +`AuthenticatedEventSourceService.open('/api/v1/events/status')`. The transport: -| Service | Path | Purpose | -|---------------------|---------------------------------------------------------------|-----------------------------------------------------------| -| `AuthService` | `frontend/src/app/core/services/auth.service.ts` | Signal-backed access token + current user; persists session to `sessionStorage`, exposes `restoreSession()` + `waitUntilHydrated()`, plus `login()` / `register()` / `logout()` / `me()` / `changePassword()` (each calls `ensureCsrf()` first). Returns discriminated `LoginResult` / `RegisterResult` / `ChangePasswordResult` for component-level error handling. | -| `BootstrapService` | `frontend/src/app/core/services/bootstrap.service.ts` | Fetches `/api/v1/bootstrap`, applies theme tokens to CSS, exposes a `passwordPolicy` computed signal derived from the bootstrap payload; `load()` / `ready()` deduplicate the in-flight fetch. | -| `EventStatusStore` | `frontend/src/app/core/services/event-status.store.ts` | Signal store for the live event window. Owns `state`/`serverNowUtc`/`eventStartUtc`/`eventEndUtc`, derives a `countdownText` computed signal (`DD:HH:mm` or `"Event ended"`), and exposes `start(createSource)` / `stop()` so `HomeComponent` can drive a 1-second tick that re-derives the seconds-to-start/end locally between SSE pushes. | -| `EventStatusPure` | `frontend/src/app/core/services/event-status.pure.ts` | Pure helpers consumed by the store and by tests: `deriveCountdownText(state, sStart, sEnd)`, `formatDdHhMm(totalSeconds)`, `EventSourceLike` interface, and the `EventState`/`EventStatePayload` types. | -| `UserStore` | `frontend/src/app/core/services/user.store.ts` | Signal store for the authenticated user (id/username/role/rank/points + loading/error). `hydrateFromAuth()` mirrors `AuthService.currentUser()`; `loadMe()` calls `AuthService.me()` and merges rank/points. Exposes `rankText` (`"Rank: - Points:

"`). | -| `MarkdownService` | `frontend/src/app/core/services/markdown.service.ts` | Wraps `DomSanitizer.bypassSecurityTrustHtml` around the pure `renderMarkdownToHtml` helper. Used to render `welcomeMarkdown` and blog post bodies. | -| `AuthSessionStorage` (helpers) | `frontend/src/app/core/services/auth.session-storage.ts` | Pure `readStoredSession` / `writeStoredSession` / `clearStoredSession` against `sessionStorage` (key `hipctf.auth.v1`). | -| `SetupCreateAdminService` | `frontend/src/app/features/setup/setup-create-admin.service.ts` | Preflights the CSRF cookie, calls `POST /api/v1/setup/create-admin`, and normalizes success/failure responses. | -| `ChangePassword` (pure helpers) | `frontend/src/app/features/shell/change-password/password-feedback.ts` | `formatChangePasswordError({code, message})` maps the API error code (`INVALID_OLD_PASSWORD` / `PASSWORDS_DO_NOT_MATCH` / `PASSWORD_POLICY` / `UNAUTHORIZED`) to user-facing copy. | -| `ChangePassword` (validators) | `frontend/src/app/features/shell/change-password/change-password-modal.validators.ts` | `passwordMatchValidator` (group-level reactive-forms validator) + `PasswordPolicy` zod schema used by the modal. | +1. Reads the access token from `AuthService`. +2. Sends `Accept: text/event-stream`, optional `Authorization: Bearer `, + `credentials: 'include'`, and an `AbortSignal`. +3. Reads the response body with a stream reader and decodes UTF-8 chunks. +4. Splits complete SSE records on blank lines, joins repeated `data:` lines, and + dispatches `MessageEvent` objects. +5. Buffers records received before the message listener is attached. +6. Aborts and clears listeners when `close()` is called. -# Guards and interceptors +`EventStatusStore` parses each message as `EventStatePayload`, updates its signals, +and runs a one-second local timer. `HomeComponent` stops the store on destruction. -| Symbol | Path | Purpose | -|---------------------|---------------------------------------------------------------|-----------------------------------------------------------| -| `authGuard` | `frontend/src/app/core/guards/auth.guard.ts` | Async `CanActivateFn`; awaits `BootstrapService.ready()` and `AuthService.waitUntilHydrated()`, then delegates to `decideAuthRedirect`. | -| `decideAuthRedirect`| `frontend/src/app/core/guards/auth.guard.decision.ts` | Pure decision function mirroring `decideAdminGuard`; redirects to `/bootstrap` or `/login`, returns `true` otherwise. | -| `adminGuard` | `frontend/src/app/core/guards/admin.guard.ts` | Async `CanActivateFn`; awaits bootstrap + auth hydration, then delegates to `decideAdminGuard` (pure). Redirects to `/bootstrap`, `/login`, or `/` based on state; allows only admins. | -| `decideAdminGuard` | `frontend/src/app/core/guards/admin.guard.decision.ts` | Pure decision function: returns `{kind:'allow'}` or `{kind:'redirect', path}`. | -| `landingGuard` | `frontend/src/app/core/guards/landing.guard.ts` | Async `CanActivateFn` for `/login`; awaits bootstrap + auth hydration, then delegates to the pure `decideLandingGuard`. Redirects to `/bootstrap` (not initialized) or `/` (already authenticated). | -| `decideLandingGuard`| `frontend/src/app/core/guards/landing.guard.decision.ts` | Pure decision function: returns `true` to render the landing page, or `UrlTree` to `/bootstrap` / `/`. | -| `authInterceptor` | `frontend/src/app/core/interceptors/auth.interceptor.ts` | Attaches `Authorization: Bearer ` header. | -| `csrfInterceptor` | `frontend/src/app/core/interceptors/csrf.interceptor.ts` | Attaches `X-CSRF-Token` header on POST/PUT/PATCH/DELETE. | +# Key integration files -Both interceptors are wired in `frontend/src/main.ts` via -`provideHttpClient(withInterceptors([csrfInterceptor, authInterceptor]))` -(notably CSRF runs first so the token is attached before the auth header). - -# Bootstrap flow - -1. `AppComponent.ngOnInit()` → `BootstrapService.load()` then - `AuthService.restoreSession()`. Both are deduplicated so concurrent - callers share a single in-flight promise (`loadPromise` / - `hydrateWaiters`). -2. `BootstrapService` calls `GET /api/v1/bootstrap` (public, with - `credentials: 'include'`). -3. The payload sets `initialized` (true iff any admin user exists), - `pageTitle`, `logo`, `welcomeMarkdown`, the active `theme`, the - `defaultChallengeIp`, the `registrationsEnabled` flag, and the - `passwordPolicy` descriptor consumed by the change-password modal. -4. Theme tokens are written to CSS custom properties on - `document.documentElement` (`--color-primary`, `--font-family`, - `--radius-*`, etc.) consumed by `frontend/src/styles.css`. -5. `AuthService.restoreSession()` rehydrates the in-memory signals from - `sessionStorage` (key `hipctf.auth.v1`) and POSTs - `/api/v1/auth/refresh` with `withCredentials: true` to mint a fresh - access token. On success it persists the new token; on failure it - clears storage. Either way it flips `hydrated()` so the guards can - unblock. -6. The `authGuard` (and `adminGuard`) `await` `BootstrapService.ready()` - and `AuthService.waitUntilHydrated()` before reading `initialized()` - / `isAuthenticated()`, which prevents a flash of `/bootstrap` or - `/login` on a hard refresh of a deep link. - -# Authenticated shell flow - -After successful auth, `HomeComponent` renders a full shell layout: - -1. **`ShellHeaderComponent`** is fed (signal inputs) the page title - from bootstrap, the active section label (computed from the current - router URL), the `EventStatusStore.state()` (used to colour the - LED — `running` / `countdown` / `stopped` / `unconfigured`), and the - `countdownText()` (`DD:HH:mm` or `"Event ended"`). The user menu - trigger opens an inline dropdown with `Rank: - Points:

`, - `Change password`, optional `Admin area` (visible only when - `canAccessAdmin()` is true), and `Logout`. -2. **`QuickTabsComponent`** renders `Challenges / Scoreboard / Blog` - tabs. The `active` input is computed from the current URL - (`HomeComponent.activeTabId()`). Clicking a tab emits `tabChange` - and the parent calls `router.navigateByUrl('/' + id)` (or `/admin` - for `id === 'admin'`). -3. The `` renders whichever child route is active - (`ChallengesPage`, `ScoreboardPage`, `BlogPage`, or `AdminUsersComponent`). -4. On `ngOnInit`, `HomeComponent` calls - `UserStore.hydrateFromAuth()` + `UserStore.loadMe()` (which calls - `AuthService.me()` → `GET /api/v1/auth/me`) and starts the SSE - consumer via `EventStatusStore.start(() => new EventSource('/api/v1/events/status', { withCredentials: true }))`. - The store parses each `message` frame, applies the state via - `applyServerStatus(payload)`, and ticks the local - `secondsToStart` / `secondsToEnd` signals every second. -5. On `ngOnDestroy` (and via `DestroyRef.onDestroy` for safety), - `HomeComponent` calls `EventStatusStore.stop()` to close the SSE - source and clear the tick interval. -6. Clicking `Change password` in the header opens the - `ChangePasswordModalComponent` (mode `'self'`). Submitting posts - `/api/v1/auth/change-password` via `AuthService.changePassword()`; - on success the modal closes and the user's existing refresh tokens - are revoked server-side. See the - [Change Password Guide](/guides/change-password.md) for the full - tester flow. -7. Clicking `Logout` calls `AuthService.logout()` (which POSTs - `/api/v1/auth/logout`), resets the `UserStore`, and navigates to - `/login`. +| File | Responsibility | +|---|---| +| `frontend/src/app/app.routes.ts` | Registers shell and child routes. | +| `frontend/src/app/core/interceptors/auth.interceptor.ts` | Adds Bearer authentication to Angular HTTP requests. | +| `frontend/src/app/core/services/authenticated-event-source.service.ts` | Adds Bearer authentication to the browser SSE request, which does not use the Angular HTTP interceptor chain. | +| `frontend/src/app/core/services/event-status.pure.ts` | Defines the event payload and transport interface. | +| `tests/frontend/authenticated-event-source.spec.ts` | Regression coverage for authenticated SSE transport behavior. | # See also -- [System Overview](/architecture/overview.md) -- [Backend Module Map](/architecture/backend-modules.md) -- [Key Files Index](/architecture/key-files.md) -- [First-Run Bootstrap](/guides/bootstrap.md) -- [Admin Shell & User Management](/guides/admin-shell.md) - [Authenticated Shell](/guides/authenticated-shell.md) -- [Change Password](/guides/change-password.md) +- [Event Window](/guides/event-window.md) +- [System Overview](/architecture/overview.md) diff --git a/docs/architecture/key-files.md b/docs/architecture/key-files.md index ba6279a..de069ab 100644 --- a/docs/architecture/key-files.md +++ b/docs/architecture/key-files.md @@ -1,185 +1,47 @@ --- type: architecture title: Key Files Index -description: One-line responsibility for every important source file in the repository. +description: One-line responsibility for important source files, including authenticated event streaming. tags: [architecture, index, key-files] -timestamp: 2026-07-21T22:19:08Z +timestamp: 2026-07-21T23:25:00Z --- # Backend -## Entrypoint & config - -| File | Responsibility | -|--------------------------------------------------------------------------|--------------------------------------------------------------------------------| -| `backend/src/main.ts` | Bootstraps Nest, registers middleware, builds OpenAPI 3.1, mounts SPA fallback. | -| `backend/src/app.module.ts` | Root module; wires every feature module + global guards/filters. | -| `backend/src/config/env.schema.ts` | Zod-validated env schema, `SETTINGS_KEYS`, system category metadata. | - -## Database - -| File | Responsibility | -|--------------------------------------------------------------------------|--------------------------------------------------------------------------------| -| `backend/src/database/database.module.ts` | TypeORM `better-sqlite3` config, entity + migration registration. | -| `backend/src/database/database-init.service.ts` | Initializes the DataSource, enforces WAL PRAGMA, runs migrations, verifies seed. | -| `backend/src/database/migrations/1700000000000-InitSchema.ts` | Creates all tables and sets WAL + foreign keys. | -| `backend/src/database/migrations/1700000000100-SeedSystemData.ts` | Seeds the 6 system categories and default settings. | -| `backend/src/database/entities/user.entity.ts` | `user` table (admin/player role, enabled/disabled status). | -| `backend/src/database/entities/setting.entity.ts` | `setting` table. | -| `backend/src/database/entities/category.entity.ts` | `category` table with `system_key` unique index. | -| `backend/src/database/entities/challenge.entity.ts` | `challenge` table (difficulty, decay, protocol, flag, port). | -| `backend/src/database/entities/challenge-file.entity.ts` | `challenge_file` table for attachments. | -| `backend/src/database/entities/solve.entity.ts` | `solve` table with unique `(challenge_id, user_id)` (used by `UsersRankService`). | -| `backend/src/database/entities/refresh-token.entity.ts` | `refresh_token` table (token_hash + revocation). | -| `backend/src/database/entities/blog-post.entity.ts` | `blog_post` table (draft/published + publishedAt index). | - -## Common - -| File | Responsibility | -|--------------------------------------------------------------------------|--------------------------------------------------------------------------------| -| `backend/src/common/common.module.ts` | Aggregates common services, middleware, and providers. | -| `backend/src/common/middleware/csrf.middleware.ts` | Double-submit CSRF check; mints/reads cookie, compares header. | -| `backend/src/common/guards/jwt-auth.guard.ts` | Global JWT guard; honors `@Public()`. | -| `backend/src/common/guards/admin.guard.ts` | Requires `req.user.role === 'admin'`. | -| `backend/src/common/guards/roles.guard.ts` | Enforces `@Roles(...)` metadata. | -| `backend/src/common/decorators/public.decorator.ts` | `@Public()` marker. | -| `backend/src/common/decorators/roles.decorator.ts` | `@Roles(...)` metadata. | -| `backend/src/common/decorators/skip-csrf.decorator.ts` | `@SkipCsrf()` marker. | -| `backend/src/common/filters/global-exception.filter.ts` | Converts all errors to the standard envelope. | -| `backend/src/common/pipes/zod-validation.pipe.ts` | Validates body/param/query with a zod schema. | -| `backend/src/common/interceptors/transform.interceptor.ts` | Optional response transformer. | -| `backend/src/common/services/event-status.service.ts` | 4-state machine (`countdown`/`running`/`stopped`/`unconfigured`) via `getState()`; legacy `getStatus()` shape preserved for backward compatibility. | -| `backend/src/common/services/login-backoff.service.ts` | Per-IP+username failed-login throttle. | -| `backend/src/common/services/registration-rate-limit.service.ts` | Per-IP first-admin registration throttle. | -| `backend/src/common/services/sse-hub.service.ts` | In-process pub/sub for event status and scoreboard streams. | -| `backend/src/common/utils/openapi31.ts` | Converts the Nest-generated OpenAPI 3.0 doc to OpenAPI 3.1. | -| `backend/src/common/utils/password-policy.ts` | Argon2 password validator (length, mixed-case policy). | -| `backend/src/common/utils/theme-loader.service.ts` | Loads themes from disk, backfills built-ins, validates tokens. | -| `backend/src/common/utils/upload.ts` | Upload size-limit parser + filename sanitizer. | -| `backend/src/common/types/theme.ts`, `theme-ids.ts` | `Theme` interface and the 10 canonical `ThemeId`s. | -| `backend/src/common/errors/api-error.ts` | `ApiError` class (status + machine code + message + details). | -| `backend/src/common/errors/error-codes.ts` | Canonical `ERROR_CODES` enum (now includes `INVALID_OLD_PASSWORD`, `PASSWORD_POLICY`, `PASSWORDS_DO_NOT_MATCH`). | - -## Modules - -| File | Responsibility | -|--------------------------------------------------------------------------|--------------------------------------------------------------------------------| -| `backend/src/modules/auth/auth.module.ts` | Wires `AuthController`, `AuthService`, `JwtStrategy`; imports `SettingsModule` for the public register flow and `UsersModule` (forwardRef) for `UsersRankService`. | -| `backend/src/modules/auth/auth.controller.ts` | `/api/v1/auth/{register,login,refresh,logout,csrf,me,change-password}`. `logout`, `me`, and `change-password` are JWT-protected (CSRF-enforced on `change-password`). | -| `backend/src/modules/auth/auth.service.ts` | Login, refresh, logout, register-first-admin, public player self-registration (`registerPlayer`), session minting, `getMe(userId)` (rank + points), `changePassword(userId, dto)` (revoke-all-refresh-tokens on success). | -| `backend/src/modules/auth/cookie.ts` | `setRefreshCookie` / `clearRefreshCookie` helpers. | -| `backend/src/modules/auth/dto/auth.dto.ts` | Zod schemas for login + refresh + `RegisterDtoSchema` + `ChangePasswordDtoSchema` + `MeResponseDtoSchema`. | -| `backend/src/modules/auth/strategies/jwt.strategy.ts` | Passport JWT strategy. | -| `backend/src/modules/users/users.module.ts` | Wires `UsersController` + `UsersService` + `UsersRankService`. | -| `backend/src/modules/users/users-rank.service.ts` | Pure provider `rankOfUser(manager, userId)` returning `{rank, points}` by summing `pointsAwarded` from `solve` and counting distinct users ahead. | -| `backend/src/modules/users/users.controller.ts` | `/api/v1/auth/register-first-admin`. | -| `backend/src/modules/users/users.service.ts` | `enforceLastAdminInvariant` helper. | -| `backend/src/modules/users/dto/create-first-admin.dto.ts` | Zod schema for first-admin registration. | -| `backend/src/modules/setup/setup.module.ts` | Wires `SetupController` + `SetupService` (dedicated first-admin bootstrap flow). | -| `backend/src/modules/setup/setup.controller.ts` | `POST /api/v1/setup/create-admin` (returns 404 once an admin exists). | -| `backend/src/modules/setup/setup.service.ts` | Hashes password (Argon2id), runs the create inside a transaction, mints session via `AuthService.createSession`. | -| `backend/src/modules/setup/dto/create-admin.dto.ts` | Zod schema with `password === passwordConfirm` refine. | -| `backend/src/modules/admin/admin.module.ts` | Wires `AdminController` + `AdminService`. | -| `backend/src/modules/admin/admin.controller.ts` | `/api/v1/admin/users[/{id}]`. | -| `backend/src/modules/admin/admin.service.ts` | list / create / update role / delete user with last-admin guard. | -| `backend/src/modules/admin/dto/admin.dto.ts` | Zod schemas for admin endpoints. | -| `backend/src/modules/system/system.module.ts` | Wires `SystemController` + `SystemService` + status + hub services. | -| `backend/src/modules/system/system.controller.ts` | `/api/v1/{bootstrap,event/status,event/stream,settings/event,scoreboard/stream}` (public) + authenticated SSE `/events/status` (plural, JWT-protected). | -| `backend/src/modules/system/system.service.ts` | Builds the public bootstrap payload from settings + theme + admin count + password policy. | -| `backend/src/modules/uploads/uploads.module.ts` | Wires `UploadsController`. | -| `backend/src/modules/uploads/uploads.controller.ts` | `/api/v1/uploads/{category-icon,challenge-file}`. | -| `backend/src/modules/blog/blog.module.ts` | Wires `BlogController` + `BlogService` (registers `BlogPostEntity`). | -| `backend/src/modules/blog/blog.controller.ts` | `GET /api/v1/blog/posts` (public). | -| `backend/src/modules/blog/blog.service.ts` | `listPublished()` returns published posts ordered by `publishedAt DESC`. | -| `backend/src/modules/blog/dto/blog.dto.ts` | Zod schemas `PublicBlogPostSchema` + `PublicBlogListSchema`. | -| `backend/src/modules/settings/settings.module.ts` | `SettingsService` (get/set/getAll over the `setting` table). | -| `backend/src/frontend/frontend.module.ts` | Wires SPA fallback + uploads static middleware. | -| `backend/src/frontend/spa.controller.ts` | `SpaFallbackMiddleware` (returns `index.html` for client routes). | -| `backend/src/frontend/uploads-static.middleware.ts` | Mounts `/uploads` static helper. | - -## Themes - -| File | Responsibility | -|----------------------------------------------|-------------------------------------------------------------| -| `backend/themes/01-classic.json` | `classic` theme tokens. | -| `backend/themes/02-midnight.json` … `10-monochrome.json` | The other 9 canonical themes. | +| File | Responsibility | +|---|---| +| `backend/src/main.ts` | Bootstraps Nest, middleware, OpenAPI, static assets, and SPA fallback. | +| `backend/src/app.module.ts` | Wires feature modules and global providers. | +| `backend/src/database/database.module.ts` | Configures TypeORM with SQLite. | +| `backend/src/database/database-init.service.ts` | Initializes the database and runs migrations. | +| `backend/src/common/guards/jwt-auth.guard.ts` | Global JWT authorization guard. | +| `backend/src/common/middleware/csrf.middleware.ts` | Double-submit CSRF protection. | +| `backend/src/common/services/event-status.service.ts` | Computes the event-window state machine. | +| `backend/src/common/services/sse-hub.service.ts` | In-process pub/sub for server-sent event streams. | +| `backend/src/modules/system/system.controller.ts` | Registers bootstrap, event status, settings, and SSE endpoints. | +| `backend/src/modules/system/system.service.ts` | Builds the public bootstrap payload. | +| `backend/src/modules/auth/auth.controller.ts` | Registers authentication and account endpoints. | +| `backend/src/modules/auth/auth.service.ts` | Handles sessions, authentication, registration, and password changes. | # Frontend -| File | Responsibility | -|-----------------------------------------------------|--------------------------------------------------------------------------------| -| `frontend/src/main.ts` | Bootstraps the standalone Angular app + registers interceptors. | -| `frontend/src/index.html` | Root HTML shell. | -| `frontend/src/styles.css` | Global styles consuming theme CSS custom properties. | -| `frontend/src/app/app.component.ts` | Root component; triggers bootstrap then `AuthService.restoreSession()` on init. | -| `frontend/src/app/app.routes.ts` | Angular route table (`bootstrap`, `login`, shell `/` with child routes `challenges`/`scoreboard`/`blog`/`admin`, wildcard `**`). | -| `frontend/src/app/core/services/auth.service.ts` | Signal store for access token + current user; persists to `sessionStorage`, exposes `restoreSession()`, `waitUntilHydrated()`, `login()`, `register()`, `logout()`, `me()`, `changePassword()` (all with `ensureCsrf()` + discriminated `LoginResult`/`RegisterResult`/`ChangePasswordResult`), `setSession()`. | -| `frontend/src/app/core/services/auth.session-storage.ts` | Pure `sessionStorage` helpers (`readStoredSession`, `writeStoredSession`, `clearStoredSession`) under key `hipctf.auth.v1`. | -| `frontend/src/app/core/services/bootstrap.service.ts` | Fetches `/api/v1/bootstrap`, applies theme tokens to CSS; exposes a `passwordPolicy` computed signal derived from the bootstrap payload. | -| `frontend/src/app/core/services/event-status.store.ts` | Signal store for the live event window (`state`, `serverNowUtc`, `eventStartUtc`, `eventEndUtc`, derived `countdownText`); `start(createSource)`/`stop()` manage the SSE connection + 1-second tick. | -| `frontend/src/app/core/services/event-status.pure.ts` | Pure helpers used by the store and tests: `deriveCountdownText`, `formatDdHhMm`, `EventSourceLike`, `EventState`/`EventStatePayload`. | -| `frontend/src/app/core/services/user.store.ts` | Signal store for the authenticated user (`user`, `loading`, `error`, derived `rankText`); `hydrateFromAuth()` + `loadMe()` (calls `AuthService.me()`). | -| `frontend/src/app/core/services/admin.service.ts` | `GET /api/v1/admin/users` returning `AdminUser[]`. | -| `frontend/src/app/core/services/markdown.service.ts` | Thin Angular `MarkdownService.render(md): string` that forwards to `renderMarkdownToHtml`. The DOMPurify sanitization happens inside `renderMarkdownToHtml` so Angular's `[innerHTML]` binding receives a sanitized plain string — no `DomSanitizer.bypassSecurityTrustHtml` wrapper is required. | -| `frontend/src/app/core/services/markdown.pure.ts` | Pure `renderMarkdownToHtml` helper using `marked` + `DOMPurify` (kept in its own file so it can be unit-tested without Angular's ESM-only runtime and so the sanitization boundary is shared between production and tests). | -| `frontend/src/app/core/guards/auth.guard.ts` | Async `CanActivateFn`; awaits bootstrap + auth hydration, delegates to `decideAuthRedirect`. | -| `frontend/src/app/core/guards/auth.guard.decision.ts` | Pure decision function for the auth guard (returns `true` or a redirect `UrlTree`). | -| `frontend/src/app/core/guards/admin.guard.ts` | Async `CanActivateFn` for admin-only routes; awaits bootstrap + auth hydration, delegates to `decideAdminGuard`. | -| `frontend/src/app/core/guards/admin.guard.decision.ts` | Pure decision function for the admin guard (returns `allow` / `redirect`). | -| `frontend/src/app/core/guards/landing.guard.ts` | Async `CanActivateFn` for `/login`; awaits bootstrap + auth hydration, then delegates to the pure `decideLandingGuard`. | -| `frontend/src/app/core/guards/landing.guard.decision.ts` | Pure decision: returns `true` to render the landing page, or `UrlTree` to `/bootstrap` (not initialized) or `/` (already authenticated). | -| `frontend/src/app/core/interceptors/auth.interceptor.ts` | Attaches Bearer header. | -| `frontend/src/app/core/interceptors/csrf.interceptor.ts` | Attaches `X-CSRF-Token` on unsafe methods. | -| `frontend/src/app/features/landing/landing.component.{ts,html,css}` | Public landing page (logo, page title, rendered `welcomeMarkdown`, login button, blog list) + modal containing the login and registration forms (toggled by the same `mode` signal). The template binds the welcome card via `[innerHTML]="welcomeHtml()"` (the computed signal **invoked**); binding the raw `welcomeHtml` signal object would render the runtime helper source — see `tests/frontend/landing-welcome.spec.ts`. | -| `frontend/src/app/features/landing/landing.service.ts` | Fetches `GET /api/v1/blog/posts`, exposes `posts`/`loading`/`error` signals, `refresh()`. | -| `frontend/src/app/features/landing/login-modal.service.ts` | Pure `buildLoginFailureMessage` helper that maps `{code,message}` into user-facing copy (handles `INVALID_CREDENTIALS`, `RATE_LIMITED`, `CSRF_INVALID`, `USERNAME_TAKEN`, `WEAK_PASSWORD`, `REGISTRATIONS_DISABLED`, `VALIDATION_FAILED`). | -| `frontend/src/app/features/admin/admin-users.component.ts` | Admin-only user list (signals: `loading`, `error`, `users`). | -| `frontend/src/app/features/setup/setup-create-admin.component.ts` | First-admin modal overlay (non-dismissible; typed reactive form). | -| `frontend/src/app/features/setup/setup-create-admin.service.ts` | POSTs to `/api/v1/setup/create-admin` and tags the result with the error code. | -| `frontend/src/app/features/setup/setup-create-admin.validators.ts` | Standalone `passwordMatchValidator` group-level reactive form validator. | -| `frontend/src/app/features/setup/setup-create-admin.field-errors.ts` | Pure `usernameError` / `passwordError` / `confirmError` helpers that turn reactive-form state into the inline validation messages rendered next to each input. | -| `frontend/src/app/features/home/home.component.ts` | Smart authenticated shell: renders `ShellHeaderComponent`, `QuickTabsComponent`, `ChangePasswordModalComponent`, and ``. Owns the SSE lifecycle (`EventStatusStore.start`/`stop`), the `UserStore.loadMe()` call, and the open/close signals for the change-password modal. | -| `frontend/src/app/features/home/home.shell.ts` | Pure `shouldShowAdminNav({isAuthenticated, role})` and `deriveActiveSectionFromUrl(url)` predicates (unit-tested). | -| `frontend/src/app/features/shell/header/shell-header.component.ts` | Dumb header: page title, active-section label, LED (`led-{state}` class), countdown text, user menu trigger, inline user menu dropdown with rank / change-password / admin / logout entries. | -| `frontend/src/app/features/shell/tabs/quick-tabs.component.ts` | Dumb `Challenges / Scoreboard / Blog` tab strip; `tabs`/`active` inputs, `tabChange` output. | -| `frontend/src/app/features/shell/change-password/change-password-modal.component.ts` | Dumb reactive-form modal (mode `'self'|'admin-reset'`, `passwordMatchValidator`, policy hint, `errorMessage`/`submitting` inputs). Emits `submit({old,new,confirm})` and `cancel()`. | -| `frontend/src/app/features/shell/change-password/change-password-modal.validators.ts` | `passwordMatchValidator` group validator + `PasswordPolicy` zod schema. | -| `frontend/src/app/features/shell/change-password/password-feedback.ts` | Pure `formatChangePasswordError({code, message})` helper that maps the API code to user-facing copy. | -| `frontend/src/app/features/challenges/challenges.page.ts` | Placeholder `/challenges` page. | -| `frontend/src/app/features/scoreboard/scoreboard.page.ts` | Placeholder `/scoreboard` page. | -| `frontend/src/app/features/blog/blog.page.ts` | Placeholder `/blog` page. | - -# Tests - -| File | Responsibility | -|------------------------------------------------|--------------------------------------------------------------------------------| -| `tests/backend/*.spec.ts` (~25 suites) | Jest tests for backend modules, CSRF, OpenAPI 3.1, migrations, rate limits, the new `/me` + `/change-password` + authenticated SSE flows, and `UsersRankService`. | -| `tests/frontend/theme.spec.ts` | Jest test for theme token application. | -| `tests/frontend/admin-shell.spec.ts` | Unit tests for `shouldShowAdminNav` predicate. | -| `tests/frontend/admin-navigation.spec.ts` | Tests for admin guard decision + nav-link rendering. | -| `tests/frontend/auth-restore.spec.ts` | Round-trips `auth.session-storage` helpers + refresh success/failure paths. | -| `tests/frontend/setup-create-admin.spec.ts` | Pure-Jest tests for `passwordMatchValidator` and the three field-error helpers (`usernameError`, `passwordError`, `confirmError`). | -| `tests/frontend/guard-bootstrap-race.spec.ts` | Pins `decideAuthRedirect` so a refresh on a deep link never flashes `/bootstrap` or `/login` while auth is hydrating. | -| `tests/frontend/landing-guard.spec.ts` | Pins `decideLandingGuard` for the not-initialized / authenticated / anonymous branches. | -| `tests/frontend/landing-markdown.spec.ts` | Pins `renderMarkdownToHtml` (sanitization of raw `