56 lines
3.3 KiB
Markdown
56 lines
3.3 KiB
Markdown
---
|
|
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)
|