AI Implementation feature(857): Authenticated Shell: Header, Quick Tabs and Change Password (#14)

This commit was merged in pull request #14.
This commit is contained in:
2026-07-21 22:26:47 +00:00
parent 8dc8cee769
commit b6dbfcc511
46 changed files with 2705 additions and 211 deletions
+5 -5
View File
@@ -3,7 +3,7 @@ type: architecture
title: Backend Module Map
description: NestJS modules, controllers, services, and how they are wired together.
tags: [architecture, backend, nestjs, modules]
timestamp: 2026-07-21T14:43:00Z
timestamp: 2026-07-21T22:19:08Z
---
# Module Map
@@ -23,8 +23,8 @@ also registers two global providers:
| `DatabaseModule` | `backend/src/database/database.module.ts` | Configures TypeORM with `better-sqlite3`, registers entities, runs migrations, enforces WAL. |
| `CommonModule` | `backend/src/common/common.module.ts` | Provides shared services (CSRF middleware class, backoff, registration rate limit, SSE hub, theme loader, etc.). |
| `SettingsModule` | `backend/src/modules/settings/settings.module.ts` | Exposes `SettingsService` (get/set/getAll over `setting` table). |
| `AuthModule` | `backend/src/modules/auth/auth.module.ts` | `AuthController` (`/api/v1/auth/*`) + `AuthService` (login/refresh/logout/register-first-admin + public `register`). Imports `SettingsModule` so the public-register flow can read the `registrationsEnabled` flag. |
| `UsersModule` | `backend/src/modules/users/users.module.ts` | `UsersController` (`/api/v1/auth/register-first-admin`) + `UsersService` (last-admin invariant). |
| `AuthModule` | `backend/src/modules/auth/auth.module.ts` | `AuthController` (`/api/v1/auth/{register,login,refresh,logout,me,change-password,csrf}`) + `AuthService` (login/refresh/logout/register-first-admin + public `register` + `getMe` + `changePassword`). Imports `SettingsModule` so the public-register flow can read the `registrationsEnabled` flag, and `UsersModule` (forwardRef) for `UsersRankService`. |
| `UsersModule` | `backend/src/modules/users/users.module.ts` | `UsersController` (`/api/v1/auth/register-first-admin`) + `UsersService` (last-admin invariant) + `UsersRankService` (`rankOfUser(manager, userId) -> {rank, points}` over the `solve` table). |
| `SetupModule` | `backend/src/modules/setup/setup.module.ts` | `SetupController` (`POST /api/v1/setup/create-admin`) + `SetupService`. Returns 409 `SYSTEM_INITIALIZED` once any admin exists; serializes concurrent first-admin attempts via an in-process promise chain. |
| `SystemModule` | `backend/src/modules/system/system.module.ts` | `SystemController` (`/api/v1/bootstrap`, `/event/status`, SSE streams) + `SystemService`. |
| `AdminModule` | `backend/src/modules/admin/admin.module.ts` | `AdminController` (`/api/v1/admin/users*`) + `AdminService`. Gated by `AdminGuard` + `@Roles('admin')`. |
@@ -40,7 +40,7 @@ also registers two global providers:
| `UsersController` | `/api/v1/auth/register-first-admin` | Public (bootstrap-only) | `backend/src/modules/users/users.controller.ts` |
| `SetupController` | `/api/v1/setup/create-admin` | Public (bootstrap-only; 409 `SYSTEM_INITIALIZED` once an admin exists) | `backend/src/modules/setup/setup.controller.ts` |
| `AdminController` | `/api/v1/admin` | Admin only | `backend/src/modules/admin/admin.controller.ts` |
| `SystemController` | `/api/v1` | Public | `backend/src/modules/system/system.controller.ts` |
| `SystemController` | `/api/v1` | Mixed (bootstrap/event/scoreboard/settings are public; `events/status` SSE is JWT-protected) | `backend/src/modules/system/system.controller.ts` |
| `UploadsController` | `/api/v1/uploads` | Admin only | `backend/src/modules/uploads/uploads.controller.ts` |
| `BlogController` | `/api/v1/blog` | Public | `backend/src/modules/blog/blog.controller.ts` |
@@ -53,7 +53,7 @@ also registers two global providers:
| `AdminGuard` | `backend/src/common/guards/admin.guard.ts` | Requires authenticated user with `role === 'admin'`. |
| `RolesGuard` | `backend/src/common/guards/roles.guard.ts` | Enforces `@Roles(...)` metadata. |
| `GlobalExceptionFilter` | `backend/src/common/filters/global-exception.filter.ts` | Converts exceptions to standard envelope. |
| `EventStatusService` | `backend/src/common/services/event-status.service.ts` | Computes `Stopped`/`Running` and `countdownMs`. |
| `EventStatusService` | `backend/src/common/services/event-status.service.ts` | Computes the 4-state event state machine (`countdown`/`running`/`stopped`/`unconfigured`) via `getState()` plus the legacy `getStatus()` shape (`Stopped`/`Running` + `countdownMs`). |
| `LoginBackoffService` | `backend/src/common/services/login-backoff.service.ts` | Per-IP+username brute-force throttle. |
| `RegistrationRateLimitService` | `backend/src/common/services/registration-rate-limit.service.ts` | Per-IP rate limit on first-admin registration. |
| `SseHubService` | `backend/src/common/services/sse-hub.service.ts` | In-process pub/sub for SSE streams. |
+72 -28
View File
@@ -2,21 +2,24 @@
type: architecture
title: Frontend Structure
description: Angular routes, components, services, guards, and interceptors.
tags: [architecture, frontend, angular]
timestamp: 2026-07-21T18:28:00Z
tags: [architecture, frontend, angular, shell, sse]
timestamp: 2026-07-21T22:19:08Z
---
# 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 a `<router-outlet>` for child routes. |
| `/admin` | `AdminUsersComponent` | `adminGuard` | Child of `/`; requires `role === 'admin'`. |
| `**` | Redirect to `/` | — | Wildcard fallback. |
| 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 `<router-outlet>` 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. |
# Components
@@ -26,23 +29,34 @@ All components are standalone (no NgModules). Each component imports
| Component | Path | Purpose |
|------------------------|-----------------------------------------------------------------|-------------------------------------------------|
| `AppComponent` | `frontend/src/app/app.component.ts` | Root; renders `<router-outlet>`, calls `BootstrapService.load()` then `AuthService.restoreSession()`. |
| `HomeComponent` | `frontend/src/app/features/home/home.component.ts` | Authenticated shell with header, conditional admin nav, and `<router-outlet>` for children. |
| `HomeComponent` | `frontend/src/app/features/home/home.component.ts` | Smart authenticated shell. Hosts the `ShellHeaderComponent`, `QuickTabsComponent`, the `ChangePasswordModalComponent`, and the `<router-outlet>` 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. |
| `LoginComponent` | *(removed)* — replaced by `LandingComponent`. | |
| `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. |
| `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). |
# Services
| 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()` (each calls `ensureCsrf()` then posts, returning a discriminated `LoginResult` / `RegisterResult`). |
| `BootstrapService` | `frontend/src/app/core/services/bootstrap.service.ts` | Fetches `/api/v1/bootstrap`, applies theme tokens to CSS; exposes `ready()` that deduplicates the in-flight load. |
| `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: <r> - Points: <p>"`). |
| `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. |
# Guards and interceptors
@@ -70,8 +84,9 @@ Both interceptors are wired in `frontend/src/main.ts` via
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`, and the
default challenge IP.
`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`.
@@ -86,23 +101,52 @@ Both interceptors are wired in `frontend/src/main.ts` via
/ `isAuthenticated()`, which prevents a flash of `/bootstrap` or
`/login` on a hard refresh of a deep link.
# Home shell flow
# Authenticated shell flow
After successful auth, `HomeComponent` renders a thin shell layout:
After successful auth, `HomeComponent` renders a full shell layout:
1. The header (`shell-header`) shows the page title and signed-in
identity.
2. When `shouldShowAdminNav({isAuthenticated, role})` returns `true`
(see `frontend/src/app/features/home/home.shell.ts`) the admin nav
link (`data-testid="nav-admin"`) is rendered.
3. Child routes (e.g. `/admin``AdminUsersComponent`) render into the
shell's `<router-outlet>`.
4. `AdminUsersComponent` calls `AdminService.listUsers()` on init;
loading, error, and the rendered list are exposed as Angular signals.
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: <r> - Points: <p>`,
`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 `<router-outlet>` 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`.
# 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)
+39 -19
View File
@@ -3,7 +3,7 @@ type: architecture
title: Key Files Index
description: One-line responsibility for every important source file in the repository.
tags: [architecture, index, key-files]
timestamp: 2026-07-21T18:45:54Z
timestamp: 2026-07-21T22:19:08Z
---
# Backend
@@ -25,12 +25,12 @@ timestamp: 2026-07-21T18:45:54Z
| `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` key/value table. |
| `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)`. |
| `backend/src/database/entities/refresh-token.entity.ts` | `refresh_token` table (token_hash + revocation). |
| `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
@@ -48,7 +48,7 @@ timestamp: 2026-07-21T18:45:54Z
| `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` | Computes event `Stopped`/`Running` and `countdownMs`. |
| `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. |
@@ -58,19 +58,20 @@ timestamp: 2026-07-21T18:45:54Z
| `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. |
| `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. |
| `backend/src/modules/auth/auth.controller.ts` | `/api/v1/auth/{register,login,refresh,logout,csrf}`. |
| `backend/src/modules/auth/auth.service.ts` | Login, refresh, logout, register-first-admin, public player self-registration (`registerPlayer`), session minting. |
| `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` (with `password === passwordConfirm` refine + username regex). |
| `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`. |
| `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. |
@@ -83,8 +84,8 @@ timestamp: 2026-07-21T18:45:54Z
| `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}` + SSE `/event/stream`, `/scoreboard/stream`. |
| `backend/src/modules/system/system.service.ts` | Builds the public bootstrap payload from settings + theme + admin count. |
| `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`). |
@@ -111,10 +112,13 @@ timestamp: 2026-07-21T18:45:54Z
| `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 (declares `/` shell with `adminGuard`-gated `/admin` child). |
| `frontend/src/app/core/services/auth.service.ts` | Signal store for access token + current user; persists to `sessionStorage`, exposes `restoreSession()`, `waitUntilHydrated()`, `login()`, `register()` (both with `ensureCsrf()` + discriminated `LoginResult`/`RegisterResult`), `setSession()`. |
| `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; `load()` / `ready()` deduplicate the in-flight fetch. |
| `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). |
@@ -134,14 +138,22 @@ timestamp: 2026-07-21T18:45:54Z
| `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` | Authenticated shell: header + conditional admin nav + `<router-outlet>`. |
| `frontend/src/app/features/home/home.shell.ts` | Pure `shouldShowAdminNav({isAuthenticated, role})` predicate (unit-tested). |
| `frontend/src/app/features/home/home.component.ts` | Smart authenticated shell: renders `ShellHeaderComponent`, `QuickTabsComponent`, `ChangePasswordModalComponent`, and `<router-outlet>`. 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` (18 suites) | Jest tests for backend modules, CSRF, OpenAPI 3.1, migrations, rate limits. |
| `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. |
@@ -152,6 +164,14 @@ timestamp: 2026-07-21T18:45:54Z
| `tests/frontend/landing-markdown.spec.ts` | Pins `renderMarkdownToHtml` (sanitization of raw `<script>` payloads, script URL schemes, etc.). |
| `tests/frontend/landing-welcome.spec.ts` | Regression test for the landing welcome binding — asserts the bootstrap payload's `welcomeMarkdown` produces the expected sanitized HTML (h1 + paragraph) and never the Angular runtime helper source string, including for null/empty/hostile payloads. |
| `tests/frontend/landing-modal.spec.ts` | Pins `buildLoginFailureMessage` (`INVALID_CREDENTIALS`, `RATE_LIMITED` with retry-seconds extraction, `CSRF_INVALID`, `USERNAME_TAKEN`, `WEAK_PASSWORD`, `REGISTRATIONS_DISABLED`, `VALIDATION_FAILED`, default fallback). |
| `tests/frontend/event-status.store.spec.ts` | Pins `EventStatusStore.applyServerStatus` (state + delta anchor) and `deriveCountdownText` (`countdown` / `running` / `stopped` / `unconfigured`). |
| `tests/frontend/change-password-modal.spec.ts` | Pins the dumb modal's reactive form (validation, submit/cancel emission, error rendering). |
| `tests/frontend/shell-active-section.spec.ts` | Pins `deriveActiveSectionFromUrl` for the four URL branches. |
| `tests/backend/change-password.spec.ts` | Integration coverage for `/api/v1/auth/change-password` (wrong-old / mismatched / weak / unauth branches). |
| `tests/backend/event-status-state.spec.ts` | Pins the 4-state derivation in `EventStatusService.getState()`. |
| `tests/backend/events-status-sse.spec.ts` | Pins the authenticated SSE stream (401 + initial `EventStatePayload`). |
| `tests/backend/me-endpoint.spec.ts` | Pins `/api/v1/auth/me` (401 + payload shape including rank/points). |
| `tests/backend/rank-points.spec.ts` | Pure unit test for `UsersRankService.rankOfUser()`. |
| `tests/backend/auth-register.spec.ts` | Backend integration tests for `POST /api/v1/auth/register` (happy path, disabled flag, duplicate username, weak password, rate limit). |
| `tests/backend/blog-public.spec.ts` | Backend integration tests for `GET /api/v1/blog/posts` (only published rows, descending order). |
| `tests/backend/csrf-protected-routes.spec.ts` | Pins CSRF enforcement on the formerly-skipped `/api/v1/auth/login` route. |