6.5 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | |||||
|---|---|---|---|---|---|---|---|---|---|
| api | Admin Endpoints | Admin-only endpoints for user management, required general settings, categories, and supporting uploads. |
|
2026-07-22T14:24:08Z |
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/eventEndUtcmust each be non-empty ISO-8601 datetimes. Empty and malformed strings produce a field-specific400 VALIDATION_FAILEDissue (eventStartUtc must be a valid ISO-8601 datetimeoreventEndUtc must be a valid ISO-8601 datetime).superRefineadditionally requireseventEndUtc > eventStartUtcand reportseventEndUtc must be strictly after eventStartUtcon the end field when both values parse but are out of order.defaultChallengeIp1–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.