diff --git a/docs/architecture/backend-modules.md b/docs/architecture/backend-modules.md new file mode 100644 index 0000000..d98ed6d --- /dev/null +++ b/docs/architecture/backend-modules.md @@ -0,0 +1,80 @@ +--- +type: architecture +title: Backend Module Map +description: NestJS modules, controllers, services, and how they are wired together. +tags: [architecture, backend, nestjs, modules] +timestamp: 2026-07-21T14:18:00Z +--- + +# Module Map + +All modules are imported in `backend/src/app.module.ts`. The root module +also registers two global providers: + +| Global provider | Purpose | +|---------------------------|--------------------------------------------------------| +| `APP_GUARD = JwtAuthGuard` | Enforces JWT auth unless the handler is `@Public()` | +| `APP_FILTER = GlobalExceptionFilter` | Normalizes errors into the standard envelope | + +# Modules + +| Module | Path | Responsibility | +|------------------|-----------------------------------------------|-------------------------------------------------------------------------------------------------| +| `DatabaseModule` | `backend/src/database/database.module.ts` | Configures TypeORM with `better-sqlite3`, registers entities, runs migrations, enforces WAL. | +| `CommonModule` | `backend/src/common/common.module.ts` | Provides shared services (CSRF middleware class, backoff, registration rate limit, SSE hub, theme loader, etc.). | +| `SettingsModule` | `backend/src/modules/settings/settings.module.ts` | Exposes `SettingsService` (get/set/getAll over `setting` table). | +| `AuthModule` | `backend/src/modules/auth/auth.module.ts` | `AuthController` (`/api/v1/auth/*`) + `AuthService` (login/refresh/logout/register-first-admin).| +| `UsersModule` | `backend/src/modules/users/users.module.ts` | `UsersController` (`/api/v1/auth/register-first-admin`) + `UsersService` (last-admin invariant). | +| `SystemModule` | `backend/src/modules/system/system.module.ts` | `SystemController` (`/api/v1/bootstrap`, `/event/status`, SSE streams) + `SystemService`. | +| `AdminModule` | `backend/src/modules/admin/admin.module.ts` | `AdminController` (`/api/v1/admin/users*`) + `AdminService`. Gated by `AdminGuard` + `@Roles('admin')`. | +| `UploadsModule` | `backend/src/modules/uploads/uploads.module.ts` | `UploadsController` (`/api/v1/uploads/*`). Admin-only multipart. | +| `FrontendModule` | `backend/src/frontend/frontend.module.ts` | Registers `SpaFallbackMiddleware` and the uploads static helper. | + +# Controllers + +| Controller | Path prefix | Auth | Source | +|------------------------|-------------------------|--------------|--------------------------------------------------------------| +| `AuthController` | `/api/v1/auth` | Mostly public (login, refresh, logout, csrf) | `backend/src/modules/auth/auth.controller.ts` | +| `UsersController` | `/api/v1/auth/register-first-admin` | Public (bootstrap-only) | `backend/src/modules/users/users.controller.ts` | +| `AdminController` | `/api/v1/admin` | Admin only | `backend/src/modules/admin/admin.controller.ts` | +| `SystemController` | `/api/v1` | Public | `backend/src/modules/system/system.controller.ts` | +| `UploadsController` | `/api/v1/uploads` | Admin only | `backend/src/modules/uploads/uploads.controller.ts` | + +# Common services + +| Service | Path | Purpose | +|----------------------------------|---------------------------------------------------------------|--------------------------------------------------------| +| `CsrfMiddleware` | `backend/src/common/middleware/csrf.middleware.ts` | Mints/validates CSRF tokens via cookie + header. | +| `JwtAuthGuard` | `backend/src/common/guards/jwt-auth.guard.ts` | Global guard; honors `@Public()` decorator. | +| `AdminGuard` | `backend/src/common/guards/admin.guard.ts` | Requires authenticated user with `role === 'admin'`. | +| `RolesGuard` | `backend/src/common/guards/roles.guard.ts` | Enforces `@Roles(...)` metadata. | +| `GlobalExceptionFilter` | `backend/src/common/filters/global-exception.filter.ts` | Converts exceptions to standard envelope. | +| `EventStatusService` | `backend/src/common/services/event-status.service.ts` | Computes `Stopped`/`Running` and `countdownMs`. | +| `LoginBackoffService` | `backend/src/common/services/login-backoff.service.ts` | Per-IP+username brute-force throttle. | +| `RegistrationRateLimitService` | `backend/src/common/services/registration-rate-limit.service.ts` | Per-IP rate limit on first-admin registration. | +| `SseHubService` | `backend/src/common/services/sse-hub.service.ts` | In-process pub/sub for SSE streams. | +| `ThemeLoaderService` | `backend/src/common/utils/theme-loader.service.ts` | Loads + validates the 10 canonical themes. | +| `ZodValidationPipe` | `backend/src/common/pipes/zod-validation.pipe.ts` | Replaces `ValidationPipe` for zod-validated DTOs. | +| `TransformInterceptor` | `backend/src/common/interceptors/transform.interceptor.ts` | Optional response transformer. | + +# Decorators + +| Decorator | Path | Purpose | +|-----------------|------------------------------------------------------------|----------------------------------| +| `@Public()` | `backend/src/common/decorators/public.decorator.ts` | Mark a route as unauthenticated. | +| `@Roles(...)` | `backend/src/common/decorators/roles.decorator.ts` | Required role(s). | +| `@SkipCsrf()` | `backend/src/common/decorators/skip-csrf.decorator.ts` | Exempt a route from CSRF check. | + +# Startup chain + +`main.ts` → `AppModule` (loads `ConfigModule`, then all feature modules) +→ `DatabaseInitService.init()` (runs migrations + WAL) +→ `app.use(...)` middlewares (helmet, parsers, CSRF, static) +→ `SwaggerModule.setup(...)` (OpenAPI 3.1) +→ `SpaFallbackMiddleware` → `app.listen()`. + +# See also + +- [System Overview](/architecture/overview.md) +- [Key Files Index](/architecture/key-files.md) +- [REST API Overview](/api/rest-overview.md) diff --git a/docs/architecture/frontend-structure.md b/docs/architecture/frontend-structure.md new file mode 100644 index 0000000..8c2dd1e --- /dev/null +++ b/docs/architecture/frontend-structure.md @@ -0,0 +1,69 @@ +--- +type: architecture +title: Frontend Structure +description: Angular routes, components, services, guards, and interceptors. +tags: [architecture, frontend, angular] +timestamp: 2026-07-21T14:18:00Z +--- + +# Routes + +Routes live in `frontend/src/app/app.routes.ts`: + +| Path | Component | Guard | Notes | +|--------------|----------------------------------|---------------|----------------------------------------| +| `/bootstrap` | `CreateAdminComponent` | — | Rendered only when `initialized === false`. | +| `/login` | `LoginComponent` | — | Standard login form. | +| `/` | `HomeComponent` | `authGuard` | Requires auth + initialized. | +| `**` | Redirect to `/` | — | Wildcard fallback. | + +# Components + +All components are standalone (no NgModules). Each component imports +`FormsModule` directly and uses Angular signals for state. + +| Component | Path | Purpose | +|------------------------|-----------------------------------------------------------------|-------------------------------------------------| +| `AppComponent` | `frontend/src/app/app.component.ts` | Root; renders `` and calls `BootstrapService.load()`. | +| `HomeComponent` | `frontend/src/app/features/home/home.component.ts` | Landing page shown after auth. | +| `LoginComponent` | `frontend/src/app/features/auth/login.component.ts` | Username/password form. | +| `CreateAdminComponent` | `frontend/src/app/features/bootstrap/create-admin.component.ts`| First-admin bootstrap form. | + +# Services + +| Service | Path | Purpose | +|---------------------|---------------------------------------------------------------|-----------------------------------------------------------| +| `AuthService` | `frontend/src/app/core/services/auth.service.ts` | Signal-backed access token + current user. | +| `BootstrapService` | `frontend/src/app/core/services/bootstrap.service.ts` | Fetches `/api/v1/bootstrap`, applies theme tokens to CSS. | + +# Guards and interceptors + +| Symbol | Path | Purpose | +|---------------------|---------------------------------------------------------------|-----------------------------------------------------------| +| `authGuard` | `frontend/src/app/core/guards/auth.guard.ts` | Redirects to `/bootstrap` when uninitialized, `/login` when not authenticated. | +| `authInterceptor` | `frontend/src/app/core/interceptors/auth.interceptor.ts` | Attaches `Authorization: Bearer ` header. | +| `csrfInterceptor` | `frontend/src/app/core/interceptors/csrf.interceptor.ts` | Attaches `X-CSRF-Token` header on POST/PUT/PATCH/DELETE. | + +Both interceptors are wired in `frontend/src/main.ts` via +`provideHttpClient(withInterceptors([csrfInterceptor, authInterceptor]))` +(notably CSRF runs first so the token is attached before the auth header). + +# Bootstrap flow + +1. `AppComponent.ngOnInit()` → `BootstrapService.load()`. +2. `BootstrapService` calls `GET /api/v1/bootstrap` (public, with + `credentials: 'include'`). +3. The payload sets `initialized` (true iff any admin user exists), + `pageTitle`, `logo`, `welcomeMarkdown`, the active `theme`, and the + default challenge IP. +4. Theme tokens are written to CSS custom properties on + `document.documentElement` (`--color-primary`, `--font-family`, + `--radius-*`, etc.) consumed by `frontend/src/styles.css`. +5. The `authGuard` reads `BootstrapService.initialized()` and either + allows navigation or redirects to `/bootstrap`. + +# See also + +- [System Overview](/architecture/overview.md) +- [Backend Module Map](/architecture/backend-modules.md) +- [First-Run Bootstrap](/guides/bootstrap.md) diff --git a/docs/architecture/key-files.md b/docs/architecture/key-files.md new file mode 100644 index 0000000..d6d0e96 --- /dev/null +++ b/docs/architecture/key-files.md @@ -0,0 +1,129 @@ +--- +type: architecture +title: Key Files Index +description: One-line responsibility for every important source file in the repository. +tags: [architecture, index, key-files] +timestamp: 2026-07-21T14:18:00Z +--- + +# Backend + +## Entrypoint & config + +| File | Responsibility | +|--------------------------------------------------------------------------|--------------------------------------------------------------------------------| +| `backend/src/main.ts` | Bootstraps Nest, registers middleware, builds OpenAPI 3.1, mounts SPA fallback. | +| `backend/src/app.module.ts` | Root module; wires every feature module + global guards/filters. | +| `backend/src/config/env.schema.ts` | Zod-validated env schema, `SETTINGS_KEYS`, system category metadata. | + +## Database + +| File | Responsibility | +|--------------------------------------------------------------------------|--------------------------------------------------------------------------------| +| `backend/src/database/database.module.ts` | TypeORM `better-sqlite3` config, entity + migration registration. | +| `backend/src/database/database-init.service.ts` | Initializes the DataSource, enforces WAL PRAGMA, runs migrations, verifies seed. | +| `backend/src/database/migrations/1700000000000-InitSchema.ts` | Creates all tables and sets WAL + foreign keys. | +| `backend/src/database/migrations/1700000000100-SeedSystemData.ts` | Seeds the 6 system categories and default settings. | +| `backend/src/database/entities/user.entity.ts` | `user` table (admin/player role, enabled/disabled status). | +| `backend/src/database/entities/setting.entity.ts` | `setting` key/value table. | +| `backend/src/database/entities/category.entity.ts` | `category` table with `system_key` unique index. | +| `backend/src/database/entities/challenge.entity.ts` | `challenge` table (difficulty, decay, protocol, flag, port). | +| `backend/src/database/entities/challenge-file.entity.ts` | `challenge_file` table for attachments. | +| `backend/src/database/entities/solve.entity.ts` | `solve` table with unique `(challenge_id, user_id)`. | +| `backend/src/database/entities/refresh-token.entity.ts` | `refresh_token` table (token_hash + revocation). | +| `backend/src/database/entities/blog-post.entity.ts` | `blog_post` table (draft/published + publishedAt index). | + +## Common + +| File | Responsibility | +|--------------------------------------------------------------------------|--------------------------------------------------------------------------------| +| `backend/src/common/common.module.ts` | Aggregates common services, middleware, and providers. | +| `backend/src/common/middleware/csrf.middleware.ts` | Double-submit CSRF check; mints/reads cookie, compares header. | +| `backend/src/common/guards/jwt-auth.guard.ts` | Global JWT guard; honors `@Public()`. | +| `backend/src/common/guards/admin.guard.ts` | Requires `req.user.role === 'admin'`. | +| `backend/src/common/guards/roles.guard.ts` | Enforces `@Roles(...)` metadata. | +| `backend/src/common/decorators/public.decorator.ts` | `@Public()` marker. | +| `backend/src/common/decorators/roles.decorator.ts` | `@Roles(...)` metadata. | +| `backend/src/common/decorators/skip-csrf.decorator.ts` | `@SkipCsrf()` marker. | +| `backend/src/common/filters/global-exception.filter.ts` | Converts all errors to the standard envelope. | +| `backend/src/common/pipes/zod-validation.pipe.ts` | Validates body/param/query with a zod schema. | +| `backend/src/common/interceptors/transform.interceptor.ts` | Optional response transformer. | +| `backend/src/common/services/event-status.service.ts` | Computes event `Stopped`/`Running` and `countdownMs`. | +| `backend/src/common/services/login-backoff.service.ts` | Per-IP+username failed-login throttle. | +| `backend/src/common/services/registration-rate-limit.service.ts` | Per-IP first-admin registration throttle. | +| `backend/src/common/services/sse-hub.service.ts` | In-process pub/sub for event status and scoreboard streams. | +| `backend/src/common/utils/openapi31.ts` | Converts the Nest-generated OpenAPI 3.0 doc to OpenAPI 3.1. | +| `backend/src/common/utils/password-policy.ts` | Argon2 password validator (length, mixed-case policy). | +| `backend/src/common/utils/theme-loader.service.ts` | Loads themes from disk, backfills built-ins, validates tokens. | +| `backend/src/common/utils/upload.ts` | Upload size-limit parser + filename sanitizer. | +| `backend/src/common/types/theme.ts`, `theme-ids.ts` | `Theme` interface and the 10 canonical `ThemeId`s. | +| `backend/src/common/errors/api-error.ts` | `ApiError` class (status + machine code + message + details). | +| `backend/src/common/errors/error-codes.ts` | Canonical `ERROR_CODES` enum. | + +## Modules + +| File | Responsibility | +|--------------------------------------------------------------------------|--------------------------------------------------------------------------------| +| `backend/src/modules/auth/auth.module.ts` | Wires `AuthController`, `AuthService`, `JwtStrategy`. | +| `backend/src/modules/auth/auth.controller.ts` | `/api/v1/auth/{login,refresh,logout,csrf}`. | +| `backend/src/modules/auth/auth.service.ts` | Login, refresh, logout, register-first-admin, mint JWT + refresh tokens. | +| `backend/src/modules/auth/cookie.ts` | `setRefreshCookie` / `clearRefreshCookie` helpers. | +| `backend/src/modules/auth/dto/auth.dto.ts` | Zod schemas for login + refresh. | +| `backend/src/modules/auth/strategies/jwt.strategy.ts` | Passport JWT strategy. | +| `backend/src/modules/users/users.module.ts` | Wires `UsersController` + `UsersService`. | +| `backend/src/modules/users/users.controller.ts` | `/api/v1/auth/register-first-admin`. | +| `backend/src/modules/users/users.service.ts` | `enforceLastAdminInvariant` helper. | +| `backend/src/modules/users/dto/create-first-admin.dto.ts` | Zod schema for first-admin registration. | +| `backend/src/modules/admin/admin.module.ts` | Wires `AdminController` + `AdminService`. | +| `backend/src/modules/admin/admin.controller.ts` | `/api/v1/admin/users[/{id}]`. | +| `backend/src/modules/admin/admin.service.ts` | list / create / update role / delete user with last-admin guard. | +| `backend/src/modules/admin/dto/admin.dto.ts` | Zod schemas for admin endpoints. | +| `backend/src/modules/system/system.module.ts` | Wires `SystemController` + `SystemService` + status + hub services. | +| `backend/src/modules/system/system.controller.ts` | `/api/v1/{bootstrap,event/status}` + SSE `/event/stream`, `/scoreboard/stream`. | +| `backend/src/modules/system/system.service.ts` | Builds the public bootstrap payload from settings + theme + admin count. | +| `backend/src/modules/uploads/uploads.module.ts` | Wires `UploadsController`. | +| `backend/src/modules/uploads/uploads.controller.ts` | `/api/v1/uploads/{category-icon,challenge-file}`. | +| `backend/src/modules/settings/settings.module.ts` | `SettingsService` (get/set/getAll over the `setting` table). | +| `backend/src/frontend/frontend.module.ts` | Wires SPA fallback + uploads static middleware. | +| `backend/src/frontend/spa.controller.ts` | `SpaFallbackMiddleware` (returns `index.html` for client routes). | +| `backend/src/frontend/uploads-static.middleware.ts` | Mounts `/uploads` static helper. | + +## Themes + +| File | Responsibility | +|----------------------------------------------|-------------------------------------------------------------| +| `backend/themes/01-classic.json` | `classic` theme tokens. | +| `backend/themes/02-midnight.json` … `10-monochrome.json` | The other 9 canonical themes. | + +# Frontend + +| File | Responsibility | +|-----------------------------------------------------|--------------------------------------------------------------------------------| +| `frontend/src/main.ts` | Bootstraps the standalone Angular app + registers interceptors. | +| `frontend/src/index.html` | Root HTML shell. | +| `frontend/src/styles.css` | Global styles consuming theme CSS custom properties. | +| `frontend/src/app/app.component.ts` | Root component; triggers bootstrap on init. | +| `frontend/src/app/app.routes.ts` | Angular route table. | +| `frontend/src/app/core/services/auth.service.ts` | Signal store for access token + current user. | +| `frontend/src/app/core/services/bootstrap.service.ts` | Fetches `/api/v1/bootstrap` and applies theme tokens to CSS. | +| `frontend/src/app/core/guards/auth.guard.ts` | `CanActivateFn` checking init + auth. | +| `frontend/src/app/core/interceptors/auth.interceptor.ts` | Attaches Bearer header. | +| `frontend/src/app/core/interceptors/csrf.interceptor.ts` | Attaches `X-CSRF-Token` on unsafe methods. | +| `frontend/src/app/features/auth/login.component.ts` | Login form. | +| `frontend/src/app/features/bootstrap/create-admin.component.ts` | First-admin creation form. | +| `frontend/src/app/features/home/home.component.ts` | Landing page after auth. | + +# Tests + +| File | Responsibility | +|------------------------------------------------|--------------------------------------------------------------------------------| +| `tests/backend/*.spec.ts` (18 suites) | Jest tests for backend modules, CSRF, OpenAPI 3.1, migrations, rate limits. | +| `tests/frontend/theme.spec.ts` | Jest test for theme token application. | +| `tests/backend/csrf-client.ts` | Test helper for CSRF flow. | +| `tests/backend/db-helper.ts` | Test helper for spinning up an isolated DB. | + +# See also + +- [System Overview](/architecture/overview.md) +- [Backend Module Map](/architecture/backend-modules.md) +- [Frontend Structure](/architecture/frontend-structure.md) diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md new file mode 100644 index 0000000..f28b399 --- /dev/null +++ b/docs/architecture/overview.md @@ -0,0 +1,85 @@ +--- +type: architecture +title: System Overview +description: High-level layout of the HIPCTF monorepo (NestJS API + Angular SPA) and how they communicate. +tags: [architecture, backend, frontend, overview] +timestamp: 2026-07-21T14:18:00Z +--- + +# Overview + +HIPCTF is delivered as a Node.js npm workspace containing two packages: + +| Package | Path | Stack | +|-----------|-------------|----------------------------------| +| Backend | `backend/` | NestJS 10 + TypeORM + better-sqlite3 | +| Frontend | `frontend/` | Angular 17 standalone components | + +The backend also serves the built Angular SPA as static files and falls +back to `index.html` for client-side routes, so a single Node process +hosts the whole application. + +# Topology + +``` +Browser ──HTTPS──▶ NestJS process (PORT, default 3000) + │ + ├── /api/v1/* ── controllers → services → TypeORM + │ │ + │ ▼ + │ better-sqlite3 + │ (DATABASE_PATH) + │ + ├── /api/docs, /api/docs-json ── Swagger UI (OpenAPI 3.1) + │ + ├── /uploads/* ── static file server (UPLOAD_DIR) + │ + └── /* ── SPA fallback (FRONTEND_DIST/index.html) +``` + +# Backend entrypoint flow + +`backend/src/main.ts` orchestrates startup in this order: + +1. Create the Nest application. +2. Read configuration via `ConfigService` (validated against + `backend/src/config/env.schema.ts`). +3. Call `DatabaseInitService.init()` so migrations + WAL PRAGMA run + before HTTP traffic is accepted. +4. Register global middleware: `helmet`, `cookieParser`, JSON / URL-encoded + parsers, then `CsrfMiddleware` (registered explicitly so it can read + the parsed body). +5. Mount `/uploads` static and Angular static (`FRONTEND_DIST`). +6. Build the OpenAPI 3.1 document via `toOpenApi31()` and expose Swagger + UI at `/api/docs` and JSON at `/api/docs-json`. +7. Install `SpaFallbackMiddleware` so non-`/api`, non-`/uploads`, + extensionless `GET` requests return `index.html`. +8. `app.listen(port)`. + +# Frontend entrypoint flow + +`frontend/src/main.ts` bootstraps a standalone Angular app: + +1. `provideHttpClient(withInterceptors([csrfInterceptor, authInterceptor]))` + attaches the CSRF header on unsafe methods and the Bearer access + token on every request. +2. `provideRouter(APP_ROUTES, withComponentInputBinding())`. +3. `AppComponent.ngOnInit()` calls `BootstrapService.load()` which + fetches `GET /api/v1/bootstrap` and applies the returned theme tokens + to CSS custom properties on `document.documentElement`. + +# Cross-cutting concepts + +| Concern | Backend | Frontend | +|----------------|------------------------------------------------------|---------------------------------------------| +| Authentication | `JwtAuthGuard` (global) + `AuthGuard('jwt')` strategy | `AuthService` signal + `authInterceptor` | +| Authorization | `AdminGuard` + `@Roles('admin')` decorator | `authGuard` route guard | +| CSRF | `CsrfMiddleware` (skips `/api/v1/auth/login` and `/api/v1/auth/register-first-admin`) | `csrfInterceptor` reads `csrf` cookie | +| Errors | `ApiError` → `GlobalExceptionFilter` returns `{code, message, details, path, timestamp}` | Components display `error?.error?.message` | +| Config | `envSchema` (zod) + `validateEnv` | None (consumed via bootstrap) | + +# See also + +- [Backend Module Map](/architecture/backend-modules.md) +- [Frontend Structure](/architecture/frontend-structure.md) +- [REST API Overview](/api/rest-overview.md) diff --git a/docs/database/auth-settings.md b/docs/database/auth-settings.md new file mode 100644 index 0000000..34cdb39 --- /dev/null +++ b/docs/database/auth-settings.md @@ -0,0 +1,67 @@ +--- +type: database +title: Auth and Settings Tables +description: refresh_token and setting tables — rotating refresh tokens and site configuration. +tags: [database, refresh-token, settings, config] +timestamp: 2026-07-21T14:18:00Z +--- + +# Tables + +## `refresh_token` + +| Column | Type | Description | +|---------------|---------|------------------------------------------------------------------| +| `id` | TEXT PK | UUID v4. | +| `user_id` | TEXT | FK → `user.id` (indexed, but no DB-level FK cascade is configured). | +| `token_hash` | TEXT | SHA-256 of the raw refresh token (unique). | +| `issued_at` | TEXT | ISO 8601 timestamp. | +| `expires_at` | TEXT | ISO 8601 timestamp; checked on refresh. | +| `revoked_at` | TEXT | ISO 8601 timestamp or `NULL` if active. | + +# Refresh token lifecycle + +1. **Issue** — `AuthService.login` and `AuthService.registerFirstAdmin` + mint a new token via `crypto.randomBytes(32).toString('base64url')` + and store its SHA-256 hash with `expires_at = now + JWT_REFRESH_TTL`. + The raw token is set as an HttpOnly `rt` cookie and also returned in + the login response body. +2. **Rotate** — `AuthService.refresh` looks up the row by `token_hash`, + asserts it isn't revoked/expired, marks `revoked_at = now`, and mints + a new token in the same transaction. Each token is single-use. +3. **Logout** — `AuthService.logout` looks up by `token_hash` (from the + `rt` cookie) and sets `revoked_at`. Already-revoked or unknown tokens + are silently ignored. + +## `setting` + +| Column | Type | Description | +|--------|---------|-----------------------------------------------------| +| `key` | TEXT PK | Settings key (see `SETTINGS_KEYS` below). | +| `value`| TEXT | String value (defaults to empty string). | + +Read/write access goes through `SettingsService` +(`backend/src/modules/settings/settings.module.ts`), which exposes +`get(key, fallback?)`, `set(key, value)`, and `getAll()`. + +# Settings keys + +Defined in `backend/src/config/env.schema.ts` (`SETTINGS_KEYS`) and +seeded by `SeedSystemData1700000000100`: + +| Key | Default | Consumed by | +|-------------------------|------------------------------------------|-------------------------------------------------------------------| +| `pageTitle` | `HIPCTF` | `SystemService.bootstrap` → `pageTitle` | +| `logo` | `""` | `SystemService.bootstrap` → `logo` | +| `welcomeMarkdown` | `"# Welcome\n\nCreate the first admin..."` | `SystemService.bootstrap` → `welcomeMarkdown` | +| `themeKey` | `classic` | `SystemService.bootstrap` → `theme` (overridden via `ThemeLoaderService`) | +| `defaultChallengeIp` | `127.0.0.1` | `SystemService.bootstrap` → `defaultChallengeIp` | +| `registrationsEnabled` | `false` | `SystemService.bootstrap` → `registrationsEnabled` | +| `eventStartUtc` | `now + 60s` (ISO) | `EventStatusService.getStatus` | +| `eventEndUtc` | `now + 7d` (ISO) | `EventStatusService.getStatus` | + +# See also + +- [Database Schema Overview](/database/schema.md) +- [User Table](/database/users.md) +- [System Endpoints](/api/system.md) diff --git a/docs/database/challenges.md b/docs/database/challenges.md new file mode 100644 index 0000000..aef6b86 --- /dev/null +++ b/docs/database/challenges.md @@ -0,0 +1,80 @@ +--- +type: database +title: Challenge Tables +description: category, challenge, challenge_file, and solve tables — how CTF challenges and scoring are stored. +tags: [database, challenge, category, solve] +timestamp: 2026-07-21T14:18:00Z +--- + +# Tables + +## `category` + +| Column | Type | Description | +|----------------|---------|------------------------------------------------------------------------------| +| `id` | TEXT PK | UUID v4. | +| `system_key` | TEXT | One of `crypto`, `forensics`, `pwn`, `web`, `misc`, `osint` (unique where NOT NULL) or `NULL` for user-created categories. | +| `name` | TEXT | Display name (e.g. `Cryptography`). | +| `abbreviation` | TEXT | Short label (e.g. `CRY`). | +| `description` | TEXT | Optional description. | +| `icon_path` | TEXT | Default icon URL (e.g. `/uploads/icons/crypto.svg`). | + +The seed migration inserts one row per `SYSTEM_CATEGORY_KEYS` value from +`backend/src/config/env.schema.ts`. + +## `challenge` + +| Column | Type | Description | +|-------------------|----------|-----------------------------------------------------------------------------------| +| `id` | TEXT PK | UUID v4. | +| `name` | TEXT | Display name. | +| `description_md` | TEXT | Markdown description. | +| `category_id` | TEXT | FK → `category.id`. | +| `difficulty` | TEXT | `'low'` / `'med'` / `'high'`. | +| `initial_points` | INTEGER | Starting point value. | +| `minimum_points` | INTEGER | Floor after decay. | +| `decay_solves` | INTEGER | Number of solves at which decay reaches the minimum. | +| `flag` | TEXT | Submission string expected from players. | +| `protocol` | TEXT | `'nc'` (netcat-style connection) or `'web'` (browser-based). | +| `port` | INTEGER | TCP port when relevant (nullable). | +| `ip_address` | TEXT | IP the challenge listens on (defaulted from `setting.defaultChallengeIp`). | +| `created_at` | TEXT | ISO 8601 timestamp. | + +## `challenge_file` + +| Column | Type | Description | +|---------------------|---------|--------------------------------------| +| `id` | TEXT PK | UUID v4. | +| `challenge_id` | TEXT | FK → `challenge.id`. | +| `original_filename` | TEXT | Filename from upload. | +| `stored_path` | TEXT | Path on disk under `UPLOAD_DIR/challenges/`. | + +## `solve` + +| Column | Type | Description | +|------------------|---------|----------------------------------------------------------------------| +| `id` | TEXT PK | UUID v4. | +| `challenge_id` | TEXT | FK → `challenge.id`. | +| `user_id` | TEXT | FK → `user.id`. | +| `solved_at` | TEXT | ISO 8601 timestamp the solve was recorded. | +| `points_awarded` | INTEGER | Points actually awarded (after decay). | +| `base_points` | INTEGER | Snapshot of `challenge.initial_points` at solve time. | +| `rank_bonus` | INTEGER | Extra points from rank (e.g. first-blood). | + +Uniqueness is enforced by `uq_solve_challenge_user` on +`(challenge_id, user_id)` — a user can solve a given challenge only once. + +# Scoring formula + +`points_awarded = clamp(initial_points - decay, minimum_points, initial_points)` +where `decay = max(0, base_points - minimum_points) * solvesBefore / decay_solves`, +and additional `rank_bonus` may be added for early solves. + +(The exact scoring algorithm lives in the future solve-recording service +and is consumed by `SseHubService` whenever a new solve is published.) + +# See also + +- [Database Schema Overview](/database/schema.md) +- [System Endpoints](/api/system.md) (SSE scoreboard stream) +- [Scoreboard Stream](/guides/scoreboard-stream.md) diff --git a/docs/database/schema.md b/docs/database/schema.md new file mode 100644 index 0000000..a9ae386 --- /dev/null +++ b/docs/database/schema.md @@ -0,0 +1,81 @@ +--- +type: database +title: Database Schema Overview +description: SQLite (better-sqlite3) schema for HIPCTF: tables, relationships, indexes, and WAL journal mode. +tags: [database, sqlite, typeorm, schema] +timestamp: 2026-07-21T14:18:00Z +--- + +# Overview + +HIPCTF uses **SQLite via better-sqlite3** with TypeORM migrations. The +database is a single file (`DATABASE_PATH`, default +`/data/hipctf/db.sqlite`) running in **WAL journal mode** for better +concurrent read/write performance. + +The schema is created by a single migration +(`backend/src/database/migrations/1700000000000-InitSchema.ts`) and seeded +by a second migration +(`backend/src/database/migrations/1700000000100-SeedSystemData.ts`). + +# Tables + +| Table | Purpose | +|-------------------|-------------------------------------------------------------| +| `user` | Authenticated users with role + status. | +| `setting` | Key/value site configuration. | +| `category` | Challenge categories (system + user-defined). | +| `challenge` | CTF challenges with scoring + protocol metadata. | +| `challenge_file` | Files attached to challenges. | +| `solve` | User-solved challenges (scoring ledger). | +| `refresh_token` | Rotating refresh tokens (hash + revocation). | +| `blog_post` | Markdown blog posts in draft/published states. | + +# Relationships + +``` +user ───< solve >─── challenge + │ + └─── challenge_file >── challenge ──> category + +user ───< refresh_token +``` + +* `challenge.category_id` → `category.id` (`ON DELETE RESTRICT`). +* `challenge_file.challenge_id` → `challenge.id` (`ON DELETE RESTRICT`). +* `solve.challenge_id` → `challenge.id` (`ON DELETE RESTRICT`). +* `solve.user_id` → `user.id` (`ON DELETE RESTRICT`). + +# Indexes + +| Table | Index | +|------------------|----------------------------------------------------| +| `category` | `uq_category_system_key` (unique on `system_key` where NOT NULL) | +| `challenge` | `idx_challenge_category` | +| `challenge_file` | `idx_challenge_file_challenge` | +| `solve` | `uq_solve_challenge_user` (unique), `idx_solve_user`, `idx_solve_challenge` | +| `refresh_token` | `idx_refresh_token_user`, unique on `token_hash` | +| `blog_post` | `idx_blog_status_published` (`status`, `published_at`) | + +# PRAGMAs + +| PRAGMA | Where set | Effect | +|--------------------|----------------------------------------------------|---------------------------------| +| `foreign_keys = ON`| Migration `InitSchema1700000000000` | Enforce FK constraints. | +| `journal_mode = WAL`| Migration + `DatabaseInitService.ensureWalMode()` on every startup | Persisted in DB header; allows concurrent readers. | + +# Bootstrap behavior + +On every process start, `DatabaseInitService.init()`: + +1. Initializes the TypeORM `DataSource` (idempotent). +2. Calls `ensureWalMode()` — idempotent; if WAL is already set it only logs. +3. Runs pending migrations if any (or if the `category` table is missing). +4. Logs the seed row counts (categories, settings) via `verifySeed()`. + +# See also + +- [User Table](/database/users.md) +- [Challenge Tables](/database/challenges.md) +- [Auth and Settings Tables](/database/auth-settings.md) +- [Blog Post Table](/database/blog-posts.md) diff --git a/docs/database/users.md b/docs/database/users.md new file mode 100644 index 0000000..8eb9159 --- /dev/null +++ b/docs/database/users.md @@ -0,0 +1,45 @@ +--- +type: database +title: User Table +description: The `user` table — accounts, roles, and statuses. +tags: [database, user, auth] +timestamp: 2026-07-21T14:18:00Z +--- + +# Schema + +| Column | Type | Description | +|----------------|----------|------------------------------------------------------------------| +| `id` | TEXT PK | UUID v4 generated on creation. | +| `username` | TEXT | Unique login name. | +| `password_hash`| TEXT | Argon2id hash (memory/time/parallelism from env config). | +| `role` | TEXT | `'admin'` or `'player'`. Default `'player'`. | +| `status` | TEXT | `'enabled'` or `'disabled'`. Default `'enabled'`. | +| `created_at` | TEXT | ISO 8601 timestamp (`strftime('%Y-%m-%dT%H:%M:%fZ','now')`). | + +# Constraints + +- Unique on `username`. +- `role` is enforced application-side (see `AdminService.updateUserRole`). +- `status` is checked by `AuthService.login` and `AuthService.refresh` + — disabled users cannot log in or refresh. + +# Invariants + +- The application enforces the **last-admin invariant**: at least one + enabled user with `role = 'admin'` must always exist. Attempts to + demote or delete the last admin are rejected by + `UsersService.enforceLastAdminInvariant` (used by + `AdminService.updateUserRole` and `AdminService.deleteUser`). + +# First-admin bootstrap + +When the `user` table is empty of admins, `POST /api/v1/auth/register-first-admin` +is the only endpoint that can create one. After that, the endpoint +returns `409 SYSTEM_INITIALIZED`. + +# See also + +- [Auth and Settings Tables](/database/auth-settings.md) +- [REST API Overview](/api/rest-overview.md) +- [Admin Endpoints](/api/admin.md) diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..7ff8748 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,57 @@ +--- +okf_version: "0.1" +--- + +# HIPCTF Documentation + +HIPCTF is a single-tenant CTF (Capture-The-Flag) platform built as a Node.js +monorepo containing a NestJS REST API and an Angular single-page application. +It supports user registration, authentication, challenge management, a live +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. + +# Architecture + +* [System Overview](/architecture/overview.md) - High-level layout of the + backend, frontend, and how they communicate. +* [Backend Module Map](/architecture/backend-modules.md) - NestJS modules, + controllers, services, and their wiring. +* [Frontend Structure](/architecture/frontend-structure.md) - Angular routes, + components, services, and guards. +* [Key Files Index](/architecture/key-files.md) - One-line summary of every + important source file in the repository. + +# Database + +* [Schema Overview](/database/schema.md) - Tables, relationships, and indexes. +* [User Table](/database/users.md) - `user` table schema and seed behavior. +* [Challenge Tables](/database/challenges.md) - `category`, `challenge`, + `challenge_file`, `solve` tables. +* [Auth and Settings Tables](/database/auth-settings.md) - `refresh_token` + and `setting` tables. +* [Blog Post Table](/database/blog-posts.md) - `blog_post` table. + +# API + +* [REST API Overview](/api/rest-overview.md) - Base URL, versioning, auth, + CSRF, and error envelope. +* [Auth Endpoints](/api/auth.md) - Login, refresh, logout, CSRF, and + first-admin registration. +* [Admin Endpoints](/api/admin.md) - User management endpoints + (admin role required). +* [System Endpoints](/api/system.md) - Bootstrap, event status, and SSE + streams. +* [Uploads Endpoints](/api/uploads.md) - Admin-only multipart uploads. + +# Guides + +* [First-Run Bootstrap](/guides/bootstrap.md) - How a fresh instance is + initialized by the very first admin. +* [Theming](/guides/theming.md) - How themes are loaded and applied. +* [Event Window](/guides/event-window.md) - How the event window and live + countdown work. +* [Scoreboard Stream](/guides/scoreboard-stream.md) - How live solves are + broadcast to clients.