From 94c08489f16bb978dc73a2a342e2ee82b2a99d91 Mon Sep 17 00:00:00 2001 From: OpenVelo Agent Date: Wed, 22 Jul 2026 12:05:11 +0000 Subject: [PATCH 1/2] docs: update documentation to OKF v0.1 format --- docs/api/admin.md | 132 +++++++++++++++++++--- docs/architecture/backend-modules.md | 4 +- docs/database/challenges.md | 22 +++- docs/guides/admin-categories.md | 153 ++++++++++++++++++++++++++ docs/guides/admin-general-settings.md | 126 +++++++++++++++++++++ docs/guides/admin-shell.md | 114 +++++++++++-------- docs/index.md | 17 ++- 7 files changed, 497 insertions(+), 71 deletions(-) create mode 100644 docs/guides/admin-categories.md create mode 100644 docs/guides/admin-general-settings.md diff --git a/docs/api/admin.md b/docs/api/admin.md index 051c655..b534c0c 100644 --- a/docs/api/admin.md +++ b/docs/api/admin.md @@ -1,19 +1,120 @@ --- 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 +description: Admin-only endpoints for user management, general settings, categories, and supporting uploads. +tags: [api, admin, users, general, categories] +timestamp: 2026-07-22T12:00: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. | +All handlers below are mounted behind `JwtAuthGuard` (global) + +`AdminGuard` + `@Roles('admin')`, so they require a valid admin JWT. +CSRF is enforced on every mutating call (standard cookie + header +pattern; nothing is added to the skip list). + +## User management — `/api/v1/admin` + +| Method | Path | Source | +|----------|-------------------------------|---------------------------------------------------| +| `GET` | `/api/v1/admin/users` | `backend/src/modules/admin/admin.controller.ts` | +| `POST` | `/api/v1/admin/users` | Same. | +| `PATCH` | `/api/v1/admin/users/:id` | Same. | +| `DELETE` | `/api/v1/admin/users/:id` | Same. | + +`AdminService` enforces the **last-admin invariant** (`LAST_ADMIN` / +`409`) — the final admin row cannot be demoted or deleted. `POST` +creates a new user; `PATCH .../:id` changes role / status; `DELETE .../:id` +removes the row. + +## General settings — `/api/v1/admin/general` + +| Method | Path | Source | +|--------|---------------------------------------|-------------------------------------------------------------| +| `GET` | `/api/v1/admin/general/settings` | `backend/src/modules/admin/admin-general.controller.ts` | +| `PUT` | `/api/v1/admin/general/settings` | Same. | +| `GET` | `/api/v1/admin/general/themes` | Same. | + +`GET /settings` returns the full `GeneralSettingsView` +(`pageTitle`, `logo`, `welcomeMarkdown`, `themeKey`, `eventStartUtc`, +`eventEndUtc`, `defaultChallengeIp`, `registrationsEnabled`). + +`PUT /settings` is validated by `GeneralSettingsSchema` (zod): + +* `pageTitle` 1–120 chars +* `logo` up to 2048 chars (a public URL — uploaded separately) +* `welcomeMarkdown` up to 64 000 chars +* `themeKey` is one of `THEME_IDS` +* `eventStartUtc` / `eventEndUtc` parseable dates; `superRefine` + enforces `eventEndUtc > eventStartUtc` and reports the issue on + `eventEndUtc` +* `defaultChallengeIp` 1–255 chars +* `registrationsEnabled` boolean + +On success the handler persists every field via `SettingsService`, +emits an SSE `general` event (`{ topic: 'general', themeKey }`) via +`SseHubService`, and returns the updated view. + +`GET /themes` returns the `ThemeView[]` for which a corresponding +JSON file exists under `THEMES_DIR`. Each item is `{ id, key, name }`. + +See [Admin — General Settings](/guides/admin-general-settings.md). + +## Categories — `/api/v1/admin/categories` + +| Method | Path | Source | +|----------|-------------------------------------|-----------------------------------------------------------------| +| `GET` | `/api/v1/admin/categories` | `backend/src/modules/admin/admin-categories.controller.ts` | +| `POST` | `/api/v1/admin/categories` | Same. | +| `PUT` | `/api/v1/admin/categories/:id` | Same. | +| `DELETE` | `/api/v1/admin/categories/:id` | Same. | + +Behavior: + +* `GET` returns all categories sorted by `LOWER(abbreviation)` ascending. +* `POST` uppercases the abbreviation, rejects duplicates with `409 CONFLICT`, + and persists the row. Body validated by `CreateCategorySchema` + (`name` 1–120 chars; `abbreviation` 2–6 chars; `description` up to + 2000 chars; `iconPath` optional, up to 2048 chars). +* `PUT` validates the body with `UpdateCategorySchema` (every field + optional). System rows (`system_key IS NOT NULL`) cannot change their + abbreviation — server returns `409 SYSTEM_PROTECTED`. Duplicate + abbreviations on user rows return `409 CONFLICT`. Updates the + `updated_at` timestamp on success. +* `DELETE` is blocked for system rows (`403 SYSTEM_PROTECTED`) and for + rows with at least one attached challenge (`409 CATEGORY_HAS_CHALLENGES` + with `{ count }` in `details`). Returns `404 NOT_FOUND` for unknown ids. + +Response shape (`CategoryView`): + +```json +{ + "id": "uuid", + "name": "Cryptography", + "abbreviation": "CRY", + "description": "Cryptographic challenges", + "iconPath": "/uploads/icons/CRY.png", + "isSystem": true, + "systemKey": "CRY", + "createdAt": "2026-07-21T18:00:00.000Z", + "updatedAt": "2026-07-21T18:00:00.000Z" +} +``` + +See [Admin — Categories](/guides/admin-categories.md). + +## Supporting uploads + +Logo and category-icon uploads live on the uploads module and are +documented in [Uploads Endpoints](/api/uploads.md): + +| Method | Path | Purpose | +|--------|-----------------------------------|----------------------------------| +| `POST` | `/api/v1/uploads/logo` | Logo used by General settings. | +| `POST` | `/api/v1/uploads/category-icon` | Icon used by Categories. | + +Both endpoints are admin-only and use `FileInterceptor('file')` for a +single multipart part named `file`. # Guard chain @@ -21,16 +122,13 @@ timestamp: 2026-07-21T18:28:00Z 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. +3. `RolesGuard` enforces `@Roles('admin')` metadata on the controller. # See also - [Backend Module Map](/architecture/backend-modules.md) -- [Admin Shell & User Management](/guides/admin-shell.md) +- [Admin Shell](/guides/admin-shell.md) +- [Admin — General Settings](/guides/admin-general-settings.md) +- [Admin — Categories](/guides/admin-categories.md) +- [Uploads Endpoints](/api/uploads.md) - [REST API Overview](/api/rest-overview.md) diff --git a/docs/architecture/backend-modules.md b/docs/architecture/backend-modules.md index 740383e..a09ca23 100644 --- a/docs/architecture/backend-modules.md +++ b/docs/architecture/backend-modules.md @@ -27,7 +27,7 @@ also registers two global providers: | `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')`. | +| `AdminModule` | `backend/src/modules/admin/admin.module.ts` | Three controllers (`AdminController` for `/api/v1/admin/users*`, `AdminGeneralController` for `/api/v1/admin/general/*`, `AdminCategoriesController` for `/api/v1/admin/categories*`) plus their services (`AdminService`, `AdminGeneralService`, `AdminCategoriesService`). Gated by `AdminGuard` + `@Roles('admin')`. Imports `TypeOrmModule.forFeature([UserEntity, CategoryEntity, ChallengeEntity])`, `AuthModule`, `UsersModule`, `SettingsModule`, and `CommonModule` (for `SseHubService`, `ThemeLoaderService`). | | `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. | @@ -40,6 +40,8 @@ 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` | +| `AdminGeneralController` | `/api/v1/admin/general` | Admin only | `backend/src/modules/admin/admin-general.controller.ts` | +| `AdminCategoriesController` | `/api/v1/admin/categories` | Admin only | `backend/src/modules/admin/admin-categories.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` | diff --git a/docs/database/challenges.md b/docs/database/challenges.md index aef6b86..9b4b90b 100644 --- a/docs/database/challenges.md +++ b/docs/database/challenges.md @@ -13,14 +13,26 @@ timestamp: 2026-07-21T14:18:00Z | Column | Type | Description | |----------------|---------|------------------------------------------------------------------------------| | `id` | TEXT PK | UUID v4. | -| `system_key` | TEXT | One of `crypto`, `forensics`, `pwn`, `web`, `misc`, `osint` (unique where NOT NULL) or `NULL` for user-created categories. | +| `system_key` | TEXT | Non-null for system categories (e.g. `CRY`, `MSC`, `PWN`, `REV`, `WEB`, `HW`); unique where NOT NULL; `NULL` for user-created categories. | | `name` | TEXT | Display name (e.g. `Cryptography`). | -| `abbreviation` | TEXT | Short label (e.g. `CRY`). | +| `abbreviation` | TEXT | Short label (e.g. `CRY`), 2–6 chars, server-uppercased. Globally unique — enforced by `uq_category_abbreviation`. | | `description` | TEXT | Optional description. | -| `icon_path` | TEXT | Default icon URL (e.g. `/uploads/icons/crypto.svg`). | +| `icon_path` | TEXT | Default icon URL (e.g. `/uploads/icons/CRY.png`). | +| `created_at` | TEXT | ISO 8601 timestamp the row was created (added by `AddCategoryTimestampsAndUniqueAbbrev1700000000200`). | +| `updated_at` | TEXT | ISO 8601 timestamp the row was last updated (added by the same migration; bumped on `PUT /api/v1/admin/categories/:id`). | -The seed migration inserts one row per `SYSTEM_CATEGORY_KEYS` value from -`backend/src/config/env.schema.ts`. +System rows are seeded by the +`UpdateSystemCategoryKeys1700000000300` migration so the canonical +keys are `CRY` (Cryptography), `MSC` (Misc), `PWN` (Binary +exploitation), `REV` (Reverse Engineering), `WEB` (Web), and `HW` +(Hardware). The migration drops any legacy seeded rows that no +longer match the canonical set before inserting the missing entries. +User-created categories leave `system_key` as `NULL`. + +The uniqueness on `abbreviation` is enforced both by application +validation (uppercased on save, duplicates rejected) and by the +`uq_category_abbreviation` index created in migration +`1700000000200`. ## `challenge` diff --git a/docs/guides/admin-categories.md b/docs/guides/admin-categories.md new file mode 100644 index 0000000..4f0bc45 --- /dev/null +++ b/docs/guides/admin-categories.md @@ -0,0 +1,153 @@ +--- +type: guide +title: Admin — Categories +description: How an admin lists, creates, edits, and deletes challenge categories from the /admin/categories page, including system-row protection and challenge-attached protection. +tags: [guide, admin, categories, tester] +timestamp: 2026-07-22T12:00:00Z +--- + +# When this view is available + +The Categories admin page renders at `/admin/categories` for users with +`role === 'admin'`. It is reached from the [Admin Shell](/guides/admin-shell.md) +side-nav (categories are also embedded inside the General settings page). + +| Layer | File | Check | +|------------------|----------------------------------------------------------------------------|------------------------------------------------------| +| Client route | `frontend/src/app/app.routes.ts` | `/admin/categories` child of `adminGuard`. | +| Client component | `frontend/src/app/features/admin/categories/categories.component.ts` | `AdminCategoriesComponent.ngOnInit` fetches the list. | +| Client modals | `frontend/src/app/features/admin/categories/category-form-modal.component.ts` | Create / edit modal. | +| | `frontend/src/app/features/admin/categories/category-delete-modal.component.ts` | Delete confirmation modal. | +| Server route | `backend/src/modules/admin/admin-categories.controller.ts` | `GET/POST /api/v1/admin/categories`, `PUT/DELETE /:id`. | + +# How to access (tester steps) + +1. Sign in as an admin user. +2. Open **Admin area** → **Categories** in the side-nav, or visit + `/admin/categories` directly. (The page also renders below the + General settings form at `/admin/general`.) +3. The page shows `Loading categories...` (`data-testid="cat-loading"`) + while the list request is in flight, then renders one row per + category sorted alphabetically by (lowercased) abbreviation. + +# Visual elements + +| Element | Selector | Purpose | +|------------------------|--------------------------------------------|---------| +| Page section | `[data-testid="admin-categories"]` | Root container. | +| Add button (`+`) | `[data-testid="cat-add"]` | Opens the create modal. | +| Loading | `[data-testid="cat-loading"]` | "Loading categories..." placeholder. | +| Load error | `[data-testid="cat-error"]` | Red error text. | +| List | `[data-testid="cat-list"]` | `