Files
HIPCTF2/backend/src/config/env.schema.ts
T
OpenVelo Agent af3c24275d Scaffold HIPCTF platform (NestJS + Angular)
- Backend: NestJS 10 + TypeORM (better-sqlite3) feature-modular layout.
  - Entities: user, setting, category, challenge, challenge_file, solve,
    refresh_token, blog_post. Auto-run migrations + idempotent 6-system-
    category seed on startup.
  - Argon2id password hashing with policy check; JWT access + rotating
    refresh tokens (HttpOnly cookie); CSRF middleware (SameSite + custom
    X-CSRF-Token header); global JWT auth guard with @Public() opt-out;
    per-IP login backoff + per-IP registration rate limit.
  - Endpoints: auth (login/refresh/logout/csrf), users (first-admin
    registration), system (bootstrap/event status/SSE), admin (guarded
    user CRUD with last-admin invariant), frontend module (uploads +
    SPA fallback).
  - Security: helmet+CSP+HSTS-gated-by-TLS, CORS allowlist, structured
    global exception filter, Zod request validation pipes, OpenAPI 3.1
    served at /api/docs and /api/docs-json, 10 canonical themes under
    backend/themes/.
- Frontend: Angular 17 standalone components, lazy-loaded feature routes,
  signals, functional HttpInterceptorFn (csrf + auth), functional
  CanActivateFn auth guard, HttpOnly-cookie-based auth service.
- Tests: Jest + supertest, 46 tests across 13 suites covering
  migrations, env schema, theme loader, event status, login backoff,
  registration rate limit, ApiError shape, bootstrap integration,
  auth refresh rotation, admin guard, last-admin invariant, SSE flat
  payloads. Single-command runner: `npm test`.
2026-07-21 13:25:49 +00:00

76 lines
3.6 KiB
TypeScript

import { z } from 'zod';
export const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.coerce.number().int().positive().default(3000),
DATABASE_PATH: z.string().min(1).default('/data/hipctf/db.sqlite'),
UPLOAD_DIR: z.string().min(1).default('/data/hipctf/uploads'),
THEMES_DIR: z.string().min(1).default('./themes'),
FRONTEND_DIST: z.string().min(1).default('../frontend/dist'),
JWT_ACCESS_SECRET: z.string().min(32).default('dev-access-secret-change-me-please-32chars'),
JWT_REFRESH_SECRET: z.string().min(32).default('dev-refresh-secret-change-me-please-32ch'),
CSRF_SECRET: z.string().min(32).default('dev-csrf-secret-change-me-please-32chars!!'),
JWT_ACCESS_TTL: z.string().default('15m'),
JWT_REFRESH_TTL: z.string().default('7d'),
ARGON2_MEMORY_COST: z.coerce.number().int().positive().default(65536),
ARGON2_TIME_COST: z.coerce.number().int().positive().default(3),
ARGON2_PARALLELISM: z.coerce.number().int().positive().default(1),
PASSWORD_MIN_LENGTH: z.coerce.number().int().positive().default(12),
PASSWORD_REQUIRE_MIXED: z.coerce.boolean().default(true),
CORS_ORIGINS: z.string().default('http://localhost:4200,http://localhost:3000'),
BODY_SIZE_LIMIT: z.string().default('1mb'),
UPLOAD_SIZE_LIMIT: z.string().default('50mb'),
TLS_ENABLED: z.coerce.boolean().default(false),
});
export type AppEnv = z.infer<typeof envSchema>;
export function validateEnv(raw: Record<string, unknown>): AppEnv {
const parsed = envSchema.safeParse(raw);
if (!parsed.success) {
const issues = parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ');
throw new Error(`Invalid environment configuration: ${issues}`);
}
return parsed.data;
}
export const SETTINGS_KEYS = {
PAGE_TITLE: 'pageTitle',
LOGO: 'logo',
WELCOME_MARKDOWN: 'welcomeMarkdown',
THEME_KEY: 'themeKey',
DEFAULT_CHALLENGE_IP: 'defaultChallengeIp',
REGISTRATIONS_ENABLED: 'registrationsEnabled',
EVENT_START_UTC: 'eventStartUtc',
EVENT_END_UTC: 'eventEndUtc',
} as const;
export const DEFAULT_SETTINGS: Record<string, string> = {
[SETTINGS_KEYS.PAGE_TITLE]: 'HIPCTF',
[SETTINGS_KEYS.LOGO]: '',
[SETTINGS_KEYS.WELCOME_MARKDOWN]: '# Welcome\n\nCreate the first admin to get started.',
[SETTINGS_KEYS.THEME_KEY]: 'classic',
[SETTINGS_KEYS.DEFAULT_CHALLENGE_IP]: '127.0.0.1',
[SETTINGS_KEYS.REGISTRATIONS_ENABLED]: 'false',
[SETTINGS_KEYS.EVENT_START_UTC]: new Date(Date.now() + 60_000).toISOString(),
[SETTINGS_KEYS.EVENT_END_UTC]: new Date(Date.now() + 7 * 24 * 60 * 60_000).toISOString(),
};
export const SYSTEM_CATEGORY_KEYS = ['crypto', 'forensics', 'pwn', 'web', 'misc', 'osint'] as const;
export type SystemCategoryKey = (typeof SYSTEM_CATEGORY_KEYS)[number];
export const SYSTEM_CATEGORY_META: Record<SystemCategoryKey, { name: string; abbreviation: string; description: string; iconPath: string }> = {
crypto: { name: 'Cryptography', abbreviation: 'CRY', description: 'Crypto challenges', iconPath: '/uploads/icons/crypto.svg' },
forensics: { name: 'Forensics', abbreviation: 'FOR', description: 'Forensics challenges', iconPath: '/uploads/icons/forensics.svg' },
pwn: { name: 'Pwn', abbreviation: 'PWN', description: 'Binary exploitation', iconPath: '/uploads/icons/pwn.svg' },
web: { name: 'Web', abbreviation: 'WEB', description: 'Web exploitation', iconPath: '/uploads/icons/web.svg' },
misc: { name: 'Misc', abbreviation: 'MSC', description: 'Miscellaneous challenges', iconPath: '/uploads/icons/misc.svg' },
osint: { name: 'OSINT', abbreviation: 'OSI', description: 'Open-source intelligence', iconPath: '/uploads/icons/osint.svg' },
};