|
|
|
@@ -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{<token>}` 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<system_key, id>`. 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.
|