docs: update documentation to OKF v0.1 format

This commit is contained in:
OpenVelo Agent
2026-07-21 15:07:41 +00:00
parent 9f67ec8c50
commit a3ea62640d
6 changed files with 144 additions and 13 deletions
+23 -3
View File
@@ -3,7 +3,7 @@ type: architecture
title: Frontend Structure title: Frontend Structure
description: Angular routes, components, services, guards, and interceptors. description: Angular routes, components, services, guards, and interceptors.
tags: [architecture, frontend, angular] tags: [architecture, frontend, angular]
timestamp: 2026-07-21T14:43:00Z timestamp: 2026-07-21T15:05:00Z
--- ---
# Routes # Routes
@@ -14,7 +14,8 @@ Routes live in `frontend/src/app/app.routes.ts`:
|--------------|----------------------------------|---------------|----------------------------------------| |--------------|----------------------------------|---------------|----------------------------------------|
| `/bootstrap` | `SetupCreateAdminComponent` | — | Rendered only when `initialized === false`. Non-dismissible modal overlay. | | `/bootstrap` | `SetupCreateAdminComponent` | — | Rendered only when `initialized === false`. Non-dismissible modal overlay. |
| `/login` | `LoginComponent` | — | Standard login form. | | `/login` | `LoginComponent` | — | Standard login form. |
| `/` | `HomeComponent` | `authGuard` | Requires auth + initialized. | | `/` | `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. | | `**` | Redirect to `/` | — | Wildcard fallback. |
# Components # Components
@@ -25,7 +26,8 @@ All components are standalone (no NgModules). Each component imports
| Component | Path | Purpose | | Component | Path | Purpose |
|------------------------|-----------------------------------------------------------------|-------------------------------------------------| |------------------------|-----------------------------------------------------------------|-------------------------------------------------|
| `AppComponent` | `frontend/src/app/app.component.ts` | Root; renders `<router-outlet>` and calls `BootstrapService.load()`. | | `AppComponent` | `frontend/src/app/app.component.ts` | Root; renders `<router-outlet>` and calls `BootstrapService.load()`. |
| `HomeComponent` | `frontend/src/app/features/home/home.component.ts` | Landing page shown after auth. | | `HomeComponent` | `frontend/src/app/features/home/home.component.ts` | Authenticated shell with header, conditional admin nav, and `<router-outlet>` for children. |
| `AdminUsersComponent` | `frontend/src/app/features/admin/admin-users.component.ts` | Admin-only user list rendered inside the home shell. |
| `LoginComponent` | `frontend/src/app/features/auth/login.component.ts` | Username/password form. | | `LoginComponent` | `frontend/src/app/features/auth/login.component.ts` | Username/password form. |
| `SetupCreateAdminComponent` | `frontend/src/app/features/setup/setup-create-admin.component.ts` | First-admin bootstrap modal (non-dismissible, typed reactive forms). | | `SetupCreateAdminComponent` | `frontend/src/app/features/setup/setup-create-admin.component.ts` | First-admin bootstrap modal (non-dismissible, typed reactive forms). |
@@ -35,12 +37,15 @@ All components are standalone (no NgModules). Each component imports
|---------------------|---------------------------------------------------------------|-----------------------------------------------------------| |---------------------|---------------------------------------------------------------|-----------------------------------------------------------|
| `AuthService` | `frontend/src/app/core/services/auth.service.ts` | Signal-backed access token + current user. | | `AuthService` | `frontend/src/app/core/services/auth.service.ts` | Signal-backed access token + current user. |
| `BootstrapService` | `frontend/src/app/core/services/bootstrap.service.ts` | Fetches `/api/v1/bootstrap`, applies theme tokens to CSS. | | `BootstrapService` | `frontend/src/app/core/services/bootstrap.service.ts` | Fetches `/api/v1/bootstrap`, applies theme tokens to CSS. |
| `AdminService` | `frontend/src/app/core/services/admin.service.ts` | `GET /api/v1/admin/users` (typed `AdminUser[]`). |
# Guards and interceptors # Guards and interceptors
| Symbol | Path | Purpose | | Symbol | Path | Purpose |
|---------------------|---------------------------------------------------------------|-----------------------------------------------------------| |---------------------|---------------------------------------------------------------|-----------------------------------------------------------|
| `authGuard` | `frontend/src/app/core/guards/auth.guard.ts` | Redirects to `/bootstrap` when uninitialized, `/login` when not authenticated. | | `authGuard` | `frontend/src/app/core/guards/auth.guard.ts` | Redirects to `/bootstrap` when uninitialized, `/login` when not authenticated. |
| `adminGuard` | `frontend/src/app/core/guards/admin.guard.ts` | 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}`. |
| `authInterceptor` | `frontend/src/app/core/interceptors/auth.interceptor.ts` | Attaches `Authorization: Bearer <accessToken>` header. | | `authInterceptor` | `frontend/src/app/core/interceptors/auth.interceptor.ts` | Attaches `Authorization: Bearer <accessToken>` header. |
| `csrfInterceptor` | `frontend/src/app/core/interceptors/csrf.interceptor.ts` | Attaches `X-CSRF-Token` header on POST/PUT/PATCH/DELETE. | | `csrfInterceptor` | `frontend/src/app/core/interceptors/csrf.interceptor.ts` | Attaches `X-CSRF-Token` header on POST/PUT/PATCH/DELETE. |
@@ -62,8 +67,23 @@ Both interceptors are wired in `frontend/src/main.ts` via
5. The `authGuard` reads `BootstrapService.initialized()` and either 5. The `authGuard` reads `BootstrapService.initialized()` and either
allows navigation or redirects to `/bootstrap`. allows navigation or redirects to `/bootstrap`.
# Home shell flow
After successful auth, `HomeComponent` renders a thin 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.
# See also # See also
- [System Overview](/architecture/overview.md) - [System Overview](/architecture/overview.md)
- [Backend Module Map](/architecture/backend-modules.md) - [Backend Module Map](/architecture/backend-modules.md)
- [First-Run Bootstrap](/guides/bootstrap.md) - [First-Run Bootstrap](/guides/bootstrap.md)
- [Admin Shell & User Management](/guides/admin-shell.md)
+10 -3
View File
@@ -3,7 +3,7 @@ type: architecture
title: Key Files Index title: Key Files Index
description: One-line responsibility for every important source file in the repository. description: One-line responsibility for every important source file in the repository.
tags: [architecture, index, key-files] tags: [architecture, index, key-files]
timestamp: 2026-07-21T14:43:00Z timestamp: 2026-07-21T15:05:00Z
--- ---
# Backend # Backend
@@ -107,17 +107,22 @@ timestamp: 2026-07-21T14:43:00Z
| `frontend/src/index.html` | Root HTML shell. | | `frontend/src/index.html` | Root HTML shell. |
| `frontend/src/styles.css` | Global styles consuming theme CSS custom properties. | | `frontend/src/styles.css` | Global styles consuming theme CSS custom properties. |
| `frontend/src/app/app.component.ts` | Root component; triggers bootstrap on init. | | `frontend/src/app/app.component.ts` | Root component; triggers bootstrap on init. |
| `frontend/src/app/app.routes.ts` | Angular route table. | | `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. | | `frontend/src/app/core/services/auth.service.ts` | Signal store for access token + current user. |
| `frontend/src/app/core/services/bootstrap.service.ts` | Fetches `/api/v1/bootstrap` and applies theme tokens to CSS. | | `frontend/src/app/core/services/bootstrap.service.ts` | Fetches `/api/v1/bootstrap` and applies theme tokens to CSS. |
| `frontend/src/app/core/services/admin.service.ts` | `GET /api/v1/admin/users` returning `AdminUser[]`. |
| `frontend/src/app/core/guards/auth.guard.ts` | `CanActivateFn` checking init + auth. | | `frontend/src/app/core/guards/auth.guard.ts` | `CanActivateFn` checking init + auth. |
| `frontend/src/app/core/guards/admin.guard.ts` | `CanActivateFn` for admin-only routes; 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/interceptors/auth.interceptor.ts` | Attaches Bearer header. | | `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/core/interceptors/csrf.interceptor.ts` | Attaches `X-CSRF-Token` on unsafe methods. |
| `frontend/src/app/features/auth/login.component.ts` | Login form. | | `frontend/src/app/features/auth/login.component.ts` | Login form. |
| `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.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.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.validators.ts` | Standalone `passwordMatchValidator` group-level reactive form validator. |
| `frontend/src/app/features/home/home.component.ts` | Landing page after auth. | | `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). |
# Tests # Tests
@@ -125,6 +130,8 @@ timestamp: 2026-07-21T14:43:00Z
|------------------------------------------------|--------------------------------------------------------------------------------| |------------------------------------------------|--------------------------------------------------------------------------------|
| `tests/backend/*.spec.ts` (18 suites) | Jest tests for backend modules, CSRF, OpenAPI 3.1, migrations, rate limits. | | `tests/backend/*.spec.ts` (18 suites) | Jest tests for backend modules, CSRF, OpenAPI 3.1, migrations, rate limits. |
| `tests/frontend/theme.spec.ts` | Jest test for theme token application. | | `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/backend/csrf-client.ts` | Test helper for CSRF flow. | | `tests/backend/csrf-client.ts` | Test helper for CSRF flow. |
| `tests/backend/db-helper.ts` | Test helper for spinning up an isolated DB. | | `tests/backend/db-helper.ts` | Test helper for spinning up an isolated DB. |
+2 -2
View File
@@ -3,7 +3,7 @@ type: architecture
title: System Overview title: System Overview
description: High-level layout of the HIPCTF monorepo (NestJS API + Angular SPA) and how they communicate. description: High-level layout of the HIPCTF monorepo (NestJS API + Angular SPA) and how they communicate.
tags: [architecture, backend, frontend, overview] tags: [architecture, backend, frontend, overview]
timestamp: 2026-07-21T14:18:00Z timestamp: 2026-07-21T15:05:00Z
--- ---
# Overview # Overview
@@ -73,7 +73,7 @@ Browser ──HTTPS──▶ NestJS process (PORT, default 3000)
| Concern | Backend | Frontend | | Concern | Backend | Frontend |
|----------------|------------------------------------------------------|---------------------------------------------| |----------------|------------------------------------------------------|---------------------------------------------|
| Authentication | `JwtAuthGuard` (global) + `AuthGuard('jwt')` strategy | `AuthService` signal + `authInterceptor` | | Authentication | `JwtAuthGuard` (global) + `AuthGuard('jwt')` strategy | `AuthService` signal + `authInterceptor` |
| Authorization | `AdminGuard` + `@Roles('admin')` decorator | `authGuard` route guard | | Authorization | `AdminGuard` + `@Roles('admin')` decorator | `authGuard` for `/`, `adminGuard` for `/admin` (delegates to pure `decideAdminGuard`) |
| CSRF | `CsrfMiddleware` (skips `/api/v1/auth/login` and `/api/v1/auth/register-first-admin`) | `csrfInterceptor` reads `csrf` cookie | | CSRF | `CsrfMiddleware` (skips `/api/v1/auth/login` and `/api/v1/auth/register-first-admin`) | `csrfInterceptor` reads `csrf` cookie |
| Errors | `ApiError``GlobalExceptionFilter` returns `{code, message, details, path, timestamp}` | Components display `error?.error?.message` | | Errors | `ApiError``GlobalExceptionFilter` returns `{code, message, details, path, timestamp}` | Components display `error?.error?.message` |
| Config | `envSchema` (zod) + `validateEnv` | None (consumed via bootstrap) | | Config | `envSchema` (zod) + `validateEnv` | None (consumed via bootstrap) |
+100
View File
@@ -0,0 +1,100 @@
---
type: guide
title: Admin Shell & User Management
description: How an authenticated admin navigates the post-login shell and reaches the admin user-management area.
tags: [guide, admin, shell, navigation, tester]
timestamp: 2026-07-21T15:05:00Z
---
# When this view is available
The admin shell is the post-login UI for users with `role === 'admin'`.
It is gated by:
| Layer | File | Check |
|------------------|--------------------------------------------------------------------------------------------|------------------------------------|
| Client route | `frontend/src/app/app.routes.ts` | `/admin` uses `adminGuard`. |
| Client nav link | `frontend/src/app/features/home/home.component.ts` | Renders only when `showAdminNav()` is true. |
| Client predicate | `frontend/src/app/features/home/home.shell.ts` (`shouldShowAdminNav`) | `isAuthenticated && role === 'admin'`. |
| Server route | `backend/src/modules/admin/admin.controller.ts` (mounted under `@UseGuards(AdminGuard)` + `@Roles('admin')`) | Requires admin JWT. |
A non-admin (or unauthenticated) request to `/admin` is intercepted by
the client guard *before* any HTTP call is made, so the page never
renders.
# How to access (tester steps)
1. Ensure the instance is initialized (`/api/v1/bootstrap` returns
`initialized: true`). If not, follow [First-Run Bootstrap](/guides/bootstrap.md).
2. Sign in as a user with `role === 'admin'` via `/login`.
3. The browser lands on `/` and renders the **Home shell** with a
header containing the page title and a sign-in status line.
4. Click the **Admin** nav link (`data-testid="nav-admin"`) in the
shell header, or navigate directly to `/admin`.
5. The admin area renders inside the shell:
- **Heading:** "Admin area"
- **Loading state:** `Loading users...` (`data-testid="admin-loading"`)
while the GET is in flight.
- **User list:** an unordered list (`data-testid="admin-user-list"`)
showing one `<li>` per user with the username in bold and the role
in parentheses, e.g. `root (admin)`.
- **Error state:** a red message (`data-testid="admin-error"`) when
the request fails (e.g. backend down or auth expired).
# Expected behavior
| User role | Visiting `/admin` |
|----------------------|---------------------------------------------------------|
| Unauthenticated | Redirected to `/login` by `adminGuard`. |
| Authenticated player | Redirected to `/` by `adminGuard`. |
| Authenticated admin | Page renders, fetches `GET /api/v1/admin/users`, lists users. |
* When `BootstrapService.initialized()` is still `false` the guard
redirects to `/bootstrap` instead of `/login`, so the first admin
flow is honored.
* The Admin nav link is hidden for non-admin users — there is no
visible UI affordance to reach `/admin` without the role.
* The list endpoint uses the existing `authInterceptor` + `csrfInterceptor`,
so no extra plumbing is required.
# Visual elements
| Element | Selector | Purpose |
|----------------------|-----------------------------------|----------------------------------------------|
| Shell header | `.shell-header` | Page title + signed-in identity. |
| Admin nav link | `[data-testid="nav-admin"]` | RouterLink to `/admin`; shown to admins only. |
| Shell body | `.shell-body` | Holds `<router-outlet>` for child routes. |
| Admin heading | `h2` inside `.admin-area` | "Admin area". |
| Loading message | `[data-testid="admin-loading"]` | "Loading users...". |
| Error message | `[data-testid="admin-error"]` | Red text; renders `error().error.message` or fallback. |
| User list | `[data-testid="admin-user-list"]` | Renders one `<li>` per `AdminUser`. |
# Architecture map
| Step | Where | What happens |
|------|----------------------------------------------------|-----------------------------------------------------------------------|
| 1 | `frontend/src/app/app.routes.ts` | `/` is a parent route with `authGuard`; `/admin` child uses `adminGuard` + lazy-loads `AdminUsersComponent`. |
| 2 | `frontend/src/app/core/guards/admin.guard.ts` | `adminGuard` calls `decideAdminGuard(...)` with bootstrap + auth state. |
| 3 | `frontend/src/app/core/guards/admin.guard.decision.ts` | Pure function returning `{kind: 'allow'}` or `{kind: 'redirect', path}`. |
| 4 | `frontend/src/app/features/home/home.component.ts` | Shell template renders header, conditional nav (`*ngIf="showAdminNav()"`), and `<router-outlet>`. |
| 5 | `frontend/src/app/features/home/home.shell.ts` | `shouldShowAdminNav({isAuthenticated, role})` predicate (exported and unit-tested). |
| 6 | `frontend/src/app/features/admin/admin-users.component.ts` | On init, calls `AdminService.listUsers()` and populates signals (`loading`, `error`, `users`). |
| 7 | `frontend/src/app/core/services/admin.service.ts` | `GET /api/v1/admin/users` (credentials included); typed as `AdminUser[]`. |
| 8 | `backend/src/modules/admin/admin.controller.ts` | `AdminController.list` returns `AdminService.listUsers({limit, cursor, role})`. |
# Notes
* The shell deliberately keeps `HomeComponent` as a thin layout owner —
child routes (currently just `/admin`) render into its
`<router-outlet>`. New admin sub-pages can be added as additional
children without changing the shell.
* The guard decision function (`decideAdminGuard`) is a pure module so
it is straightforward to unit-test without Angular DI. See
`tests/frontend/admin-shell.spec.ts` and `tests/frontend/admin-navigation.spec.ts`.
# See also
- [First-Run Bootstrap](/guides/bootstrap.md)
- [Frontend Structure](/architecture/frontend-structure.md)
- [REST API Overview](/api/rest-overview.md)
- [Admin Endpoints](/api/admin.md)
+7 -5
View File
@@ -3,7 +3,7 @@ type: guide
title: First-Run Bootstrap title: First-Run Bootstrap
description: How a fresh HIPCTF instance is initialized by the very first administrator. description: How a fresh HIPCTF instance is initialized by the very first administrator.
tags: [guide, bootstrap, first-admin, onboarding, tester] tags: [guide, bootstrap, first-admin, onboarding, tester]
timestamp: 2026-07-21T14:43:00Z timestamp: 2026-07-21T15:05:00Z
--- ---
# When this flow runs # When this flow runs
@@ -46,7 +46,7 @@ bypassed.
* On success the modal disappears, the app stores the access token via * On success the modal disappears, the app stores the access token via
`AuthService.setSession`, calls `BootstrapService.markInitialized()`, `AuthService.setSession`, calls `BootstrapService.markInitialized()`,
and navigates to `/challenges`. and navigates to `/admin` (see [Admin Shell & User Management](/guides/admin-shell.md)).
* On `USERNAME_TAKEN` the form shows a red alert: * On `USERNAME_TAKEN` the form shows a red alert:
`"Username already exists"`. The **Retry** button is intentionally `"Username already exists"`. The **Retry** button is intentionally
hidden because the username must be changed first. hidden because the username must be changed first.
@@ -84,7 +84,7 @@ The modal **cannot** be dismissed by the user:
| 4 | `SetupCreateAdminService.ensureCsrf()` | Preflights `GET /api/v1/auth/csrf` so the response sets the `csrf` cookie. | | 4 | `SetupCreateAdminService.ensureCsrf()` | Preflights `GET /api/v1/auth/csrf` so the response sets the `csrf` cookie. |
| 5 | `POST /api/v1/setup/create-admin` | `SetupService.createAdmin` runs in a TypeORM transaction; creates the user with `role='admin'`; mints session via `AuthService.createSession`. | | 5 | `POST /api/v1/setup/create-admin` | `SetupService.createAdmin` runs in a TypeORM transaction; creates the user with `role='admin'`; mints session via `AuthService.createSession`. |
| 6 | `setRefreshCookie` | Sets the `rt` HttpOnly cookie. | | 6 | `setRefreshCookie` | Sets the `rt` HttpOnly cookie. |
| 7 | Component on success | `AuthService.setSession`, `BootstrapService.markInitialized()`, navigate to `/challenges`. | | 7 | Component on success | `AuthService.setSession`, `BootstrapService.markInitialized()`, navigate to `/admin`. |
| 8 | Subsequent `authGuard` checks | `initialized === true`; require auth instead. | | 8 | Subsequent `authGuard` checks | `initialized === true`; require auth instead. |
# What the operator sees after init # What the operator sees after init
@@ -94,8 +94,10 @@ Once the first admin exists, `/api/v1/bootstrap` returns
* Skips `/bootstrap` entirely. * Skips `/bootstrap` entirely.
* Routes unauthenticated users to `/login`. * Routes unauthenticated users to `/login`.
* Routes authenticated users to `/` (Home) which itself redirects to * Routes authenticated admins to `/` (the Home shell) and renders the
`/challenges` once admin features are wired. admin nav link. The first admin lands directly on the user list at
`/admin` (see [Admin Shell & User Management](/guides/admin-shell.md)).
* Routes authenticated players to `/` without the admin nav link.
# See also # See also
+2
View File
@@ -52,6 +52,8 @@ they need.
* [First-Run Bootstrap](/guides/bootstrap.md) - How a fresh instance is * [First-Run Bootstrap](/guides/bootstrap.md) - How a fresh instance is
initialized by the very first admin via the non-dismissible modal. initialized by the very first admin via the non-dismissible modal.
* [Admin Shell & User Management](/guides/admin-shell.md) - How admins
navigate the post-login shell and reach the user-management area.
* [Theming](/guides/theming.md) - How themes are loaded and applied. * [Theming](/guides/theming.md) - How themes are loaded and applied.
* [Event Window](/guides/event-window.md) - How the event window and live * [Event Window](/guides/event-window.md) - How the event window and live
countdown work. countdown work.