Files
HIPCTF2/docs/api/admin.md
T

11 KiB
Raw Blame History

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.
api
admin
users
general
categories
default-challenge-ip
ip-hostname
validation
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.

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:

  • 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):

{
  "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

  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