From 09856ccf8e169c7c6cda21eca2c68ae35c1dc7ad Mon Sep 17 00:00:00 2001 From: OpenVelo Agent Date: Tue, 21 Jul 2026 18:34:42 +0000 Subject: [PATCH] docs: update documentation to OKF v0.1 format --- docs/api/admin.md | 36 ++++++++ docs/api/auth.md | 90 ++++++++++++++++++ docs/api/rest-overview.md | 10 +- docs/api/system.md | 69 ++++++++++++++ docs/api/uploads.md | 33 +++++++ docs/architecture/backend-modules.md | 4 +- docs/architecture/frontend-structure.md | 14 ++- docs/architecture/key-files.md | 30 ++++-- docs/database/blog-posts.md | 38 ++++++++ docs/guides/event-window.md | 45 +++++++++ docs/guides/landing-page.md | 118 ++++++++++++++++++++++++ docs/guides/scoreboard-stream.md | 37 ++++++++ docs/guides/theming.md | 50 ++++++++++ docs/index.md | 5 +- 14 files changed, 563 insertions(+), 16 deletions(-) create mode 100644 docs/api/admin.md create mode 100644 docs/api/auth.md create mode 100644 docs/api/system.md create mode 100644 docs/api/uploads.md create mode 100644 docs/database/blog-posts.md create mode 100644 docs/guides/event-window.md create mode 100644 docs/guides/landing-page.md create mode 100644 docs/guides/scoreboard-stream.md create mode 100644 docs/guides/theming.md diff --git a/docs/api/admin.md b/docs/api/admin.md new file mode 100644 index 0000000..051c655 --- /dev/null +++ b/docs/api/admin.md @@ -0,0 +1,36 @@ +--- +type: api +title: Admin Endpoints +description: User management endpoints mounted under /api/v1/admin (admin role required). +tags: [api, admin, users] +timestamp: 2026-07-21T18:28:00Z +--- + +# Endpoints + +| Method | Path | Auth | Source | +|---------|-----------------------|---------|-----------------------------------------------------| +| `GET` | `/api/v1/admin/users` | Admin | `backend/src/modules/admin/admin.controller.ts` | +| `POST` | `/api/v1/admin/users` | Admin | Same. | +| `PATCH` | `/api/v1/admin/users/:id` | Admin | Same. | +| `DELETE`| `/api/v1/admin/users/:id` | Admin | Same. | + +# Guard chain + +1. `JwtAuthGuard` (global) validates the access token unless the handler + is `@Public()`. Admin handlers are not. +2. `AdminGuard` (`backend/src/common/guards/admin.guard.ts`) requires + `req.user.role === 'admin'`. + +# Behavior + +* `AdminService` enforces the **last-admin invariant** (`LAST_ADMIN` / + `409`) — the final admin row cannot be demoted or deleted. +* `POST /api/v1/admin/users` creates a new user; `PATCH /api/v1/admin/users/:id` + changes role / status; `DELETE /api/v1/admin/users/:id` removes the row. + +# See also + +- [Backend Module Map](/architecture/backend-modules.md) +- [Admin Shell & User Management](/guides/admin-shell.md) +- [REST API Overview](/api/rest-overview.md) diff --git a/docs/api/auth.md b/docs/api/auth.md new file mode 100644 index 0000000..c6bb0a6 --- /dev/null +++ b/docs/api/auth.md @@ -0,0 +1,90 @@ +--- +type: api +title: Auth Endpoints +description: Login, refresh, logout, public self-registration, CSRF, and first-admin registration. +tags: [api, auth, login, register, refresh, csrf] +timestamp: 2026-07-21T18:28:00Z +--- + +# Endpoints + +| Method | Path | Auth | CSRF | Rate limited | Source | +|--------|-----------------------------------|--------|-----------------------------------------------|--------------|--------| +| `POST` | `/api/v1/auth/register` | `@Public()` | Enforced — caller must first call `GET /api/v1/auth/csrf` (the SPA's `AuthService.ensureCsrf()` does this automatically). | Yes — `RegistrationRateLimitService` per IP | `backend/src/modules/auth/auth.controller.ts` | +| `POST` | `/api/v1/auth/login` | `@Public()` | Enforced — same pattern as `register`. CSRF skip was removed in this change. | Yes — `LoginBackoffService` per IP + username | `backend/src/modules/auth/auth.controller.ts` | +| `POST` | `/api/v1/auth/refresh` | Public route, requires a valid refresh cookie. | Skipped (the request is a `POST` carrying an HttpOnly cookie, not a state-changing user action). | No | `backend/src/modules/auth/auth.controller.ts` | +| `POST` | `/api/v1/auth/logout` | Authenticated (JWT). | Enforced. | No | `backend/src/modules/auth/auth.controller.ts` | +| `GET` | `/api/v1/auth/csrf` | `@Public()` | n/a (`GET`). | No | `backend/src/modules/auth/auth.controller.ts` | +| `POST` | `/api/v1/auth/register-first-admin` | `@Public()` | Skipped (path is in `CsrfMiddleware.SKIP_PATH_PREFIXES`). | Yes | `backend/src/modules/users/users.controller.ts` | + +# Public player self-registration — `POST /api/v1/auth/register` + +New endpoint, gated by the `registrationsEnabled` setting +(`backend/src/config/env.schema.ts` → `SETTINGS_KEYS.REGISTRATIONS_ENABLED`, +default `"false"`). + +| Field | Type | Constraints | +|-------------------|--------|------------------------------------------------------------------------------| +| `username` | string | 3–32 chars, regex `^[a-zA-Z0-9_.-]+$` | +| `password` | string | 1–256 chars; also passes `validatePassword` (Argon2 policy). | +| `passwordConfirm` | string | Must equal `password` (zod refine → `VALIDATION_FAILED` on `passwordConfirm`). | + +Validated by `backend/src/modules/auth/dto/auth.dto.ts` → `RegisterDtoSchema`. + +## Successful response (201) + +```json +{ + "accessToken": "", + "expiresIn": 900, + "user": { "id": "", "username": "player1", "role": "player" } +} +``` + +The refresh token is set as an `HttpOnly` cookie via `setRefreshCookie(res, session.refreshToken, config)`. The new `user` row has `role='player'` and `status='enabled'`. + +## Error envelope + +| HTTP | `code` | When | +|------|-------------------------|---------------------------------------------------------------------------------| +| 400 | `VALIDATION_FAILED` | zod body validation failed. | +| 400 | `WEAK_PASSWORD` | Argon2 policy rejects the password. | +| 403 | `REGISTRATIONS_DISABLED`| `setting.registrationsEnabled !== 'true'`. | +| 409 | `USERNAME_TAKEN` | A user with that username already exists. | +| 429 | `RATE_LIMITED` | `RegistrationRateLimitService` blocked the IP. | + +## Wiring + +* `AuthService.registerPlayer(dto, ip)` runs inside a single + `DataSource.transaction`: + 1. `RegistrationRateLimitService.isAllowed(ip)` check. + 2. `SettingsService.get(SETTINGS_KEYS.REGISTRATIONS_ENABLED, 'false')` check. + 3. Username uniqueness lookup → `USERNAME_TAKEN` on collision. + 4. Argon2id hash + insert `UserEntity({ role: 'player', status: 'enabled' })`. + 5. `RegistrationRateLimitService.record(ip)`. + 6. `mintSession(manager, user)` → access JWT + refresh row. + +# Frontend consumer + +`frontend/src/app/core/services/auth.service.ts` exposes `login()` and +`register()`. Both: + +1. Call `ensureCsrf()` → `GET /api/v1/auth/csrf` to materialise the `csrf` + cookie if it isn't set yet (failures are swallowed because the + `CsrfMiddleware` will mint the cookie on the next response anyway). +2. `POST` with `{ withCredentials: true, observe: 'response' }`. +3. Return a discriminated union: + * `{ ok: true, accessToken, expiresIn, user }` on success. + * `{ ok: false, status, code, message }` on failure, built by the + private `mapAuthError()` helper from the standard + [error envelope](/api/rest-overview.md). + +The `LandingComponent` (`frontend/src/app/features/landing/landing.component.ts`) +consumes both methods and maps the `code` to user-facing copy via +`buildLoginFailureMessage` (see [Landing Page](/guides/landing-page.md)). + +# See also + +- [REST API Overview](/api/rest-overview.md) +- [Setup Endpoint](/api/setup.md) +- [Landing Page Guide](/guides/landing-page.md) diff --git a/docs/api/rest-overview.md b/docs/api/rest-overview.md index 5be60bf..6e77399 100644 --- a/docs/api/rest-overview.md +++ b/docs/api/rest-overview.md @@ -3,7 +3,7 @@ type: api title: REST API Overview description: Base URL, versioning, auth, CSRF, and the standard error envelope. tags: [api, rest, overview, csrf] -timestamp: 2026-07-21T14:43:00Z +timestamp: 2026-07-21T18:28:00Z --- # Base URL & versioning @@ -40,8 +40,11 @@ prefix; the SPA fallback only kicks in for non-API routes. * Skip list (`SKIP_PATH_PREFIXES` in `backend/src/common/middleware/csrf.middleware.ts`): - `/api/v1/auth/register-first-admin` - - `/api/v1/auth/login` - `/api/v1/setup/create-admin` + - `/api/v1/auth/login` is **no longer** skipped — the middleware still + mints the cookie on the first response, and the SPA's + `AuthService.login`/`AuthService.register` first call `GET /api/v1/auth/csrf` + via `ensureCsrf()` to materialise the cookie before submitting. # Error envelope @@ -73,7 +76,8 @@ endpoint). Full list lives in `backend/src/common/errors/error-codes.ts`: `VALIDATION_FAILED`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `CONFLICT`, `LAST_ADMIN`, `SYSTEM_INITIALIZED`, `RATE_LIMITED`, `CSRF_INVALID`, `WEAK_PASSWORD`, `USERNAME_TAKEN`, -`INVALID_CREDENTIALS`, `TOKEN_REVOKED`, `THEME_INVALID`, `INTERNAL`. +`INVALID_CREDENTIALS`, `TOKEN_REVOKED`, `THEME_INVALID`, +`REGISTRATIONS_DISABLED`, `INTERNAL`. # See also diff --git a/docs/api/system.md b/docs/api/system.md new file mode 100644 index 0000000..133f9b8 --- /dev/null +++ b/docs/api/system.md @@ -0,0 +1,69 @@ +--- +type: api +title: System Endpoints +description: Bootstrap payload, event status, and SSE streams. +tags: [api, system, bootstrap, event, sse] +timestamp: 2026-07-21T18:28:00Z +--- + +# Endpoints + +| Method | Path | Auth | Source | +|--------|---------------------------------|--------|-----------------------------------------------------| +| `GET` | `/api/v1/bootstrap` | Public | `backend/src/modules/system/system.controller.ts` | +| `GET` | `/api/v1/event/status` | Public | Same. | +| `GET` | `/api/v1/event/stream` | Public (SSE) | Same. | +| `GET` | `/api/v1/scoreboard/stream` | Public (SSE) | Same. | + +# `GET /api/v1/bootstrap` + +Built by `SystemService.bootstrap()`. Public payload consumed by the +SPA's `BootstrapService`: + +```json +{ + "initialized": true, + "pageTitle": "HIPCTF", + "logo": "", + "welcomeMarkdown": "# Welcome\n\n...", + "theme": { "id": "classic", "tokens": { "...": "..." } }, + "defaultChallengeIp": "127.0.0.1", + "registrationsEnabled": false +} +``` + +* `initialized` is `true` iff at least one admin user exists. +* `theme` is the resolved `Theme` (one of the 10 canonical themes; see + [Theming](/guides/theming.md)). +* `registrationsEnabled` is read from the `setting` table + (`SETTINGS_KEYS.REGISTRATIONS_ENABLED`) and gates the new public + register endpoint (see [Auth Endpoints](/api/auth.md)). + +# `GET /api/v1/event/status` + +Returns the current event window computed by `EventStatusService`: + +```json +{ "state": "Running", "countdownMs": 12345, "startUtc": "...", "endUtc": "..." } +``` + +`state` is `Stopped` when `now < eventStartUtc` (countdown), `Running` +between start and end, and `Stopped` once `now >= eventEndUtc`. + +# SSE streams + +* `/api/v1/event/stream` — emits the same payload as + `GET /api/v1/event/status` whenever `SseHubService.publish` is invoked + (typically via an admin cron tick). +* `/api/v1/scoreboard/stream` — emits the public scoreboard projection + after each new solve. + +Both use NestJS `@Sse()` and are proxied through `SseHubService` +(`backend/src/common/services/sse-hub.service.ts`). + +# See also + +- [Auth and Settings Tables](/database/auth-settings.md) +- [Theming](/guides/theming.md) +- [Event Window](/guides/event-window.md) +- [Scoreboard Stream](/guides/scoreboard-stream.md) diff --git a/docs/api/uploads.md b/docs/api/uploads.md new file mode 100644 index 0000000..eda9bf5 --- /dev/null +++ b/docs/api/uploads.md @@ -0,0 +1,33 @@ +--- +type: api +title: Uploads Endpoints +description: Admin-only multipart upload endpoints for category icons and challenge files. +tags: [api, uploads, multipart, admin] +timestamp: 2026-07-21T18:28:00Z +--- + +# Endpoints + +| Method | Path | Auth | Source | +|--------|---------------------------------------|-------|---------------------------------------------------------| +| `POST` | `/api/v1/uploads/category-icon` | Admin | `backend/src/modules/uploads/uploads.controller.ts` | +| `POST` | `/api/v1/uploads/challenge-file` | Admin | Same. | + +# Guard chain + +Both handlers are gated by `JwtAuthGuard` + `AdminGuard`. CSRF is enforced +(standard cookie + header pattern; nothing is added to the skip list). + +# Behavior + +* Each endpoint uses `FileInterceptor('file')` and parses a single + multipart part named `file`. +* File size is validated by `parseUploadSizeLimit()` in + `backend/src/common/utils/upload.ts`. +* Filenames are sanitized by the same helper before being persisted + under `UPLOAD_DIR`. + +# See also + +- [REST API Overview](/api/rest-overview.md) +- [Backend Module Map](/architecture/backend-modules.md) diff --git a/docs/architecture/backend-modules.md b/docs/architecture/backend-modules.md index dbe44ac..e965da1 100644 --- a/docs/architecture/backend-modules.md +++ b/docs/architecture/backend-modules.md @@ -23,12 +23,13 @@ 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).| +| `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). | | `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')`. | | `UploadsModule` | `backend/src/modules/uploads/uploads.module.ts` | `UploadsController` (`/api/v1/uploads/*`). Admin-only multipart. | +| `BlogModule` | `backend/src/modules/blog/blog.module.ts` | `BlogController` (`GET /api/v1/blog/posts`) + `BlogService` (lists `status='published'` rows). | | `FrontendModule` | `backend/src/frontend/frontend.module.ts` | Registers `SpaFallbackMiddleware` and the uploads static helper. | # Controllers @@ -41,6 +42,7 @@ also registers two global providers: | `AdminController` | `/api/v1/admin` | Admin only | `backend/src/modules/admin/admin.controller.ts` | | `SystemController` | `/api/v1` | Public | `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` | # Common services diff --git a/docs/architecture/frontend-structure.md b/docs/architecture/frontend-structure.md index eb46b7a..45e7078 100644 --- a/docs/architecture/frontend-structure.md +++ b/docs/architecture/frontend-structure.md @@ -3,7 +3,7 @@ type: architecture title: Frontend Structure description: Angular routes, components, services, guards, and interceptors. tags: [architecture, frontend, angular] -timestamp: 2026-07-21T16:48:20Z +timestamp: 2026-07-21T18:28:00Z --- # Routes @@ -13,7 +13,7 @@ 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` | `LoginComponent` | — | Standard login form. | +| `/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 `` for child routes. | | `/admin` | `AdminUsersComponent` | `adminGuard` | Child of `/`; requires `role === 'admin'`. | | `**` | Redirect to `/` | — | Wildcard fallback. | @@ -28,15 +28,19 @@ All components are standalone (no NgModules). Each component imports | `AppComponent` | `frontend/src/app/app.component.ts` | Root; renders ``, calls `BootstrapService.load()` then `AuthService.restoreSession()`. | | `HomeComponent` | `frontend/src/app/features/home/home.component.ts` | Authenticated shell with header, conditional admin nav, and `` 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` | *(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. | +| `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`. | # Services | Service | Path | Purpose | |---------------------|---------------------------------------------------------------|-----------------------------------------------------------| -| `AuthService` | `frontend/src/app/core/services/auth.service.ts` | Signal-backed access token + current user; persists session to `sessionStorage` and exposes `restoreSession()` + `waitUntilHydrated()`. | +| `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. | +| `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. | @@ -48,6 +52,8 @@ All components are standalone (no NgModules). Each component imports | `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. | diff --git a/docs/architecture/key-files.md b/docs/architecture/key-files.md index a83e7f2..3102256 100644 --- a/docs/architecture/key-files.md +++ b/docs/architecture/key-files.md @@ -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-21T16:48:20Z +timestamp: 2026-07-21T18:28:00Z --- # Backend @@ -64,11 +64,11 @@ timestamp: 2026-07-21T16:48:20Z | File | Responsibility | |--------------------------------------------------------------------------|--------------------------------------------------------------------------------| -| `backend/src/modules/auth/auth.module.ts` | Wires `AuthController`, `AuthService`, `JwtStrategy`. | -| `backend/src/modules/auth/auth.controller.ts` | `/api/v1/auth/{login,refresh,logout,csrf}`. | -| `backend/src/modules/auth/auth.service.ts` | Login, refresh, logout, register-first-admin, mint JWT + refresh tokens. | +| `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/cookie.ts` | `setRefreshCookie` / `clearRefreshCookie` helpers. | -| `backend/src/modules/auth/dto/auth.dto.ts` | Zod schemas for login + refresh. | +| `backend/src/modules/auth/dto/auth.dto.ts` | Zod schemas for login + refresh + `RegisterDtoSchema` (with `password === passwordConfirm` refine + username regex). | | `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.controller.ts` | `/api/v1/auth/register-first-admin`. | @@ -87,6 +87,10 @@ timestamp: 2026-07-21T16:48:20Z | `backend/src/modules/system/system.service.ts` | Builds the public bootstrap payload from settings + theme + admin count. | | `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). | @@ -108,17 +112,23 @@ timestamp: 2026-07-21T16:48:20Z | `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()` and `waitUntilHydrated()`. | +| `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/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/admin.service.ts` | `GET /api/v1/admin/users` returning `AdminUser[]`. | +| `frontend/src/app/core/services/markdown.service.ts` | `MarkdownService` (Angular) wraps `DomSanitizer.bypassSecurityTrustHtml` around the rendered Markdown. | +| `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). | | `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/auth/login.component.ts` | Login form. | +| `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). | +| `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. | @@ -138,6 +148,12 @@ timestamp: 2026-07-21T16:48:20Z | `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 `