diff --git a/docs/api/challenges.md b/docs/api/challenges.md index 345028c..f904bf8 100644 --- a/docs/api/challenges.md +++ b/docs/api/challenges.md @@ -85,6 +85,11 @@ column are sorted by `difficulty` (`LOW < MEDIUM < HIGH`) then by name. Only `enabled = 1` rows are returned. The query joins `challenge` and `category` once and groups in memory. +On a fresh database the `SeedSampleChallenges1700000000600` +migration populates the board with a representative sample set +spanning every canonical system category so the page renders cards +without any manual admin action. + # `GET /api/v1/challenges/status` Authenticated event-window snapshot (the same `EventStatePayload` diff --git a/docs/database/challenges.md b/docs/database/challenges.md index 4bd5342..9b379a9 100644 --- a/docs/database/challenges.md +++ b/docs/database/challenges.md @@ -56,6 +56,14 @@ canonical enums, the `enabled` flag, `updated_at`, the category FK stays at `ON DELETE RESTRICT` so the `CATEGORY_HAS_CHALLENGES` business rule is preserved. +The `SeedSampleChallenges1700000000600` migration inserts a small +representative set of sample challenges (one or more per canonical +system category) on a fresh database so the `/challenges` board has +cards immediately after bootstrap. It is idempotent: if the +`challenge` table already contains rows (e.g. an admin imported a +custom challenge set), it does nothing. Sample flags follow the +`flag{}` convention and are exposed only inside admin views. + ## `challenge_file` | Column | Type | Description | diff --git a/docs/database/seed-sample-challenges-migration.md b/docs/database/seed-sample-challenges-migration.md new file mode 100644 index 0000000..bb12412 --- /dev/null +++ b/docs/database/seed-sample-challenges-migration.md @@ -0,0 +1,133 @@ +--- +type: database +title: Seed Sample Challenges Migration (1700000000600) +description: Forward-only migration that inserts a representative sample set of challenges on a fresh database so the /challenges board renders cards without manual admin action. +tags: [database, migration, challenge, seed, sample] +timestamp: 2026-07-23T02:23:34Z +--- + +# Purpose + +A brand-new HIPCTF database (default path +[`./data/db.sqlite`](/database/schema.md)) has no `challenge` rows until +an admin imports or creates them. The +`SeedSampleChallenges1700000000600` migration bridges that gap by +inserting a small, representative sample set so the authenticated +`/challenges` board (see [Challenges Board](/guides/challenges-board.md)) +already has cards on the very first visit and the public landing can +demonstrate the platform with real content. + +The migration runs at the end of the `MIGRATIONS` array in +[`DatabaseModule`](/architecture/backend-modules.md) so every other +category / challenge schema migration has already landed and the +`category` rows it joins against are present. + +# File + +`backend/src/database/migrations/1700000000600-SeedSampleChallenges.ts` + +Registered last in `backend/src/database/database.module.ts` so it +runs after `UpgradeChallengeAdminSchema1700000000500`. + +# Sample set + +Eight challenges, one or more per canonical system category. Source +of truth is the typed `SAMPLE_CHALLENGES` array exported by the +migration: + +| Name | `system_key` | Difficulty | `initial_points` | `minimum_points` | `decay_solves` | Flag | +|-------------------|--------------|------------|------------------|------------------|----------------|--------------| +| Alpha Cipher | `CRY` | `LOW` | 200 | 100 | 5 | `flag{alpha}` | +| Beta Cipher | `CRY` | `MEDIUM` | 300 | 100 | 10 | `flag{beta}` | +| Zeta Key | `CRY` | `HIGH` | 500 | 100 | 10 | `flag{zeta}` | +| Web Welcome | `WEB` | `LOW` | 100 | 50 | 10 | `flag{web1}` | +| Mobile Mayhem | `PWN` | `MEDIUM` | 300 | 100 | 10 | `flag{mob1}` | +| Hardware Hello | `HW` | `LOW` | 100 | 50 | 10 | `flag{hw1}` | +| Markdown Mystery | `MSC` | `LOW` | 100 | 50 | 10 | `flag{md}` | +| Reverse Ranger | `REV` | `MEDIUM` | 300 | 100 | 10 | `flag{rev1}` | + +All rows are inserted with `protocol='WEB'`, `port=NULL`, +`ip_address=''`, and `enabled=1` so they appear on the board the +moment the event window opens (see [Event Window](/guides/event-window.md)). +Categories are resolved by `system_key` rather than UUID so the seed +works regardless of the UUIDs the category migration assigned at +install time. Sample flags follow the `flag{}` convention and +are exposed only inside admin views — the public challenge DTO +continues to strip them (see [Challenges Endpoints](/api/challenges.md)). + +# What the `up()` does + +1. **Idempotency guard** — `SELECT COUNT(*) FROM "challenge"`. If the + table already contains rows the migration returns immediately; an + admin that imported a custom challenge set first is never + overwritten. +2. **System-category lookup** — + `SELECT id, system_key FROM "category" WHERE system_key IS NOT NULL` + and builds an in-memory `Map`. If fewer than six + canonical keys are present (the seed cannot bind) the migration + aborts as a no-op rather than inserting challenges into the wrong + category. +3. **Insert sample rows** — for each entry in `SAMPLE_CHALLENGES`, an + `INSERT INTO "challenge"` with a fresh UUID v4, ISO 8601 + `created_at` / `updated_at` via `strftime('%Y-%m-%dT%H:%M:%fZ','now')`, + and `enabled=1`. Categories without a matching `system_key` are + silently skipped (defensive — only happens if the canonical six + are missing). + +# Idempotency + +* Running the migration twice (or restarting after it has been + recorded) leaves the same eight rows. The early-return count + guard skips the second run. +* Re-running the migration after a custom `challenge` row exists also + no-ops, so manually-imported content from the + [Admin — Challenges Full-Replace Import](/guides/admin-challenges-import.md) + flow is preserved. + +# `down()` + +Scoped to the seeded set: `DELETE FROM "challenge" WHERE "name" IN (...)` +using the eight fixed names from `SAMPLE_CHALLENGES`. It deliberately +matches **only** the sample names so unrelated admin rows are not +removed. + +# Tests + +| File | What it asserts | +|---|---| +| `tests/backend/migrations.spec.ts` — *"seeds sample challenges on a fresh database (Job 906)"* | After all migrations run on an in-memory SQLite DB, the eight expected sample names are present. | +| `tests/backend/migrations.spec.ts` — *"seeded sample challenges are enabled, schema-compatible, and point at the right categories (Job 906)"* | Every seeded row has `enabled=true`, a valid `difficulty`, `initial_points >= minimum_points`, a non-empty `flag`, and a `category_id` that resolves to one of the six canonical system categories. | +| `tests/backend/migrations.spec.ts` — *"SeedSampleChallenges down() scope (Job 906)"* | `down()` removes only rows whose name is in the seed list and leaves manually-inserted rows of similar names (e.g. `Other`) intact. | +| `tests/backend/challenges-board.spec.ts` | Board-listing test now allows extra seeded cards inside the CRY column (`Alpha Cipher`, `Beta Cipher`, `Zeta Key`) while still asserting that the hand-inserted `alphacipher` LOW and `Zeta-key` HIGH rows occupy the bookended slots and that the disabled `hidden` row is excluded. | + +# Operational notes + +* On a fresh database the seed fires before the first app boot, so + [`GET /api/v1/challenges/board`](/api/challenges.md) returns the + sample columns immediately and the player-facing [Challenges Board](/guides/challenges-board.md) + shows eight cards. An admin deleting every challenge is the only + way to reach an empty board. +* To confirm on the command line after `npm run setup`: + ``` + sqlite3 ./data/db.sqlite 'SELECT COUNT(*) FROM challenge;' + ``` + Expected count: `8`. Re-running setup on a populated DB leaves the + count unchanged (idempotent guard). +* Played flags are usable for end-to-end demos: e.g. submitting + `flag{alpha}` against the Alpha Cipher card immediately awards + points and broadcasts the solve on the + [Scoreboard Stream](/guides/scoreboard-stream.md). + +# See also + +* [Database Schema Overview](/database/schema.md) — full migration list + and bootstrap behavior. +* [Challenge Tables](/database/challenges.md) — `challenge` schema, + indexes, and provenance. +* [Challenges Endpoints](/api/challenges.md) — board query and flag + submission. +* [Challenges Board](/guides/challenges-board.md) — user-facing guide. +* [Admin — Challenges](/guides/admin-challenges.md) — how an admin + replaces or extends the sample set. +* [Category Repair Migration](/database/category-repair-migration.md) + — earlier canonical-six migration this seed depends on. diff --git a/docs/guides/challenges-board.md b/docs/guides/challenges-board.md index 81f7abc..4548ea0 100644 --- a/docs/guides/challenges-board.md +++ b/docs/guides/challenges-board.md @@ -10,7 +10,14 @@ timestamp: 2026-07-23T00:10:00Z `/challenges` is rendered by `ChallengesPage` (`frontend/src/app/features/challenges/challenges.page.ts`) inside the -authenticated shell. The page is gated by: +authenticated shell. + +On a fresh database the `SeedSampleChallenges1700000000600` +migration loads a representative sample set of challenges so the +board has cards the moment an admin configures the event window; an +empty board is only possible when the admin deleted every challenge. + +The page is gated by: | Gate | Where | |---------------------------------------|--------------------------------------------------| diff --git a/docs/index.md b/docs/index.md index 56f24fa..5ee4244 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,7 +11,7 @@ scoreboard, an event window with a public countdown, theming, and admin controls. The docs below are organized by purpose so agents can pull just the slice -they need. Last regenerated 2026-07-23T00:10:00Z. +they need. Last regenerated 2026-07-23T02:23:34Z. # Architecture @@ -35,6 +35,10 @@ they need. Last regenerated 2026-07-23T00:10:00Z. Forward-only migration that adds `created_at`/`updated_at` to legacy `category` tables and reconciles system rows to the canonical CRY/HW/MSC/PWN/REV/WEB set. +* [Seed Sample Challenges Migration](/database/seed-sample-challenges-migration.md) - + Forward-only migration that inserts a representative sample set of + challenges on a fresh database so the `/challenges` board has cards + immediately, with idempotency guards and scoped `down()`. * [Auth and Settings Tables](/database/auth-settings.md) - `refresh_token` and `setting` tables. * [Blog Post Table](/database/blog-posts.md) - `blog_post` table.