Files
HIPCTF2/docs/api/admin-system.md
T
2026-07-23 12:10:36 +00:00

12 KiB


type: api title: Admin System Endpoints description: Admin-only destructive System operations: backup download, restore validate/commit, re-authenticated confirmation tokens, reset-scores, and wipe-challenges. resource: backend/src/modules/admin/system/admin-system.controller.ts tags: [api, admin, system, backup, restore, danger-zone, confirmation-tokens] timestamp: 2026-07-23T12:04:50Z

Overview

All handlers below are mounted under /api/v1/admin/system and require a JWT-protected admin session (AdminGuard + @Roles('admin')). Every destructive operation is gated by a single-use confirmation token issued by the re-authentication endpoint:

client ──reauth──▶ POST /confirmations ──token──▶ client
client ──commit──▶ POST /restore/commit | /scores/reset | /challenges/wipe

The token table is documented at /database/admin-operation-tokens.md.

Endpoints

Method Path Source
GET /api/v1/admin/system/backup admin-system.controller.tsBackupService.build()
POST /api/v1/admin/system/restore/validate admin-system.controller.tsRestoreService.stageArchive()
POST /api/v1/admin/system/restore/commit admin-system.controller.tsRestoreService.commitRestore() + AuthService.revokeAllRefreshSessions()
POST /api/v1/admin/system/confirmations admin-system.controller.tsAuthService.reauthenticateAdmin() + ConfirmationTokenService.issue()
POST /api/v1/admin/system/scores/reset admin-system.controller.tsDangerZoneService.resetScores()
POST /api/v1/admin/system/challenges/wipe admin-system.controller.tsDangerZoneService.wipeChallenges()

GET /backup

Streams a complete platform snapshot as a JSON attachment. The body has the shape:

{
  "format": "hipctf-system-backup",
  "version": 1,
  "exportedAt": "2026-07-23T12:00:00.000Z",
  "tables": { "user": [...], "challenge": [...], /* every application table */ },
  "tableCounts": { "user": 12, "challenge": 4 },
  "uploads": [
    { "path": "challenges/foo.bin",
      "originalFilename": "foo.bin",
      "sizeBytes": 1024,
      "sha256": "...", "dataBase64": "..." }
  ]
}

Internal tables (admin_operation_token, migrations, all sqlite_*) are excluded. The upload root is walked recursively and the .staging directory is skipped. Files are base64-encoded inline so the artifact is a single self-contained JSON file.

The response uses Content-Disposition: attachment; filename="hipctf-backup-<ISO>.json".

POST /restore/validate

Body:

{ "text": "<raw backup JSON string>" } // or
{ "archive": { /* already-parsed object */ } }

The body is parsed with a strict zod schema (RestoreArchiveSchema). Validation enforces:

  • format === 'hipctf-system-backup', version === 1
  • every upload entry has a safe, non-.. path, decodes from base64 to exactly sizeBytes, and (when provided) matches sha256
  • total restored bytes ≤ BODY_SIZE_LIMIT

Successful validation returns a stagingId plus a summary (sorted table list, per-table counts, file count, total bytes, ISO expiresAt):

{
  "stagingId": "f0c9...",
  "expiresAt": "2026-07-23T12:15:00.000Z",
  "summary": {
    "tables": ["category", "challenge", "user", /* ... */],
    "tableCounts": { "user": 12, "challenge": 4 },
    "files": 3,
    "totalBytes": 8192
  }
}

The stage lives in-memory for SYSTEM_OP_RESTORE_STAGE_TTL_MS (default 15 min) and inside <DATA_DIR>/.system-staging/restore-<id>/.

Errors:

  • SYSTEM_RESTORE_VALIDATION_FAILED (400) — JSON parse, schema, safe path, duplicate path, base64, size mismatch, or checksum mismatch.
  • SYSTEM_RESTORE_PAYLOAD_TOO_LARGE (400) — total bytes over BODY_SIZE_LIMIT.

POST /restore/commit

Body:

{
  "operation": "restore-backup",
  "token": "<confirmation token>",
  "stagingId": "<stage returned from validate>"
}

Flow:

  1. Verifies the admin role and that operation === 'restore-backup'.
  2. ConfirmationTokenService.consume() verifies the token belongs to this admin, is for restore-backup, is unexpired, and is unused.
  3. RestoreService.commitRestore(stagingId) builds a new SQLite file by cloning the live DB, then clearing every application table and inserting the archived rows in FK-safe order. Uploads are copied aside as <UPLOAD_DIR>.restore-stage-<stamp> and the live database + uploads are swapped atomically via FilesystemTransactionService.stageSwap() (with a restore-backup rollback handle).
  4. On any failure the swap is rolled back, the staged tree is removed, and the error is mapped to SYSTEM_RESTORE_ROLLED_BACK (500).
  5. On success, every refresh token belonging to the current admin is revoked via AuthService.revokeAllRefreshSessions() so the SPA is forced back to /login.

Returns { ok: true, operation: 'restore-backup' }.

POST /confirmations

Body:

{
  "operation": "restore-backup" | "reset-scores" | "wipe-challenges",
  "password": "<current admin password>",
  "stagingId": "<optional, only when restoring>"
}

Flow:

  1. AuthService.reauthenticateAdmin(userId, password) verifies the user is still enabled + admin and the Argon2id password matches.
  2. ConfirmationTokenService.issue() stores a SHA-256 hash of the generated token and returns the raw token plus its expiresAt.

Returns:

{ "operation": "restore-backup", "token": "<raw>", "expiresAt": "2026-07-23T12:05:00.000Z" }

Errors:

  • SYSTEM_INVALID_CREDENTIALS (401) — wrong password.
  • SYSTEM_REAUTH_REQUIRED (401) — user is no longer admin/disabled.

POST /scores/reset

Body:

{ "operation": "reset-scores", "token": "<confirmation token>" }

Consumes a reset-scores token, then DangerZoneService.resetScores() runs a single DELETE FROM solve inside a transaction. Returns { ok: true, solvesRemoved: N }. Failure maps to SYSTEM_DANGER_FAILED (500).

POST /challenges/wipe

Body:

{ "operation": "wipe-challenges", "token": "<confirmation token>" }

Consumes a wipe-challenges token, then DangerZoneService.wipeChallenges():

  1. Snapshots <UPLOAD_DIR>/challenges to <challengesDir>.wipe-stage-<ts>.
  2. Runs a SQLite transaction that deletes every challenge row (cascading via FK into challenge_file and solve).
  3. After the DB commits, physically removes the live <UPLOAD_DIR>/challenges directory. If the rm fails, the snapshot is copied back into place.
  4. Removes the snapshot only once the live tree is gone.

Returns { ok: true, challengesRemoved: N, challengeFilesRemoved: M, solvesRemoved: K }. Failure maps to SYSTEM_DANGER_ROLLED_BACK (500).

Error codes

Code HTTP Source
SYSTEM_BACKUP_FAILED 500 BackupService table/file read failure
SYSTEM_FILE_READ_FAILED 500 BackupService upload walk failure
SYSTEM_RESTORE_VALIDATION_FAILED 400 RestoreService archive schema/size/sha mismatch
SYSTEM_RESTORE_PAYLOAD_TOO_LARGE 400 Restored bytes exceed BODY_SIZE_LIMIT
SYSTEM_RESTORE_STAGE_EXPIRED 400 stagingId missing or expired
SYSTEM_RESTORE_FAILED 500 Live DB not readable after swap
SYSTEM_RESTORE_ROLLED_BACK 500 Restore aborted; original state restored
SYSTEM_DANGER_FAILED 500 Reset-scores internal error
SYSTEM_DANGER_ROLLED_BACK 500 Reset/wipe failed; data unchanged
SYSTEM_PAYLOAD_INVALID 400 restore/validate got neither text nor archive
SYSTEM_REAUTH_REQUIRED 401 reauthenticateAdmin user no longer admin/enabled
SYSTEM_INVALID_CREDENTIALS 401 Wrong password
SYSTEM_TOKEN_INVALID 400 Unknown token hash
SYSTEM_TOKEN_EXPIRED 400 Token expiresAt < now
SYSTEM_TOKEN_REUSED 400 Token already consumed
SYSTEM_TOKEN_MISMATCH 400 Token issued for a different operation/user

Examples

Backup → restore on a fresh install

# 1. Download a backup
curl -sS -b cookies.txt -c cookies.txt \
  -X GET 'http://localhost:3000/api/v1/admin/system/backup' \
  -o hipctf-backup.json

# 2. Validate (and stage) it
curl -sS -b cookies.txt -c cookies.txt \
  -H 'Content-Type: application/json' \
  -X POST 'http://localhost:3000/api/v1/admin/system/restore/validate' \
  -d "{\"text\": \"$(cat hipctf-backup.json | python3 -c 'import json,sys;print(json.dumps(sys.stdin.read()))')\"}"
# => {"stagingId":"<id>", "expiresAt":"...", "summary":{...}}

# 3. Re-authenticate to get a confirmation token
curl -sS -b cookies.txt -c cookies.txt \
  -H 'Content-Type: application/json' \
  -X POST 'http://localhost:3000/api/v1/admin/system/confirmations' \
  -d '{"operation":"restore-backup","password":"<currentPassword>","stagingId":"<id>"}'
# => {"operation":"restore-backup","token":"<raw>","expiresAt":"..."}

# 4. Commit
curl -sS -b cookies.txt -c cookies.txt \
  -H 'Content-Type: application/json' \
  -X POST 'http://localhost:3000/api/v1/admin/system/restore/commit' \
  -d '{"operation":"restore-backup","token":"<raw>","stagingId":"<id>"}'

Reset scores

# 1. Re-auth
curl -sS ... -d '{"operation":"reset-scores","password":"..."}'  # → token
# 2. Commit
curl -sS ... -d '{"operation":"reset-scores","token":"<raw>"}'
# → {"ok":true,"solvesRemoved":42}

Wiring

Concern File
Controller backend/src/modules/admin/system/admin-system.controller.ts
Module backend/src/modules/admin/system/admin-system.module.ts
Backup backend/src/modules/admin/system/backup.service.ts
Restore backend/src/modules/admin/system/restore.service.ts
Confirmation tokens backend/src/modules/admin/system/confirmation-token.service.ts
Danger zone backend/src/modules/admin/system/danger-zone.service.ts
Filesystem swap backend/src/modules/admin/system/filesystem-transaction.service.ts
Staging dir helper backend/src/common/utils/upload.ts (resolveSystemStagingDir)
Password re-auth / revoke backend/src/modules/auth/auth.service.ts (reauthenticateAdmin, revokeAllRefreshSessions)
Error codes backend/src/common/errors/error-codes.ts
Front-end page frontend/src/app/features/admin/system/system.component.ts
Front-end modal frontend/src/app/features/admin/system/system-confirm-modal.component.ts
Front-end service frontend/src/app/features/admin/system/system.service.ts
Front-end pure helpers frontend/src/app/features/admin/system/system.pure.ts
Front-end nav entry frontend/src/app/features/admin/admin-shell.component.ts
Front-end route frontend/src/app/app.routes.ts (admin child system)
Cross-store invalidation frontend/src/app/core/services/system-data-change.service.ts
Forced logout helper frontend/src/app/core/services/auth.service.ts (forceServerInvalidation)
Tests tests/backend/admin-system-authorization.spec.ts, admin-system-backup.spec.ts, admin-system-confirmation-token.spec.ts, admin-system-danger.spec.ts, admin-system-restore-validation.spec.ts; tests/frontend/admin-system.pure.spec.ts