docs: update documentation to OKF v0.1 format
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
---
|
||||
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](/database/admin-operation-tokens.md).
|
||||
|
||||
# Endpoints
|
||||
|
||||
| Method | Path | Source |
|
||||
|--------|--------------------------------------------|-----------------------------------------------------------|
|
||||
| `GET` | `/api/v1/admin/system/backup` | `admin-system.controller.ts` → `BackupService.build()` |
|
||||
| `POST` | `/api/v1/admin/system/restore/validate` | `admin-system.controller.ts` → `RestoreService.stageArchive()` |
|
||||
| `POST` | `/api/v1/admin/system/restore/commit` | `admin-system.controller.ts` → `RestoreService.commitRestore()` + `AuthService.revokeAllRefreshSessions()` |
|
||||
| `POST` | `/api/v1/admin/system/confirmations` | `admin-system.controller.ts` → `AuthService.reauthenticateAdmin()` + `ConfirmationTokenService.issue()` |
|
||||
| `POST` | `/api/v1/admin/system/scores/reset` | `admin-system.controller.ts` → `DangerZoneService.resetScores()` |
|
||||
| `POST` | `/api/v1/admin/system/challenges/wipe` | `admin-system.controller.ts` → `DangerZoneService.wipeChallenges()` |
|
||||
|
||||
## GET /backup
|
||||
|
||||
Streams a complete platform snapshot as a JSON attachment. The body has
|
||||
the shape:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"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:
|
||||
|
||||
```jsonc
|
||||
{ "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):
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"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:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"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:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"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:
|
||||
|
||||
```jsonc
|
||||
{ "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:
|
||||
|
||||
```jsonc
|
||||
{ "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:
|
||||
|
||||
```jsonc
|
||||
{ "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
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```bash
|
||||
# 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` |
|
||||
@@ -27,7 +27,8 @@ also registers two global providers:
|
||||
| `UsersModule` | `backend/src/modules/users/users.module.ts` | `UsersController` (`/api/v1/auth/register-first-admin`) + `UsersService` (last-admin invariant) + `UsersRankService` (`rankOfUser(manager, userId) -> {rank, points}` over the `solve` table). |
|
||||
| `SetupModule` | `backend/src/modules/setup/setup.module.ts` | `SetupController` (`POST /api/v1/setup/create-admin`) + `SetupService`. Returns 409 `SYSTEM_INITIALIZED` once any admin exists; serializes concurrent first-admin attempts via an in-process promise chain. |
|
||||
| `SystemModule` | `backend/src/modules/system/system.module.ts` | `SystemController` (`/api/v1/bootstrap`, `/event/status`, SSE streams) + `SystemService`. |
|
||||
| `AdminModule` | `backend/src/modules/admin/admin.module.ts` | Four controllers (`AdminController` for `/api/v1/admin/users*`, `AdminGeneralController` for `/api/v1/admin/general/*`, `AdminCategoriesController` for `/api/v1/admin/categories*`, `AdminChallengesController` for `/api/v1/admin/challenges*`) plus their services (`AdminService`, `AdminGeneralService`, `AdminCategoriesService`, `AdminChallengesService`, `ChallengeFilesService`). Gated by `AdminGuard` + `@Roles('admin')`. Imports `TypeOrmModule.forFeature([UserEntity, CategoryEntity, ChallengeEntity, ChallengeFileEntity, SolveEntity])`, `AuthModule`, `UsersModule`, `SettingsModule`, and `CommonModule` (for `SseHubService`, `ThemeLoaderService`). |
|
||||
| `AdminModule` | `backend/src/modules/admin/admin.module.ts` | Five controllers (`AdminController` for `/api/v1/admin/users*`, `AdminGeneralController` for `/api/v1/admin/general/*`, `AdminCategoriesController` for `/api/v1/admin/categories*`, `AdminChallengesController` for `/api/v1/admin/challenges*`, `AdminSystemController` for `/api/v1/admin/system/*`) plus their services (`AdminService`, `AdminGeneralService`, `AdminCategoriesService`, `AdminChallengesService`, `ChallengeFilesService`, `BackupService`, `RestoreService`, `DangerZoneService`, `FilesystemTransactionService`, `ConfirmationTokenService`). Gated by `AdminGuard` + `@Roles('admin')`. Imports `TypeOrmModule.forFeature([UserEntity, CategoryEntity, ChallengeEntity, ChallengeFileEntity, SolveEntity])`, `AuthModule`, `UsersModule`, `SettingsModule`, `CommonModule` (for `SseHubService`, `ThemeLoaderService`), and `AdminSystemModule` (which adds `AdminOperationTokenEntity` + `AuthModule`). |
|
||||
| `AdminSystemModule` | `backend/src/modules/admin/system/admin-system.module.ts` | Hosts the destructive System operations: `AdminSystemController` + `BackupService` + `RestoreService` + `DangerZoneService` + `FilesystemTransactionService` + `ConfirmationTokenService`. Imports `TypeOrmModule.forFeature([AdminOperationTokenEntity])` and `AuthModule` (for `reauthenticateAdmin`/`revokeAllRefreshSessions`). |
|
||||
| `UploadsModule` | `backend/src/modules/uploads/uploads.module.ts` | `UploadsController` (`/api/v1/uploads/*`). Admin-only multipart. |
|
||||
| `BlogModule` | `backend/src/modules/blog/blog.module.ts` | `BlogController`/`BlogService` expose published posts at `GET /api/v1/blog/posts`; `AdminBlogController`/`AdminBlogService` provide guarded CRUD at `/api/v1/admin/blog/posts` using `BlogPostEntity`. |
|
||||
| `ChallengesModule` | `backend/src/modules/challenges/challenges.module.ts` | `ChallengesController` (`/api/v1/challenges/{board,status,:id,:id/solves}`) + `ChallengesEventsController` (authenticated SSE `/api/v1/events`) + `ChallengesService` (board, detail, submit, scoring util). |
|
||||
@@ -44,6 +45,7 @@ also registers two global providers:
|
||||
| `AdminGeneralController` | `/api/v1/admin/general` | Admin only | `backend/src/modules/admin/admin-general.controller.ts` |
|
||||
| `AdminCategoriesController` | `/api/v1/admin/categories` | Admin only | `backend/src/modules/admin/admin-categories.controller.ts` |
|
||||
| `AdminChallengesController` | `/api/v1/admin/challenges` | Admin only | `backend/src/modules/admin/admin-challenges.controller.ts` |
|
||||
| `AdminSystemController` | `/api/v1/admin/system` | Admin only (plus confirmation tokens for destructive ops) | `backend/src/modules/admin/system/admin-system.controller.ts` |
|
||||
| `SystemController` | `/api/v1` | Mixed (bootstrap/event/scoreboard/settings are public; `events/status` SSE is JWT-protected) | `backend/src/modules/system/system.controller.ts` |
|
||||
| `UploadsController` | `/api/v1/uploads` | Admin only | `backend/src/modules/uploads/uploads.controller.ts` |
|
||||
| `BlogController` | `/api/v1/blog` | Public | `backend/src/modules/blog/blog.controller.ts` |
|
||||
@@ -91,3 +93,4 @@ also registers two global providers:
|
||||
- [Key Files Index](/architecture/key-files.md)
|
||||
- [REST API Overview](/api/rest-overview.md)
|
||||
- [Blog API](/api/blog.md)
|
||||
- [Admin System API](/api/admin-system.md)
|
||||
|
||||
@@ -123,6 +123,36 @@ timestamp: 2026-07-23T10:12:24Z
|
||||
| `tests/backend/scoreboard.service.spec.ts` | Service-level tests covering `computeAwardedPoints` parity with `submitFlag`, color-index stability, matrix ordering, and graph state handling. |
|
||||
| `tests/frontend/scoreboard.pure.spec.ts` | Pure helpers: `applyRankingSort` (competition-rank numbering), `parseSolveEventIntoRanking` (add-or-increment + sort), `dedupEventLogBySolveId`, `applySolveToGraph` (anchors + plateau + top-10 trim), `mutateMatrixFromSolve`, `stablePlayerColorIndex`, `formatSolveDateTime`. |
|
||||
| `tests/frontend/scoreboard.store.spec.ts` | Store lifecycle + live merge: `loadAll` aggregation + idempotency, SSE `solve` frame applying to all four tabs, `stop()` teardown, exponential reconnect. |
|
||||
| `backend/src/modules/admin/system/admin-system.controller.ts` | Mounts `/api/v1/admin/system/*` (backup download, restore validate/commit, confirmations, scores/reset, challenges/wipe); all handlers require `AdminGuard` + `@Roles('admin')` and the destructive ones also consume a confirmation token. |
|
||||
| `backend/src/modules/admin/system/admin-system.module.ts` | Wires `AdminSystemController`, `BackupService`, `RestoreService`, `DangerZoneService`, `FilesystemTransactionService`, `ConfirmationTokenService`; imports `TypeOrmModule.forFeature([AdminOperationTokenEntity])` and `AuthModule`. |
|
||||
| `backend/src/modules/admin/system/backup.service.ts` | Builds the full backup JSON: every application table (`sqlite_master` discovery, excluding `admin_operation_token`/`migrations`/`sqlite_*`), with base64-encoded uploads walked recursively from `UPLOAD_DIR` (skipping `.staging`). Exposes `discoverTables()`, `getUploadDir()`, and `BackupService.stringify(doc)`. |
|
||||
| `backend/src/modules/admin/system/restore.service.ts` | Two-phase restore: `stageArchive(rawText, userId)` validates the archive (zod), decodes uploads into `<DATA_DIR>/.system-staging/restore-<id>/`, and returns a summary; `commitRestore(stagingId)` clones the live DB, clears every application table, re-inserts archived rows in FK-safe order, then atomically swaps the live DB + uploads via `FilesystemTransactionService.stageSwap({ name: 'restore-backup', ... })`. On any failure the swap is rolled back and `SYSTEM_RESTORE_ROLLED_BACK` is returned. |
|
||||
| `backend/src/modules/admin/system/danger-zone.service.ts` | `resetScores()` deletes every `solve` row transactionally; `wipeChallenges()` snapshots `<UPLOAD_DIR>/challenges` to a side directory, deletes every challenge inside a transaction, physically removes the live `-challenges` directory post-commit, and either deletes the snapshot or restores from it on failure. |
|
||||
| `backend/src/modules/admin/system/filesystem-transaction.service.ts` | Generic file/directory swap primitive: `stageSwap({ pairs, hooks })` renames each live path to a rollback location, then renames the staged path into place (with copy+unlink cross-device fallback); `commit(handle)` removes rollback artifacts; `rollback(handle)` restores the original live paths. Provides static helpers `rmSafe`, `copyDirSync`, `ensureDir`. |
|
||||
| `backend/src/modules/admin/system/confirmation-token.service.ts` | Issues SHA-256-hashed single-use tokens with `SYSTEM_OP_CONFIRM_TOKEN_TTL_MS` TTL; `consume()` runs in a transaction with conditional `WHERE consumedAt IS NULL` updates so only the first concurrent caller succeeds. `purgeExpired()` deletes expired and >24h-old consumed rows. |
|
||||
| `backend/src/modules/admin/system/dto/admin-system.dto.ts` | Re-exports `ADMIN_OPERATION_KINDS` and zod contracts for the re-auth body, danger confirm body, and restore-commit body; declares the backup format identifier + version constants. |
|
||||
| `backend/src/database/entities/admin-operation-token.entity.ts` | TypeORM entity and the canonical `ADMIN_OPERATION_KINDS` literal union (`restore-backup`, `reset-scores`, `wipe-challenges`). |
|
||||
| `backend/src/database/migrations/1700000000900-AddAdminOperationTokens.ts` | Forward-only migration that creates the `admin_operation_token` table and its three indexes (`uq_admin_op_token_hash`, `idx_admin_op_token_user_op`, `idx_admin_op_token_expiry`). |
|
||||
| `backend/src/common/utils/upload.ts` | Adds `resolveSystemStagingDir(config)` (defaults to `<DATA_DIR>/.system-staging`, never nested under `UPLOAD_DIR`) and `getRequestSizeLimit(config)` used by the system endpoints. |
|
||||
| `backend/src/config/env.schema.ts` | Adds `SYSTEM_OP_STAGING_DIR`, `SYSTEM_OP_RESTORE_STAGE_TTL_MS` (default 15 min), `SYSTEM_OP_CONFIRM_TOKEN_TTL_MS` (default 5 min). |
|
||||
| `backend/src/common/errors/error-codes.ts` | Adds the `SYSTEM_*` error codes used by the system endpoints (`SYSTEM_BACKUP_FAILED`, `SYSTEM_RESTORE_VALIDATION_FAILED`, `SYSTEM_RESTORE_PAYLOAD_TOO_LARGE`, `SYSTEM_RESTORE_STAGE_EXPIRED`, `SYSTEM_RESTORE_FAILED`, `SYSTEM_RESTORE_ROLLED_BACK`, `SYSTEM_OPERATION_IN_PROGRESS`, `SYSTEM_REAUTH_REQUIRED`, `SYSTEM_INVALID_CREDENTIALS`, `SYSTEM_TOKEN_INVALID`, `SYSTEM_TOKEN_EXPIRED`, `SYSTEM_TOKEN_REUSED`, `SYSTEM_TOKEN_MISMATCH`, `SYSTEM_DANGER_FAILED`, `SYSTEM_DANGER_ROLLED_BACK`, `SYSTEM_FILE_READ_FAILED`, `SYSTEM_PAYLOAD_INVALID`). |
|
||||
| `backend/src/modules/auth/auth.service.ts` | Adds `reauthenticateAdmin(userId, password)` (Argon2id verify + admin/enabled status check) and `revokeAllRefreshSessions(userId, manager?)` (used after a successful restore to force the SPA back to `/login`). |
|
||||
| `tests/backend/admin-system-authorization.spec.ts` | Non-admin JWTs and missing tokens are rejected on every `/api/v1/admin/system/*` endpoint. |
|
||||
| `tests/backend/admin-system-backup.spec.ts` | Backup envelope shape, table discovery exclusion, and base64 file inclusion. |
|
||||
| `tests/backend/admin-system-confirmation-token.spec.ts` | Issue/consume/expire/reuse/mismatch flow and TTL config. |
|
||||
| `tests/backend/admin-system-danger.spec.ts` | Reset-scores and wipe-challenges happy path + DB/file rollback. |
|
||||
| `tests/backend/admin-system-restore-validation.spec.ts` | Archive schema validation, base64 size mismatch, sha256 mismatch, safe-path rejection, and duplicate-path rejection. |
|
||||
| `frontend/src/app/features/admin/system/system.component.ts` | `/admin/system` smart page: two panels (Database + Danger Zone), backup download, restore pick + validate, confirm modal trigger, and per-operation success/error surfacing + cross-store invalidation. |
|
||||
| `frontend/src/app/features/admin/system/system-confirm-modal.component.ts` | Re-authentication + confirmation-phrase modal with operation-specific copy from `system.pure.ts`. |
|
||||
| `frontend/src/app/features/admin/system/system.service.ts` | HTTP client for the six `/api/v1/admin/system/*` endpoints. |
|
||||
| `frontend/src/app/features/admin/system/system.pure.ts` | Pure helpers: error → friendly message map, `destructiveCopy(op)` per-operation modal copy, `pickRestoreFile()` (jsdom-testable file picker), `deriveBackupFilename()`. |
|
||||
| `frontend/src/app/core/services/system-data-change.service.ts` | Root-provided signal bus (`'scores-reset'`, `'challenges-wiped'`, `'restore-completed'`) consumed by `ChallengesStore` and `ScoreboardStore` to invalidate their caches after a destructive operation. |
|
||||
| `frontend/src/app/core/services/auth.service.ts` | Adds `forceServerInvalidation()` so the restore flow can drop the local session and broadcast the cross-tab invalidation without invoking the backend logout. |
|
||||
| `frontend/src/app/features/challenges/challenges.store.ts` | Now subscribes to `SystemDataChangeService` and calls `reset()` on every destructive operation kind. |
|
||||
| `frontend/src/app/features/scoreboard/scoreboard.store.ts` | Now subscribes to `SystemDataChangeService` and clears ranking/matrix/event-log/graph on every destructive operation kind. |
|
||||
| `frontend/src/app/features/admin/admin-shell.component.ts` | Side-nav entry for `system` is now `enabled: true`. |
|
||||
| `frontend/src/app/app.routes.ts` | Adds the lazy `system` child route under the admin shell. |
|
||||
| `tests/frontend/admin-system.pure.spec.ts` | Pure-helper contracts for the friendly error map, destructive copy, and file picker. |
|
||||
|
||||
# See also
|
||||
|
||||
@@ -135,3 +165,6 @@ timestamp: 2026-07-23T10:12:24Z
|
||||
- [Notifications](/guides/notifications.md)
|
||||
- [Blog Publishing and Reading](/guides/blog.md)
|
||||
- [Blog API](/api/blog.md)
|
||||
- [Admin System API](/api/admin-system.md)
|
||||
- [Admin System Page Guide](/guides/admin-system-page.md)
|
||||
- [Admin Operation Tokens Table](/database/admin-operation-tokens.md)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
type: database
|
||||
title: Admin Operation Tokens Migration
|
||||
description: Forward-only migration that adds the `admin_operation_token` table and its indexes for destructive System-operation confirmation tokens.
|
||||
resource: backend/src/database/migrations/1700000000900-AddAdminOperationTokens.ts
|
||||
tags: [database, migration, admin, system, tokens]
|
||||
timestamp: 2026-07-23T12:04:50Z
|
||||
---
|
||||
|
||||
# Purpose
|
||||
|
||||
Adds the [admin_operation_token](/database/admin-operation-tokens.md)
|
||||
table that backs the destructive operations under
|
||||
`/api/v1/admin/system/*` (restore-backup, reset-scores,
|
||||
wipe-challenges). Always fresh on a new database; on existing instances
|
||||
the migration is a no-op if the table already exists.
|
||||
|
||||
# Up
|
||||
|
||||
1. `CREATE TABLE IF NOT EXISTS admin_operation_token` with the columns
|
||||
listed in the [schema doc](/database/admin-operation-tokens.md#schema).
|
||||
2. `CREATE UNIQUE INDEX IF NOT EXISTS uq_admin_op_token_hash` on
|
||||
`token_hash` to make one-shot use enforceable.
|
||||
3. `CREATE INDEX IF NOT EXISTS idx_admin_op_token_user_op` on
|
||||
`(user_id, operation)` for fast per-admin lookup.
|
||||
4. `CREATE INDEX IF NOT EXISTS idx_admin_op_token_expiry` on
|
||||
`expires_at` so the periodic purge can use an index scan.
|
||||
|
||||
# Down
|
||||
|
||||
Drops the indexes in reverse order and drops the table.
|
||||
|
||||
# Wiring
|
||||
|
||||
The migration is registered in
|
||||
`backend/src/database/database.module.ts` after the existing user
|
||||
triggers migration. The entity is registered in the same `ENTITIES`
|
||||
array so `forFeature` lets `AdminSystemModule` inject the repository.
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
type: database
|
||||
title: Admin Operation Tokens
|
||||
description: Single-use confirmation tokens backing the destructive admin System operations (restore-backup, reset-scores, wipe-challenges).
|
||||
resource: backend/src/database/entities/admin-operation-token.entity.ts
|
||||
tags: [database, admin, system, backup, restore, danger-zone, tokens]
|
||||
timestamp: 2026-07-23T12:04:50Z
|
||||
---
|
||||
|
||||
# Schema
|
||||
|
||||
| Column | Type | Description |
|
||||
|---------------|-----------|----------------------------------------------------------------------------------------------------------|
|
||||
| `id` | TEXT PK | UUIDv4 generated client-side; the primary key. |
|
||||
| `user_id` | TEXT FK | `user.id` of the administrator who issued the token. FK `ON DELETE CASCADE` to `user`. |
|
||||
| `operation` | TEXT | One of `restore-backup`, `reset-scores`, `wipe-challenges` (mirrors `ADMIN_OPERATION_KINDS`). |
|
||||
| `token_hash` | TEXT | Hex-encoded SHA-256 of the raw token. The raw token is returned once and never logged or stored. Unique. |
|
||||
| `issued_at` | TEXT | ISO-8601 timestamp at issuance. |
|
||||
| `expires_at` | TEXT | ISO-8601 timestamp after which the token is invalid (default TTL: `SYSTEM_OP_CONFIRM_TOKEN_TTL_MS`, 5 min). |
|
||||
| `consumed_at` | TEXT null | ISO-8601 timestamp when the token was consumed; null while still valid. |
|
||||
|
||||
# Indexes
|
||||
|
||||
| Index | Purpose |
|
||||
|--------------------------------|--------------------------------------------------------|
|
||||
| `uq_admin_op_token_hash` | `UNIQUE` on `token_hash` — guarantees one-shot use. |
|
||||
| `idx_admin_op_token_user_op` | `(user_id, operation)` — fast per-admin/per-op lookup. |
|
||||
| `idx_admin_op_token_expiry` | `expires_at` — enables the periodic expiry sweep. |
|
||||
|
||||
# Lifecycle
|
||||
|
||||
1. Admin re-authenticates with their current password via
|
||||
`POST /api/v1/admin/system/confirmations`. The backend verifies the
|
||||
password with Argon2id, then `ConfirmationTokenService.issue()`
|
||||
generates a 32-byte base64url token, persists only its SHA-256 hash,
|
||||
and returns the raw token plus its `expiresAt`.
|
||||
2. The client passes the raw token (and the `stagingId` for restores)
|
||||
to the matching destructive endpoint (`/restore/commit`,
|
||||
`/scores/reset`, `/challenges/wipe`).
|
||||
3. `ConfirmationTokenService.consume()` runs the consumption inside a
|
||||
transaction: it looks the token up by hash, validates
|
||||
`user_id` + `operation` (+ `stagingId` for restores), checks
|
||||
`expires_at > now`, and finally performs a conditional update
|
||||
`WHERE consumedAt IS NULL` so only the first concurrent caller
|
||||
succeeds. A second attempt returns `SYSTEM_TOKEN_REUSED`.
|
||||
4. `purgeExpired()` deletes expired rows and consumed rows older than
|
||||
24h.
|
||||
|
||||
# Related
|
||||
|
||||
* Migration: [Admin Operation Tokens Migration](/database/admin-operation-tokens-migration.md)
|
||||
* Service: `backend/src/modules/admin/system/confirmation-token.service.ts`
|
||||
* Error codes: `SYSTEM_TOKEN_INVALID`, `SYSTEM_TOKEN_EXPIRED`,
|
||||
`SYSTEM_TOKEN_REUSED`, `SYSTEM_TOKEN_MISMATCH`
|
||||
* [Admin System API](/api/admin-system.md)
|
||||
@@ -42,6 +42,7 @@ re-asserts the unique indexes on `system_key` and `abbreviation`.
|
||||
| `solve` | User-solved challenges (scoring ledger). |
|
||||
| `refresh_token` | Rotating refresh tokens (hash + revocation). |
|
||||
| `blog_post` | Markdown blog posts in draft/published states. |
|
||||
| `admin_operation_token` | Single-use confirmation tokens for destructive System operations. |
|
||||
|
||||
# Relationships
|
||||
|
||||
@@ -68,6 +69,7 @@ user ───< refresh_token
|
||||
| `solve` | `uq_solve_challenge_user` (unique), `idx_solve_user`, `idx_solve_challenge` |
|
||||
| `refresh_token` | `idx_refresh_token_user`, unique on `token_hash` |
|
||||
| `blog_post` | `idx_blog_status_published` (`status`, `published_at`) |
|
||||
| `admin_operation_token` | `uq_admin_op_token_hash` (unique on `token_hash`), `idx_admin_op_token_user_op` (`user_id`, `operation`), `idx_admin_op_token_expiry` (`expires_at`) |
|
||||
|
||||
# PRAGMAs
|
||||
|
||||
@@ -91,3 +93,4 @@ On every process start, `DatabaseInitService.init()`:
|
||||
- [Challenge Tables](/database/challenges.md)
|
||||
- [Auth and Settings Tables](/database/auth-settings.md)
|
||||
- [Blog Post Table](/database/blog-posts.md)
|
||||
- [Admin Operation Tokens Table](/database/admin-operation-tokens.md)
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
---
|
||||
type: guide
|
||||
title: Admin — System (Backup, Restore, Danger Zone)
|
||||
description: How an admin navigates to /admin/system, creates a backup, stages and commits a restore, resets all scores, and wipes challenges — including the re-authentication confirmation, progress, and side effects.
|
||||
tags: [guide, admin, system, backup, restore, danger-zone, confirmation]
|
||||
timestamp: 2026-07-23T12:04:50Z
|
||||
---
|
||||
|
||||
# Navigation
|
||||
|
||||
1. Sign in as an admin and open the admin side-nav (see the
|
||||
[Admin Shell guide](/guides/admin-shell.md)).
|
||||
2. Click **System** in the side-nav. The route resolves to
|
||||
`/admin/system` and lazily loads the
|
||||
`AdminSystemComponent` standalone component.
|
||||
3. The page renders two panels side by side: **Database** (left) and
|
||||
**Danger Zone** (right). On narrow screens they stack vertically.
|
||||
|
||||
# Database panel
|
||||
|
||||
## Create a backup
|
||||
|
||||
1. Click **Create backup** (data-testid `admin-system-create-backup`).
|
||||
A spinner appears inside the button while the request is in flight.
|
||||
2. The browser receives a JSON file via `Content-Disposition:
|
||||
attachment; filename="hipctf-backup-YYYY-MM-DD.json"` and the
|
||||
browser download UI saves it to disk.
|
||||
3. On success the panel shows a green **Backup ready.** status line
|
||||
(data-testid `admin-system-backup-success`).
|
||||
4. On failure the panel shows a red error message under the buttons
|
||||
(data-testid `admin-system-backup-error`). The friendly message
|
||||
comes from `system.pure.ts` and resolves `SYSTEM_BACKUP_FAILED`,
|
||||
`SYSTEM_FILE_READ_FAILED`, `UNAUTHORIZED`, `FORBIDDEN`, etc.
|
||||
|
||||
## Restore a backup
|
||||
|
||||
1. Click **Restore backup** (data-testid `admin-system-restore-backup`).
|
||||
A hidden file picker opens restricted to `application/json`/`.json`.
|
||||
2. Pick a backup JSON file. The progress line shows
|
||||
**Reading selected backup…**, then **Validating backup
|
||||
server-side…** (data-testid `admin-system-restore-progress`).
|
||||
3. The server validates the JSON, decodes uploads, and verifies
|
||||
sizes/checksums. On success a summary card appears
|
||||
(data-testid `admin-system-restore-summary`) showing:
|
||||
* Tables detected
|
||||
* Uploaded files
|
||||
* Total bytes
|
||||
* Stage expiry (ISO timestamp)
|
||||
4. From the summary card, the admin has two options:
|
||||
* **Discard staged backup** (data-testid
|
||||
`admin-system-discard-restore`) — drops the staged state and
|
||||
resumes from a clean panel.
|
||||
* **Continue to destructive restore…** (data-testid
|
||||
`admin-system-confirm-restore`) — opens the confirmation modal.
|
||||
|
||||
### Confirmation modal (restore-backup)
|
||||
|
||||
* The modal title is **Restore database?**.
|
||||
* The detail paragraph explains that the entire database and uploads
|
||||
will be replaced (the modal's `preserved` line says **Nothing is
|
||||
preserved**).
|
||||
* The stage banner explicitly warns: *You will be logged out after
|
||||
restore completes.*
|
||||
* The admin must enter their current password and type
|
||||
`RESTORE BACKUP` exactly into the confirmation phrase field.
|
||||
* Click **Confirm**. The modal swaps the input fields for a
|
||||
*Working…* spinner.
|
||||
* The frontend calls `POST /confirmations` with `{ operation:
|
||||
'restore-backup', password, stagingId }` to receive a single-use
|
||||
token, then immediately calls `POST /restore/commit` with that
|
||||
token and the staging id.
|
||||
* On success the SPA calls `AuthService.forceServerInvalidation()`,
|
||||
resets the `UserStore`, and navigates to `/login`. The
|
||||
`ChallengesStore` and `ScoreboardStore` invalidate themselves via
|
||||
the `SystemDataChangeService` `'restore-completed'` event.
|
||||
* On failure the modal stays open and shows the resolved error
|
||||
(e.g. `SYSTEM_TOKEN_MISMATCH`, `SYSTEM_RESTORE_ROLLED_BACK`). The
|
||||
database panel also reflects the error inline.
|
||||
|
||||
# Danger Zone panel
|
||||
|
||||
The danger zone panel is outlined by a dashed red border and contains
|
||||
two red buttons:
|
||||
|
||||
| Button | Operation |
|
||||
|--------------------------------------------|---------------------|
|
||||
| `admin-system-reset-scores` | `reset-scores` |
|
||||
| `admin-system-wipe-challenges` | `wipe-challenges` |
|
||||
|
||||
Each button opens the same confirmation modal with operation-specific
|
||||
copy:
|
||||
|
||||
* **Reset all scores?** — detail lists what is removed
|
||||
(every solve and award), preserved (users, sessions, roles,
|
||||
challenges, categories, etc.). Confirmation phrase: `RESET SCORES`.
|
||||
* **Wipe all challenges?** — detail explains that every challenge, its
|
||||
attached files, every solve, and every award are removed, and that
|
||||
every uploaded challenge file on disk is deleted. Preserved:
|
||||
users, sessions, roles, categories, settings, blog posts, and
|
||||
root-level uploads (e.g. the site logo). Confirmation phrase:
|
||||
`WIPE CHALLENGES`.
|
||||
|
||||
After the admin enters their password and types the confirmation
|
||||
phrase, the SPA re-authenticates to issue a token and then commits the
|
||||
operation. The panel shows a green summary
|
||||
(`N solve record(s) removed.` or `N challenge(s), M file(s) wiped.`)
|
||||
and a toast notification. The corresponding stores
|
||||
(`ChallengesStore`, `ScoreboardStore`) automatically reset themselves
|
||||
via the `SystemDataChangeService` `scores-reset` and `challenges-wiped`
|
||||
events so every open tab sees fresh data.
|
||||
|
||||
# Cross-tab invalidation
|
||||
|
||||
After any of the destructive operations, the
|
||||
`SystemDataChangeService` notifies all subscribers in the same tab via
|
||||
an Angular signal. The injected effects in `ChallengesStore` and
|
||||
`ScoreboardStore` reset their cached board/matrix/event-log/graph
|
||||
data. For a restore, the SPA additionally forces a cross-tab
|
||||
invalidation by calling `AuthService.forceServerInvalidation()`,
|
||||
which clears the access token and notifies peer tabs via
|
||||
`BroadcastChannel` + the `storage`-event fallback so they redirect to
|
||||
`/login` as well.
|
||||
|
||||
# Error mapping
|
||||
|
||||
The pure helper `friendlySystemErrorMessage(op, err)` in
|
||||
`system.pure.ts` converts the backend error codes into user-facing
|
||||
messages. Notable mappings:
|
||||
|
||||
| Backend code | UI message |
|
||||
|---------------------------------------|----------------------------------------------------------------------------|
|
||||
| `SYSTEM_REAUTH_REQUIRED` | "Your session is no longer valid. Please log in again." |
|
||||
| `SYSTEM_INVALID_CREDENTIALS` | "The current password is incorrect." |
|
||||
| `SYSTEM_TOKEN_INVALID` / `_EXPIRED` | "Confirmation token is invalid / expired. Re-authenticate and try again." |
|
||||
| `SYSTEM_TOKEN_REUSED` | "Confirmation token was already used. Re-authenticate to get a fresh token." |
|
||||
| `SYSTEM_TOKEN_MISMATCH` | "Confirmation token does not match this operation." |
|
||||
| `SYSTEM_RESTORE_VALIDATION_FAILED` | "Backup validation failed. No data was changed." |
|
||||
| `SYSTEM_RESTORE_PAYLOAD_TOO_LARGE` | "The backup is larger than the configured upload limit." |
|
||||
| `SYSTEM_RESTORE_STAGE_EXPIRED` | "The backup staging window expired. Re-upload and validate again." |
|
||||
| `SYSTEM_RESTORE_ROLLED_BACK` | "Restore failed and was rolled back. Your data is unchanged." |
|
||||
| `SYSTEM_DANGER_ROLLED_BACK` | "Operation failed and was rolled back. Nothing was changed." |
|
||||
| `SYSTEM_BACKUP_FAILED` | "Backup could not be generated. No data was changed." |
|
||||
| `SYSTEM_DANGER_FAILED` | "Operation could not complete. No data was changed." |
|
||||
| `SYSTEM_OPERATION_IN_PROGRESS` | "Another destructive operation is currently running." |
|
||||
|
||||
# Examples
|
||||
|
||||
## Manual restore happy path
|
||||
|
||||
1. Download the backup from a known-good environment.
|
||||
2. Sign in as the admin on the target environment.
|
||||
3. Navigate to `/admin/system`, click **Restore backup**, select the
|
||||
downloaded file.
|
||||
4. Verify the summary matches the expected table counts and file
|
||||
counts.
|
||||
5. Click **Continue to destructive restore…**, enter the admin
|
||||
password, type `RESTORE BACKUP`, click **Confirm**.
|
||||
6. The modal closes, the page shows a brief *Restore complete.
|
||||
Logging out…* toast, and the SPA navigates to `/login`.
|
||||
7. Sign in again with the same admin credentials. The data on the
|
||||
target environment now matches the source backup.
|
||||
|
||||
## Manual reset happy path
|
||||
|
||||
1. Navigate to `/admin/system`.
|
||||
2. Click **Reset all scores**, enter the password, type
|
||||
`RESET SCORES`, click **Confirm**.
|
||||
3. The danger zone panel shows the number of solve records removed
|
||||
and a toast notifies the operation completed. Refresh the
|
||||
Scoreboard page — every player is back at zero.
|
||||
|
||||
## Manual wipe happy path
|
||||
|
||||
1. Navigate to `/admin/system`.
|
||||
2. Click **Wipe challenges**, enter the password, type
|
||||
`WIPE CHALLENGES`, click **Confirm**.
|
||||
3. The danger zone panel shows counts of challenges, files, and
|
||||
solves removed. The Challenges page becomes empty and the
|
||||
`/admin/challenges` list is empty. The `challenges/` folder on
|
||||
disk is also gone.
|
||||
+14
-1
@@ -12,7 +12,7 @@ controls, administrator-managed Markdown blog publishing, and public and
|
||||
signed-in blog views.
|
||||
|
||||
The docs below are organized by purpose so agents can pull just the slice
|
||||
they need. Last regenerated 2026-07-23T10:36:45Z.
|
||||
they need. Last regenerated 2026-07-23T12:04:50Z.
|
||||
|
||||
# Architecture
|
||||
|
||||
@@ -43,6 +43,11 @@ they need. Last regenerated 2026-07-23T10:36:45Z.
|
||||
* [Auth and Settings Tables](/database/auth-settings.md) - `refresh_token`
|
||||
and `setting` tables.
|
||||
* [Blog Post Table](/database/blog-posts.md) - `blog_post` table.
|
||||
* [Admin Operation Tokens Table](/database/admin-operation-tokens.md) -
|
||||
Single-use confirmation tokens for destructive System operations.
|
||||
* [Admin Operation Tokens Migration](/database/admin-operation-tokens-migration.md) -
|
||||
Forward-only migration that adds the `admin_operation_token` table
|
||||
and its indexes.
|
||||
|
||||
# API
|
||||
|
||||
@@ -63,6 +68,9 @@ they need. Last regenerated 2026-07-23T10:36:45Z.
|
||||
`/events/status` SSE stream.
|
||||
* [Uploads Endpoints](/api/uploads.md) - Admin-only multipart uploads for category icons, challenge files, and validated site logos.
|
||||
* [Blog Endpoints](/api/blog.md) - Public published-post listing and admin-only post CRUD, validation, and publication behavior.
|
||||
* [Admin System Endpoints](/api/admin-system.md) - Admin-only backup
|
||||
download, restore validate/commit, re-authenticated confirmation
|
||||
tokens, reset-scores, and wipe-challenges.
|
||||
|
||||
# Guides
|
||||
|
||||
@@ -87,6 +95,11 @@ they need. Last regenerated 2026-07-23T10:36:45Z.
|
||||
* [Admin — Challenges Full-Replace Import](/guides/admin-challenges-import.md) -
|
||||
Transactional full-replace semantics, side effects, and the auto-close
|
||||
contract of the confirmed challenges import on `/admin/challenges`.
|
||||
* [Admin — System (Backup, Restore, Danger Zone)](/guides/admin-system-page.md) -
|
||||
How an admin navigates to `/admin/system`, creates a backup, stages
|
||||
and commits a restore, resets all scores, and wipes challenges,
|
||||
including the re-authentication confirmation flow and cross-tab
|
||||
invalidation.
|
||||
* [Authenticated Shell](/guides/authenticated-shell.md) - The header, LED
|
||||
+ countdown, quick-jump tabs, user menu, and SSE lifecycle that frame
|
||||
every signed-in page.
|
||||
|
||||
Reference in New Issue
Block a user