Files
HIPCTF2/docs/guides/admin-challenges-import.md
T

5.7 KiB

type, title, description, tags, timestamp
type title description tags timestamp
guide Admin — Challenges Full-Replace Import How the confirmed challenges import wipes every existing challenge (with cascading files/solves and disk unlink) and inserts the archive contents in one transaction.
guide
admin
challenges
import
full-replace
tester
2026-07-22T22:17:11Z

What it is

The confirmed challenges import is the second step of the import flow on /admin/challenges (after the preview). When the admin clicks Overwrite & Import the client POSTs the already-validated archive to POST /api/v1/admin/challenges/import?confirm=overwrite. The server performs a full replace of the challenge set inside one DB transaction, then best-effort unlinks the disk files owned by the wiped challenges.

When to use it

Use this when an admin wants to restore the platform challenge set to the exact contents of a previously exported JSON archive. The user guide for the surrounding flow lives at Admin — Challenges; this concept focuses on the import-specific behavior and its consequences.

Behavior contract

Phase What happens
Pre-flight The client already validated the archive against the IMPORT_VALIDATION_FAILED Zod schema in Admin Challenges DTOs. The server re-validates; preview counts (total, conflicts, unknownCategories, warnings) are surfaced by the import modal.
Staging Every dataBase64 file in the archive is decoded and staged to disk via ChallengeFilesService.stage(...). If any stage fails the server rolls back every previously staged file and re-throws (mirrors the existing rollbackStaged safety net).
Transaction (DB) The server begins a single TypeORM transaction. It loads every existing challenge, captures the disk-bound stored_filenames of every existing ChallengeFile, then DELETEs all existing ChallengeEntity rows by id. ChallengeFileEntity and SolveEntity cascade-delete via FK, so dependent rows disappear too. The archive's challenges are then inserted under their original names.
Post-commit (FS) For every captured storedFilename the server calls ChallengeFilesService.removeFinal(storedFilename) (best-effort; failures are logged but do not abort). Each staged file for the imported set is committed to its final filename.
Response Returns { imported, skipped, warnings }. Known unknown categorySystemKeys are reported in skipped[] (still wipe everything else).

Side effects to expect as a tester

  • Every existing challenge disappears from the list immediately after the success banner — including rows whose names did not appear in the archive. Use the Players page to confirm that pre-existing solve rows for wiped challenges also vanish (they cascade).
  • Uploaded challenge files owned by wiped rows are removed from ./data/uploads/challenges/ post-commit. The imported archive's files appear under the same directory using their original filenames encoded onto the wipe.
  • An empty archive ({ challenges: [] }) is valid and produces imported: 0, an empty list, and zero on-disk challenge files.
  • The import modal auto-closes on success; the page-level status banner ([data-testid="challenges-status"]) shows the imported/skipped/warning summary. Failures keep the modal open and show the error inline.

Idempotency

  • Re-importing the same archive twice yields the same final DB state.
  • The previous upsert-on-name semantics are gone: even challenges whose names did not appear in the archive are removed.

Error codes

See Admin Endpoints and the Admin — Challenges guide:

  • IMPORT_VALIDATION_FAILED (400) — schema failure on preview or confirm.
  • IMPORT_PARTIAL_FAILURE is no longer emitted: the response is always 200 with { imported, skipped, warnings } (consistent with partial success being the normal case when unknown categories are present).
  • Any DB transaction failure rolls back every staged file and re-throws the original error, leaving the DB untouched.

Wiring

Concern File
Endpoint backend/src/modules/admin/admin-challenges.controller.ts (POST /api/v1/admin/challenges/import?confirm=overwrite).
Orchestration backend/src/modules/admin/challenges.service.ts (importConfirmed) — full-replace transaction + post-commit unlink.
File staging and disk ops backend/src/modules/admin/challenge-files.service.ts (stage, commit, removeFinal).
Schema backend/src/modules/admin/dto/challenges.dto.ts (CHALLENGE_FORMAT, CHALLENGE_FORMAT_VERSION, import document schema).
Client UI frontend/src/app/features/admin/challenges/challenges.component.ts (onImportConfirm) — refreshes list, sets status, calls closeImport().
Pure helpers frontend/src/app/features/admin/challenges/challenges.pure.ts (summarizeImportResult, importErrorMessage).
Backend contract tests tests/backend/admin-challenges-import-replace.spec.ts — full-replace, disk-file unlink, empty archive, unknown-category wipe.
Frontend contract tests tests/frontend/admin-challenges-import-autoclose.spec.ts — pure helpers + modal auto-close branch.

Notes

  • The flag is not in the archive's per-challenge data; only admins can read the plaintext, and the export/import preserves the same restriction.
  • Wiping cascades remove solve rows for wiped challenges — the scoreboard will recompute on the next render without those entries.
  • The destroy-then-insert happens in one transaction, so a partial failure cannot leave the platform with a mix of old and new rows.