feat: Admin Area General Settings and Categories 1.11

This commit is contained in:
OpenVelo Agent
2026-07-22 16:45:59 +00:00
parent b02369dd41
commit 9a6c805525
8 changed files with 431 additions and 91 deletions
@@ -85,6 +85,22 @@ export class DatabaseInitService implements OnApplicationBootstrap {
if (categoryCount < 6) {
this.logger.warn(`Expected at least 6 seeded categories, found ${categoryCount}`);
}
try {
const systemRows: any[] = await this.dataSource.query(
`SELECT system_key, abbreviation FROM "category" WHERE "system_key" IS NOT NULL ORDER BY abbreviation`,
);
const expected = ['CRY', 'HW', 'MSC', 'PWN', 'REV', 'WEB'];
const present = new Set(systemRows.map((r) => String(r.system_key)));
const missing = expected.filter((k) => !present.has(k));
const duplicateSystemKeys = systemRows.length - new Set(systemRows.map((r) => String(r.system_key))).size;
if (missing.length > 0 || duplicateSystemKeys > 0) {
this.logger.warn(
`Canonical system categories mismatch: missing=[${missing.join(',')}] duplicateSystemRows=${duplicateSystemKeys}`,
);
}
} catch (e) {
this.logger.warn(`verifySeed system-category check skipped: ${(e as Error).message}`);
}
}
private async hasCategoryTable(): Promise<boolean> {
+2
View File
@@ -15,6 +15,7 @@ import { InitSchema1700000000000 } from './migrations/1700000000000-InitSchema';
import { SeedSystemData1700000000100 } from './migrations/1700000000100-SeedSystemData';
import { AddCategoryTimestampsAndUniqueAbbrev1700000000200 } from './migrations/1700000000200-AddCategoryTimestampsAndUniqueAbbrev';
import { UpdateSystemCategoryKeys1700000000300 } from './migrations/1700000000300-UpdateSystemCategoryKeys';
import { RepairCategorySchemaAndSystemCategories1700000000400 } from './migrations/1700000000400-RepairCategorySchemaAndSystemCategories';
import { DatabaseInitService } from './database-init.service';
const ENTITIES = [
@@ -33,6 +34,7 @@ const MIGRATIONS = [
SeedSystemData1700000000100,
AddCategoryTimestampsAndUniqueAbbrev1700000000200,
UpdateSystemCategoryKeys1700000000300,
RepairCategorySchemaAndSystemCategories1700000000400,
];
@Global()
@@ -0,0 +1,166 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export const CANONICAL_SYSTEM_CATEGORY_KEYS = ['CRY', 'HW', 'MSC', 'PWN', 'REV', 'WEB'] as const;
export interface CanonicalSystemCategory {
key: string;
name: string;
abbreviation: string;
description: string;
iconPath: string;
}
export const CANONICAL_SYSTEM_CATEGORIES: ReadonlyArray<CanonicalSystemCategory> = [
{
key: 'CRY',
name: 'Cryptography',
abbreviation: 'CRY',
description: 'Cryptographic challenges',
iconPath: '/uploads/icons/CRY.png',
},
{
key: 'HW',
name: 'Hardware',
abbreviation: 'HW',
description: 'Hardware challenges',
iconPath: '/uploads/icons/HW.png',
},
{
key: 'MSC',
name: 'Misc',
abbreviation: 'MSC',
description: 'Miscellaneous challenges',
iconPath: '/uploads/icons/MSC.png',
},
{
key: 'PWN',
name: 'Pwn',
abbreviation: 'PWN',
description: 'Binary exploitation',
iconPath: '/uploads/icons/PWN.png',
},
{
key: 'REV',
name: 'Reverse Engineering',
abbreviation: 'REV',
description: 'Reverse engineering challenges',
iconPath: '/uploads/icons/REV.png',
},
{
key: 'WEB',
name: 'Web',
abbreviation: 'WEB',
description: 'Web exploitation challenges',
iconPath: '/uploads/icons/WEB.png',
},
];
export class RepairCategorySchemaAndSystemCategories1700000000400
implements MigrationInterface
{
name = 'RepairCategorySchemaAndSystemCategories1700000000400';
public async up(queryRunner: QueryRunner): Promise<void> {
await this.ensureCategoryTimestamps(queryRunner);
await this.reconcileSystemCategories(queryRunner);
await this.ensureCategoryIndexes(queryRunner);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE UNIQUE INDEX IF NOT EXISTS "uq_category_system_key" ON "category"("system_key") WHERE "system_key" IS NOT NULL;`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX IF NOT EXISTS "uq_category_abbreviation" ON "category"("abbreviation");`,
);
}
private async ensureCategoryTimestamps(queryRunner: QueryRunner): Promise<void> {
const cols: any[] = await queryRunner.query(`PRAGMA table_info("category")`);
const names = new Set(cols.map((c: any) => String(c?.name ?? '')));
if (!names.has('created_at')) {
await queryRunner.query(
`ALTER TABLE "category" ADD COLUMN "created_at" TEXT NOT NULL DEFAULT '';`,
);
}
if (!names.has('updated_at')) {
await queryRunner.query(
`ALTER TABLE "category" ADD COLUMN "updated_at" TEXT NOT NULL DEFAULT '';`,
);
}
await queryRunner.query(
`UPDATE "category" SET "created_at" = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE "created_at" IS NULL OR "created_at" = '';`,
);
await queryRunner.query(
`UPDATE "category" SET "updated_at" = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE "updated_at" IS NULL OR "updated_at" = '';`,
);
}
private async reconcileSystemCategories(queryRunner: QueryRunner): Promise<void> {
const desired = CANONICAL_SYSTEM_CATEGORIES.map((c) => ({ ...c }));
const desiredKeys = new Set(desired.map((d) => d.key));
const desiredAbbrs = new Set(desired.map((d) => d.abbreviation));
const existingSystemRows: any[] = await queryRunner.query(
`SELECT id, system_key, abbreviation FROM "category" WHERE "system_key" IS NOT NULL`,
);
const bySystemKey = new Map<string, any>();
const byAbbreviation = new Map<string, any>();
for (const row of existingSystemRows) {
if (row.system_key) bySystemKey.set(String(row.system_key), row);
if (row.abbreviation) byAbbreviation.set(String(row.abbreviation), row);
}
for (const row of existingSystemRows) {
const systemKey = String(row.system_key ?? '');
const abbreviation = String(row.abbreviation ?? '');
const isCanonicalKey = desiredKeys.has(systemKey);
const isCanonicalAbbr = desiredAbbrs.has(abbreviation);
if (!isCanonicalKey) {
await queryRunner.query(`DELETE FROM "category" WHERE "id" = ?`, [row.id]);
byAbbreviation.delete(abbreviation);
}
}
for (const d of desired) {
const keyRow = bySystemKey.get(d.key);
if (keyRow) {
await queryRunner.query(
`UPDATE "category" SET "name" = ?, "abbreviation" = ?, "description" = ?, "icon_path" = ?, "created_at" = COALESCE(NULLIF("created_at",''), strftime('%Y-%m-%dT%H:%M:%fZ','now')), "updated_at" = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE "id" = ?`,
[d.name, d.abbreviation, d.description, d.iconPath, keyRow.id],
);
byAbbreviation.delete(d.abbreviation);
continue;
}
const abbrRow = byAbbreviation.get(d.abbreviation);
if (abbrRow) {
await queryRunner.query(
`UPDATE "category" SET "system_key" = ?, "name" = ?, "description" = ?, "icon_path" = ?, "created_at" = COALESCE(NULLIF("created_at",''), strftime('%Y-%m-%dT%H:%M:%fZ','now')), "updated_at" = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE "id" = ?`,
[d.key, d.name, d.description, d.iconPath, abbrRow.id],
);
byAbbreviation.delete(d.abbreviation);
continue;
}
const { v4: uuid } = await import('uuid');
await queryRunner.query(
`INSERT INTO "category" ("id","system_key","name","abbreviation","description","icon_path","created_at","updated_at") VALUES (?,?,?,?,?,?, strftime('%Y-%m-%dT%H:%M:%fZ','now'), strftime('%Y-%m-%dT%H:%M:%fZ','now'))`,
[uuid(), d.key, d.name, d.abbreviation, d.description, d.iconPath],
);
}
}
private async ensureCategoryIndexes(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE UNIQUE INDEX IF NOT EXISTS "uq_category_system_key" ON "category"("system_key") WHERE "system_key" IS NOT NULL;`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX IF NOT EXISTS "uq_category_abbreviation" ON "category"("abbreviation");`,
);
}
}