11 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-22T15:46:18Z |
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.defaultChallengeIpis trimmed server-side; empty/whitespace-only values are rejected (defaultChallengeIp is required and cannot contain only whitespace), and the trimmed value must be a complete IPv4 address or a valid hostname, otherwise400 VALIDATION_FAILEDwith a per-fielddefaultChallengeIpissue (defaultChallengeIp must be a valid IPv4 address or hostname). Maximum length is 255 characters.registrationsEnabledboolean
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. |
See Admin — Categories.
Challenges — /api/v1/admin/challenges
| Method | Path | Source |
|---|---|---|
GET |
/api/v1/admin/challenges |
backend/src/modules/admin/admin-challenges.controller.ts |
GET |
/api/v1/admin/challenges/:id |
Same. |
POST |
/api/v1/admin/challenges |
Same. |
PUT |
/api/v1/admin/challenges/:id |
Same. |
DELETE |
/api/v1/admin/challenges/:id |
Same. |
GET |
/api/v1/admin/challenges/export |
Same — JSON attachment challenges-export-<ISO-date>.json. |
POST |
/api/v1/admin/challenges/import |
Same — preview by default, ?confirm=overwrite to commit. |
POST |
/api/v1/admin/challenges/files/stage |
Same — multipart upload that returns an opaque staging token. |
DELETE |
/api/v1/admin/challenges/files/stage/:token |
Same — discards a previously staged file. |
List response (admin-only, never includes the secret flag):
{
"id": "uuid",
"name": "Welcome",
"categoryAbbreviation": "CRY",
"categoryIconPath": "/uploads/icons/CRY.png",
"difficulty": "MEDIUM",
"protocol": "WEB",
"enabled": true,
"createdAt": "2026-07-22T20:00:00.000Z",
"updatedAt": "2026-07-22T20:00:00.000Z",
"fileCount": 1,
"solveCount": 0
}
Optional list query: ?q=substring&categoryId=uuid (both optional).
Detail response (admin-only, may include plaintext flag):
{
"id": "uuid",
"name": "Welcome",
"descriptionMd": "...",
"categoryId": "uuid",
"categoryAbbreviation": "CRY",
"categoryIconPath": "/uploads/icons/CRY.png",
"difficulty": "MEDIUM",
"initialPoints": 100,
"minimumPoints": 50,
"decaySolves": 10,
"flag": "flag{...}",
"protocol": "WEB",
"port": null,
"ipAddress": "",
"enabled": true,
"createdAt": "...",
"updatedAt": "...",
"files": [
{ "id": "uuid", "originalFilename": "readme.txt", "mimeType": "text/plain", "sizeBytes": 123, "createdAt": "..." }
]
}
Import document shape:
{
"format": "hipctf-challenges",
"version": 1,
"exportedAt": "2026-07-22T20:00:00Z",
"challenges": [
{
"name": "...",
"descriptionMd": "...",
"categorySystemKey": "CRY",
"difficulty": "LOW|MEDIUM|HIGH",
"initialPoints": 100,
"minimumPoints": 50,
"decaySolves": 10,
"flag": "plaintext",
"protocol": "NC|WEB",
"port": 1337,
"ipAddress": "",
"files": [
{ "originalFilename": "...", "mimeType": "...", "sizeBytes": 123, "dataBase64": "..." }
]
}
]
}
Validation errors return 400 VALIDATION_FAILED with stable codes
(CHALLENGE_NAME_TAKEN, CHALLENGE_INVALID_CATEGORY,
IMPORT_VALIDATION_FAILED, IMPORT_PARTIAL_FAILURE) and a
details[] array of { path, message } entries. Delete cascades
challenge_file and solve rows inside one DB transaction;
filesystem cleanup of the deleted challenge's files runs best-effort
after the transaction commits.
See Admin — Challenges.
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.