135 lines
6.1 KiB
Markdown
135 lines
6.1 KiB
Markdown
---
|
||
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` 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
|
||
|
||
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)
|