AI Implementation feature(871): Admin Area System: Database Backup/Restore and Danger Zone (#61)
This commit was merged in pull request #61.
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user