--- type: api title: Admin Endpoints 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 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` is trimmed server-side and must be 1–120 chars after trimming (whitespace-only values are rejected). * `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 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'`. 3. `RolesGuard` enforces `@Roles('admin')` metadata on the controller. # See also - [Backend Module Map](/architecture/backend-modules.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)