6.1 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | |||||
|---|---|---|---|---|---|---|---|---|---|
| api | Admin Endpoints | Admin-only endpoints for user management, general settings, categories, and supporting uploads. |
|
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):
pageTitleis trimmed server-side and must be 1–120 chars after trimming (whitespace-only values are rejected).logoup to 2048 chars (a public URL — uploaded separately)welcomeMarkdownup to 64 000 charsthemeKeyis one ofTHEME_IDSeventStartUtc/eventEndUtcparseable dates;superRefineenforceseventEndUtc > eventStartUtcand reports the issue oneventEndUtcdefaultChallengeIp1–255 charsregistrationsEnabledboolean
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 }.
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:
GETreturns all categories sorted byLOWER(abbreviation)ascending.POSTuppercases the abbreviation, rejects duplicates with409 CONFLICT, and persists the row. Body validated byCreateCategorySchema(name1–120 chars;abbreviation2–6 chars;descriptionup to 2000 chars;iconPathoptional, up to 2048 chars).PUTvalidates the body withUpdateCategorySchema(every field optional). System rows (system_key IS NOT NULL) cannot change their abbreviation — server returns409 SYSTEM_PROTECTED. Duplicate abbreviations on user rows return409 CONFLICT. Updates theupdated_attimestamp on success.DELETEis blocked for system rows (403 SYSTEM_PROTECTED) and for rows with at least one attached challenge (409 CATEGORY_HAS_CHALLENGESwith{ count }indetails). Returns404 NOT_FOUNDfor unknown ids.
Response shape (CategoryView):
{
"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.
Supporting uploads
Logo and category-icon uploads live on the uploads module and are documented in Uploads Endpoints:
| 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
JwtAuthGuard(global) validates the access token unless the handler is@Public(). Admin handlers are not.AdminGuard(backend/src/common/guards/admin.guard.ts) requiresreq.user.role === 'admin'.RolesGuardenforces@Roles('admin')metadata on the controller.