docs: update documentation to OKF v0.1 format
This commit is contained in:
@@ -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)
|
||||
@@ -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": "<jwt>",
|
||||
"expiresIn": 900,
|
||||
"user": { "id": "<uuid>", "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)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user