AI Implementation feature(906): Challenges Page and Challenge Solve Modal 1.02 (#48)

This commit was merged in pull request #48.
This commit is contained in:
2026-07-23 02:24:44 +00:00
parent cbfa9c35ca
commit f194641e8c
11 changed files with 506 additions and 133 deletions
+2
View File
@@ -17,6 +17,7 @@ import { AddCategoryTimestampsAndUniqueAbbrev1700000000200 } from './migrations/
import { UpdateSystemCategoryKeys1700000000300 } from './migrations/1700000000300-UpdateSystemCategoryKeys';
import { RepairCategorySchemaAndSystemCategories1700000000400 } from './migrations/1700000000400-RepairCategorySchemaAndSystemCategories';
import { UpgradeChallengeAdminSchema1700000000500 } from './migrations/1700000000500-UpgradeChallengeAdminSchema';
import { SeedSampleChallenges1700000000600 } from './migrations/1700000000600-SeedSampleChallenges';
import { DatabaseInitService } from './database-init.service';
const ENTITIES = [
@@ -37,6 +38,7 @@ const MIGRATIONS = [
UpdateSystemCategoryKeys1700000000300,
RepairCategorySchemaAndSystemCategories1700000000400,
UpgradeChallengeAdminSchema1700000000500,
SeedSampleChallenges1700000000600,
];
@Global()
@@ -0,0 +1,168 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import { v4 as uuid } from 'uuid';
interface SampleChallenge {
name: string;
systemKey: 'CRY' | 'HW' | 'MSC' | 'PWN' | 'REV' | 'WEB';
descriptionMd: string;
difficulty: 'LOW' | 'MEDIUM' | 'HIGH';
initialPoints: number;
minimumPoints: number;
decaySolves: number;
flag: string;
}
const SAMPLE_CHALLENGES: SampleChallenge[] = [
{
name: 'Alpha Cipher',
systemKey: 'CRY',
descriptionMd: '# Alpha Cipher\n\nA gentle warm-up for the cryptography track.',
difficulty: 'LOW',
initialPoints: 200,
minimumPoints: 100,
decaySolves: 5,
flag: 'flag{alpha}',
},
{
name: 'Beta Cipher',
systemKey: 'CRY',
descriptionMd: '# Beta Cipher\n\nClassical substitution with a twist.',
difficulty: 'MEDIUM',
initialPoints: 300,
minimumPoints: 100,
decaySolves: 10,
flag: 'flag{beta}',
},
{
name: 'Zeta Key',
systemKey: 'CRY',
descriptionMd: '# Zeta Key\n\nA tougher modular arithmetic problem.',
difficulty: 'HIGH',
initialPoints: 500,
minimumPoints: 100,
decaySolves: 10,
flag: 'flag{zeta}',
},
{
name: 'Web Welcome',
systemKey: 'WEB',
descriptionMd: '# Web Welcome\n\nInspect the page source to find the flag.',
difficulty: 'LOW',
initialPoints: 100,
minimumPoints: 50,
decaySolves: 10,
flag: 'flag{web1}',
},
{
name: 'Mobile Mayhem',
systemKey: 'PWN',
descriptionMd: '# Mobile Mayhem\n\nExploit a small userspace service.',
difficulty: 'MEDIUM',
initialPoints: 300,
minimumPoints: 100,
decaySolves: 10,
flag: 'flag{mob1}',
},
{
name: 'Hardware Hello',
systemKey: 'HW',
descriptionMd: '# Hardware Hello\n\nRead the serial console output.',
difficulty: 'LOW',
initialPoints: 100,
minimumPoints: 50,
decaySolves: 10,
flag: 'flag{hw1}',
},
{
name: 'Markdown Mystery',
systemKey: 'MSC',
descriptionMd: '# Markdown Mystery\n\nA flag is hidden somewhere in this README.',
difficulty: 'LOW',
initialPoints: 100,
minimumPoints: 50,
decaySolves: 10,
flag: 'flag{md}',
},
{
name: 'Reverse Ranger',
systemKey: 'REV',
descriptionMd: '# Reverse Ranger\n\nDisassemble the binary and recover the key.',
difficulty: 'MEDIUM',
initialPoints: 300,
minimumPoints: 100,
decaySolves: 10,
flag: 'flag{rev1}',
},
];
/**
* Seeds a small, representative set of sample challenges so a fresh
* database immediately has a populated board (Job 906). Idempotent: if
* the `challenge` table already contains any rows, the migration is a
* no-op so manually-imported challenges are never overwritten.
*
* Categories are looked up by `system_key` rather than UUID so the seed
* works regardless of the UUIDs the category migration assigned at
* install time.
*/
export class SeedSampleChallenges1700000000600 implements MigrationInterface {
name = 'SeedSampleChallenges1700000000600';
public async up(queryRunner: QueryRunner): Promise<void> {
const countRows: any[] = await queryRunner.query(`SELECT COUNT(*) AS c FROM "challenge"`);
const existing = Number(countRows?.[0]?.c ?? 0);
if (existing > 0) {
return;
}
const catRows: any[] = await queryRunner.query(
`SELECT "id", "system_key" FROM "category" WHERE "system_key" IS NOT NULL`,
);
const idByKey = new Map<string, string>();
for (const row of catRows) {
if (row?.system_key && row?.id) {
idByKey.set(String(row.system_key), String(row.id));
}
}
if (idByKey.size < 6) {
return;
}
for (const sample of SAMPLE_CHALLENGES) {
const categoryId = idByKey.get(sample.systemKey);
if (!categoryId) continue;
await queryRunner.query(
`INSERT INTO "challenge" (
"id","name","description_md","category_id","difficulty",
"initial_points","minimum_points","decay_solves","flag","protocol",
"port","ip_address","enabled","created_at","updated_at"
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,strftime('%Y-%m-%dT%H:%M:%fZ','now'),strftime('%Y-%m-%dT%H:%M:%fZ','now'))`,
[
uuid(),
sample.name,
sample.descriptionMd,
categoryId,
sample.difficulty,
sample.initialPoints,
sample.minimumPoints,
sample.decaySolves,
sample.flag,
'WEB',
null,
'',
1,
],
);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
const names = SAMPLE_CHALLENGES.map((s) => s.name);
if (names.length === 0) return;
const placeholders = names.map(() => '?').join(',');
await queryRunner.query(
`DELETE FROM "challenge" WHERE "name" IN (${placeholders})`,
names,
);
}
}