AI Implementation feature(918): Admin Area System: Database Backup/Restore and Danger Zone 1.01 #63
@@ -4,7 +4,7 @@ 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
|
||||
timestamp: 2026-07-23T13:27:25Z
|
||||
---
|
||||
|
||||
# Overview
|
||||
@@ -130,6 +130,31 @@ Flow:
|
||||
database + uploads are swapped atomically via
|
||||
`FilesystemTransactionService.stageSwap()` (with a `restore-backup`
|
||||
rollback handle).
|
||||
|
||||
The offline clone rebuild begins by capturing the exact SQL of the
|
||||
deployment-wide `trg_user_last_admin_update` and
|
||||
`trg_user_last_admin_delete` triggers from `sqlite_master`,
|
||||
dropping them in the candidate database, then clearing the
|
||||
application tables and re-inserting the archived rows. SQLite
|
||||
triggers fire regardless of `PRAGMA foreign_keys = OFF`, so
|
||||
clearing the sole admin row before the archive's admin is inserted
|
||||
would otherwise abort the rebuild with `LAST_ADMIN`. The suspension
|
||||
is confined to the offline candidate file. Before the candidate is
|
||||
eligible to swap, the rebuild:
|
||||
|
||||
- asserts at least one `user.role = 'admin'` row exists in the
|
||||
rebuilt data (zero-admin archives fail with
|
||||
`SYSTEM_RESTORE_ROLLED_BACK` so the live system never lands in a
|
||||
no-admin state, even momentarily),
|
||||
- re-creates both captured triggers via `db.exec(sql)` so the
|
||||
`LAST_ADMIN` invariant is restored on the candidate, and
|
||||
- re-enables `PRAGMA foreign_keys` and runs `PRAGMA foreign_key_check`.
|
||||
|
||||
On any rebuild, integrity, swap, or verification error the capture
|
||||
loop is wrapped in a `try/catch` that best-effort re-creates any
|
||||
missing trigger before rethrowing, so a rolled-back candidate never
|
||||
leaks to disk missing the `LAST_ADMIN` safety net. The original
|
||||
failure is then rethrown unchanged.
|
||||
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
|
||||
@@ -288,4 +313,4 @@ curl -sS ... -d '{"operation":"reset-scores","token":"<raw>"}'
|
||||
| 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` |
|
||||
| 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`, `admin-system-restore-commit.spec.ts`; `tests/frontend/admin-system.pure.spec.ts` |
|
||||
|
||||
@@ -3,7 +3,7 @@ type: architecture
|
||||
title: Key Files Index
|
||||
description: One-line responsibility for important source and contract-test files, including event-window, challenge, scoreboard, session, notification, and blog flows.
|
||||
tags: [architecture, key-files, event-window, validation, default-challenge-ip, sse, bootstrap, migrations, challenges, import, board, notifications, per-user, session, blog]
|
||||
timestamp: 2026-07-23T10:12:24Z
|
||||
timestamp: 2026-07-23T13:27:25Z
|
||||
---
|
||||
|
||||
# Backend
|
||||
@@ -126,7 +126,7 @@ timestamp: 2026-07-23T10:12:24Z
|
||||
| `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/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, captures and temporarily drops the `trg_user_last_admin_update` / `trg_user_last_admin_delete` triggers in the offline candidate, clears every application table, re-inserts archived rows in FK-safe order, asserts the archive still contains at least one admin, re-creates the captured triggers, 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; the rebuild also best-effort re-creates any missing trigger before rethrowing so a rolled-back candidate never leaks missing the `LAST_ADMIN` safety net. |
|
||||
| `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. |
|
||||
@@ -142,6 +142,7 @@ timestamp: 2026-07-23T10:12:24Z
|
||||
| `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. |
|
||||
| `tests/backend/admin-system-restore-commit.spec.ts` | Controller forwards `stagingId` to `RestoreService.commitRestore`, plus a real-file service-level regression that restores a backup containing the same admin as the live system, asserts swapped table settings + uploads + trigger preservation, and verifies an admin-less archive is rejected with `SYSTEM_RESTORE_ROLLED_BACK`. |
|
||||
| `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. |
|
||||
|
||||
+11
-1
@@ -3,7 +3,7 @@ type: database
|
||||
title: User Table
|
||||
description: The `user` table — accounts, roles, and statuses.
|
||||
tags: [database, user, auth]
|
||||
timestamp: 2026-07-21T17:37:18Z
|
||||
timestamp: 2026-07-23T13:27:25Z
|
||||
---
|
||||
|
||||
# Schema
|
||||
@@ -31,6 +31,16 @@ timestamp: 2026-07-21T17:37:18Z
|
||||
demote or delete the last admin are rejected by
|
||||
`UsersService.enforceLastAdminInvariant` (used by
|
||||
`AdminService.updateUserRole` and `AdminService.deleteUser`).
|
||||
- A deployment-wide database safety net backs the invariant up at the
|
||||
SQLite level via two triggers created by the
|
||||
`AddUserLastAdminTriggers1700000000800` migration:
|
||||
`trg_user_last_admin_update` (fires on `UPDATE OF role` when the old
|
||||
row was admin, the new role is not, and no other admin row exists)
|
||||
and `trg_user_last_admin_delete` (fires on `DELETE` of an admin row
|
||||
when no other admin row exists). Both abort the offending statement
|
||||
with `RAISE(ABORT, 'LAST_ADMIN')`. They are recreated by the restore
|
||||
rebuild after the candidate database is rebuilt — see
|
||||
[Admin System Endpoints — POST /restore/commit](/api/admin-system.md).
|
||||
|
||||
# First-admin bootstrap
|
||||
|
||||
|
||||
+1
-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-23T12:04:50Z.
|
||||
they need. Last regenerated 2026-07-23T13:27:25Z.
|
||||
|
||||
# Architecture
|
||||
|
||||
|
||||
Reference in New Issue
Block a user