Files
HIPCTF2/docs/guides/admin-challenges.md
T
2026-07-22 21:26:21 +00:00

13 KiB
Raw Blame History

type, title, description, tags, timestamp
type title description tags timestamp
guide Admin — Challenges How an admin lists, searches, creates, edits, deletes, imports, and exports challenges from the /admin/challenges page.
guide
admin
challenges
import
export
files
tester
2026-07-22T21:25:29Z

When this view is available

The Challenges admin page renders at /admin/challenges for users with role === 'admin'. It is reached from the Admin Shell side-nav.

Layer File Check
Client route frontend/src/app/app.routes.ts /admin/challenges lazy-loaded under adminGuard.
Client component frontend/src/app/features/admin/challenges/challenges.component.ts Smart container; owns search, sort, modal state.
Client modals frontend/src/app/features/admin/challenges/challenge-form-modal.component.ts Create / edit modal with General / Connection / Files tabs.
frontend/src/app/features/admin/challenges/challenge-delete-modal.component.ts Non-backdrop-dismissible delete confirmation.
frontend/src/app/features/admin/challenges/challenge-import-modal.component.ts Non-backdrop-dismissible import preview/result.
Server route backend/src/modules/admin/admin-challenges.controller.ts /api/v1/admin/challenges*, mounted under AdminGuard + @Roles('admin').

How to access (tester steps)

  1. Sign in as an admin user.
  2. Open Admin areaChallenges in the side-nav, or visit /admin/challenges directly.
  3. The page shows Loading challenges… (data-testid="challenges-loading") while the request is in flight, then renders the list, an empty state, or a load error.

Visual elements

Element Selector Purpose
Page section [data-testid="admin-challenges"] Root container.
Search input [data-testid="challenges-search"] Debounced (~250 ms) case-insensitive substring filter on name.
Add button [data-testid="challenges-add"] Opens the create modal.
Import button [data-testid="challenges-import"] Opens a file picker for .json; preview/confirm flow runs in the import modal.
Export button [data-testid="challenges-export"] Downloads challenges-export-<ISO-date>.json.
Status banner [data-testid="challenges-status"] role="status", success message after CRUD/import/export.
Error banner [data-testid="challenges-error"] role="alert", red load error.
Loading [data-testid="challenges-loading"] "Loading challenges…" placeholder.
Empty (unfiltered) [data-testid="challenges-empty"] Centered "No challenges yet" + Add challenge button.
Empty (filtered) [data-testid="challenges-empty-search"] "No challenges match ''".
Table [data-testid="challenges-table"] One row per challenge.
Row [data-testid="challenge-row-<id>"] Row in the list.
Sortable Name [data-testid="challenges-sort-name"] Toggle asc/desc.
Edit row action [data-testid="challenges-row-edit"] Opens the edit modal (loads detail).
Delete row action [data-testid="challenges-row-delete"] Opens the delete confirmation modal.
Form modal [data-testid="challenge-form-modal"] Backdrop-dismissible (this is the only non-destructive modal).
Form tabs [data-testid="cf-tab-general" / -connection / -files"] Switch between sections.
Delete modal [data-testid="challenge-delete-modal"] Non-backdrop-dismissible; "Cancel" + "Delete".
Import modal [data-testid="challenge-import-modal"] Non-backdrop-dismissible; preview, confirm, result.

Form fields

The form modal has three sections (tabs): General, Connection, Files. Fields marked required are not nullable; the server enforces all rules and surfaces structured 400 VALIDATION_FAILED errors that the modal renders inline (data-testid="cf-error-<field>").

Label data-testid Required Notes
Name (1128 chars) cf-name yes Server enforces case-insensitive uniqueness; CHALLENGE_NAME_TAKEN is shown inline.
Description (Markdown) cf-desc yes "Preview" toggle renders sanitized HTML using MarkdownService.
Category dropdown cf-category yes Populated from GET /api/v1/admin/categories.
Difficulty radios cf-difficulty-* yes LOW / MEDIUM / HIGH.
Initial points cf-initial yes Integer 1100000 (default 100).
Minimum points cf-minimum yes Integer 1100000; default = floor(initialPoints / 2). Auto-updates while untouched; once edited, the value sticks.
Decay solves cf-decay yes Integer 110000.
Secret flag (password) cf-flag yes Stored plaintext server-side; toggled between password and text via cf-flag-toggle. The flag is omitted from any non-admin DTO.
Protocol radios cf-protocol-* yes NC / WEB.
Port (NC: required) cf-port NC only Digits-only text input with numeric keyboard hint; integer 165535. Switching to WEB clears it.
IP address cf-ip no Free text — falls back to setting.defaultChallengeIp at solve time.
File picker / drop zone cf-upload, cf-drop-zone no Accepts multiple files of any type. Users may choose files or drag them onto the dashed drop zone; oversize files are rejected before upload.
Upload error cf-upload-error n/a Reports the rejected filename and global size limit, or a staging request failure.
File list cf-files n/a Existing files (with 🗑 to mark for removal) plus staged uploads.

Expected behavior

  • The list is fetched via GET /api/v1/admin/challenges and contains the admin-only summary DTO (categoryAbbreviation, categoryIconPath, difficulty, protocol, enabled, fileCount, solveCount, createdAt, updatedAt). The secret flag is never in the response.
  • Typing in the search input triggers a 250 ms debounced reload with ?q=.
  • Toggling the sort header flips asc/desc over LOWER(name). The client re-sorts defensively in applySort.
  • While the list is in flight, the toolbar buttons stay disabled and show an inline spinner.
  • The icon column renders <img src="iconPath">; on error it hides the image and the abbreviation fallback appears as text.

Create / Edit

  1. Click Add challenge (or the row's ✎ button) → form modal opens in create (or edit) mode.
  2. Edit mode fetches GET /api/v1/admin/challenges/:id to populate every field including flag, then renders sanitized description preview.
  3. For NC challenges, Port starts blank and must contain digits representing an integer from 1 through 65535. Invalid or missing values display inline, and Save automatically opens the Connection tab so the error is visible. General-field errors similarly open the General tab.
  4. Files can be selected through the picker or dropped onto the Files tab drop zone. Files within the global limit immediately appear as uploading placeholders and are staged through POST /api/v1/admin/challenges/files/stage; oversize files never trigger that request and show cf-upload-error.
  5. Save calls POST (or PUT) with the manifest of staged file tokens and existing-file removal ids.
  6. Server-side errors map to inline field messages (cf-error-<field>) and stay on the modal; success closes the modal, refreshes the list, and surfaces a success banner.
  7. Cancel/Esc/backdrop discards every pending staged token via DELETE /files/stage/:token.

Delete

  1. Click the row's 🗑 button → delete modal opens. Backdrop dismissal and Esc are intentionally disabled.
  2. Confirm → DELETE /api/v1/admin/challenges/:id. The server runs one transaction that cascades challenge_file and solve rows, then best-effort unlinks the disk files. Failures to unlink are logged but do not abort the transaction.
  3. Success closes the modal, refreshes the list, and announces a status banner.

Export

  • Click Export challengesGET /api/v1/admin/challenges/export returns a JSON attachment named challenges-export-<ISO-date>.json. The browser downloads it; the URL is revoked after a short delay.

Import

  1. Click Import challenges → a file picker accepts .json. The client reads it and posts it to POST /api/v1/admin/challenges/import.
  2. On validation success the import modal opens with the preview (total, conflicts, unknownCategories, warnings) and the standard copy: "Import N challenges. Existing challenges with matching names will be overwritten. New categories referenced but missing locally will be skipped with a warning. Continue?"
  3. Click Overwrite & Import → the client posts the same document with ?confirm=overwrite. The server stages every file up front (rolling back any FS failure), then upserts challenges inside one transaction by case-insensitive name.
  4. The response renders imported/skipped/warning counts; the list refreshes automatically.

Validation & error codes (server)

Code HTTP Triggered by
VALIDATION_FAILED 400 Zod schema failure on create/update/list query/import document.
CHALLENGE_NAME_TAKEN 409 Case-insensitive duplicate name.
CHALLENGE_INVALID_CATEGORY 400 categoryId does not match any existing category.
IMPORT_VALIDATION_FAILED 400 Import document fails top-level or per-challenge validation.
IMPORT_PARTIAL_FAILURE 200 Import committed but some entries were skipped (returns {imported, skipped, warnings}).
NOT_FOUND 404 GET/PUT/DELETE on a non-existent challenge id.

Architecture map

Step Where What happens
1 frontend/src/app/app.routes.ts /admin/challenges lazy-loads AdminChallengesComponent.
2 frontend/src/app/features/admin/challenges/challenges.component.ts Owns search debounce, modal state, in-flight locks, refresh.
3 frontend/src/app/core/services/admin.service.ts Typed listChallenges, getChallengeDetail, createChallenge, updateChallenge, deleteChallenge, exportChallenges, previewChallengeImport, confirmChallengeImport, stageChallengeFile, discardChallengeFile.
4 frontend/src/app/features/admin/challenges/challenge-form.pure.ts Defaults/prefill, points/port validation, first-invalid-tab selection, file-size partitioning, payload mapping.
5 backend/src/modules/admin/admin-challenges.controller.ts AdminGuard + @Roles('admin') on every handler.
6 backend/src/modules/admin/challenges.service.ts CRUD, list aggregates, transactional deletion, export, import orchestration.
7 backend/src/modules/admin/challenge-files.service.ts Staging, commit, discard, read for export, limits.
8 backend/src/modules/admin/dto/challenges.dto.ts Zod schemas for IDs, list query, create/update, staged-file manifest, import document with field paths.

Notes

  • The flag is never present in any non-admin DTO. The player-facing challenges route and scoreboard remain unchanged; only admin endpoints can fetch the plaintext.
  • All mutating endpoints require the standard CSRF cookie + header pair; the existing csrfInterceptor handles the headers automatically.
  • The list query enforces a deterministic LOWER(name), name order so the UI sort is reproducible.
  • Importing the same document twice is idempotent: every incoming challenge either replaces an existing row by lowercased name or inserts a new one.