Files
HIPCTF2/docs/api/admin.md
T

246 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
type: api
title: Admin Endpoints
description: Admin-only endpoints for user management, required general settings, categories, and supporting uploads.
tags: [api, admin, users, general, categories, default-challenge-ip, ip-hostname, validation]
timestamp: 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):
* `pageTitle` is trimmed server-side and must be 1120 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` must each be non-empty ISO-8601
datetimes. Empty and malformed strings produce a field-specific
`400 VALIDATION_FAILED` issue (`eventStartUtc must be a valid ISO-8601
datetime` or `eventEndUtc must be a valid ISO-8601 datetime`).
`superRefine` additionally requires `eventEndUtc > eventStartUtc` and
reports `eventEndUtc must be strictly after eventStartUtc` on the end
field when both values parse but are out of order.
* `defaultChallengeIp` is 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, otherwise `400 VALIDATION_FAILED` with a per-field
`defaultChallengeIp` issue (`defaultChallengeIp must be a valid IPv4
address or hostname`). Maximum length is 255 characters.
* `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. |
See [Admin — Categories](/guides/admin-categories.md).
## 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**):
```json
{
"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`**):
```json
{
"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:
```json
{
"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](/guides/admin-challenges.md).
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` 1120 chars; `abbreviation` 26 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)