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`.
This commit is contained in:
OpenVelo Agent
2026-07-21 13:25:49 +00:00
parent e55c9cda56
commit af3c24275d
112 changed files with 21931 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
# Implementation Plan: Reviewer Follow-ups (Part 2)
## 0. Scope
Six discrete hardenings of the scaffold already on the branch:
1. Commit the actual scaffold to the branch (the working-tree only had README.md deleted in the previous commit).
2. Make `main.ts` explicitly await DB initialization (migrations + seeds) before `app.listen()`.
3. The Angular `authInterceptor` should call `auth.getAccessToken()` (it already does — confirm wiring & ensure clones set `Authorization: Bearer`; also make sure it's registered BEFORE `csrfInterceptor` in the providers list).
4. Add Zod + class-validator DTOs for every admin body/path/query input, with `@Body(new ZodValidationPipe(...))` etc.
5. Implement real upload endpoints using a configured per-route/global Multer `fileSize` limit and a safe filename strategy.
6. `ThemeLoaderService` must REQUIRE and validate the ten theme files at startup; validate the configured theme key; fall back with a warning if missing/invalid.
## 1. Architectural Reconnaissance
- **Current state:** all scaffold files are present in the working tree and staged; only `README.md` was deleted in the prior commit. `git status` shows the planned commit is already a no-op on tracked paths except the working-tree edits.
- **Stack:** NestJS 10 + TypeORM/better-sqlite3 backend, Angular 17 standalone frontend, Jest + supertest tests in `/repo/tests/`, single command `npm test`.
- **Key existing modules:** `DatabaseModule` (TypeORM async + `migrationsRun: true`), `ThemeLoaderService` (currently warn-and-fallback on missing themes), `CommonModule` (exports CsrfMiddleware), `AuthModule`, `UsersModule`, `AdminModule`, `SystemModule`, `FrontendModule`.
- **Tests live in:** `/repo/tests/backend/` and `/repo/tests/frontend/`, runnable with `npm test`.
- **Persistent data:** `/data/hipctf/db.sqlite` and `/data/hipctf/uploads/`.
## 2. Impacted Files
### To Create
- `backend/src/modules/admin/dto/create-user.dto.ts` (Zod schema)
- `backend/src/modules/admin/dto/update-user-role.dto.ts`
- `backend/src/modules/admin/dto/list-users.query.dto.ts`
- `backend/src/modules/admin/dto/user-id.param.dto.ts`
- `backend/src/modules/uploads/uploads.module.ts`
- `backend/src/modules/uploads/uploads.controller.ts`
- `backend/src/modules/uploads/uploads.service.ts`
- `backend/src/modules/uploads/dto/upload-category-image.dto.ts`
- `tests/backend/admin-validation.spec.ts`
- `tests/backend/uploads.spec.ts`
- `tests/backend/theme-required.spec.ts`
### To Modify
- `.gitignore` — keep `/data/` and `node_modules/` ignored; keep the prior diff already staged.
- `backend/src/main.ts` — await `dataSource.initialize()` (or a dedicated `DatabaseInitService.init()`) BEFORE `app.listen()`; ensure SPA static and helmet/CORS ordering unchanged.
- `backend/src/database/database.module.ts` — implement `OnApplicationBootstrap` that awaits `DataSource.initialize()` and logs; export `DatabaseInitService` so `main.ts` can await it.
- `backend/src/common/utils/theme-loader.service.ts` — make the service REQUIRE all 10 themes; at the end of `loadAll()` compare loaded ids vs `THEME_IDS`; if any are missing OR if the configured `themeKey` setting does not match a loaded id, log a clear warning and fall back to default.
- `backend/src/modules/admin/admin.controller.ts` — replace untyped `@Body()`/`@Param()` with `@Body(new ZodValidationPipe(...))`/`@Param(new ZodValidationPipe(...))`/query DTOs; add class-validator metadata via `@ApiProperty` for OpenAPI.
- `backend/src/common/utils/upload.ts` — add a `createMulterMiddleware(config, opts)` factory returning a configured `multer` instance with `limits.fileSize`; add a `safeFilename(original)` helper that strips path traversal, lowercases, restricts charset, and deduplicates.
- `frontend/src/main.ts` — verify the providers order: `[csrfInterceptor, authInterceptor]`; ensure `AuthService.getAccessToken()` is wired and the clone sets `Authorization: Bearer`.
## 3. Proposed Changes
### 3.1 Commit the scaffold
The working tree is already correctly staged. Steps:
1. `git add -A` to capture any untracked files (`.gitignore` already modified).
2. `git commit -m "Scaffold HIPCTF platform (NestJS + Angular)"` on the current branch `feature-820-1784637300954`. (No amend; no force-push.)
3. Verify the new commit includes all scaffold files: `git show --stat HEAD` lists 50+ files across `backend/`, `frontend/`, `tests/`.
### 3.2 Await DB init in `main.ts`
1. Add `DatabaseInitService` exporting `init(): Promise<void>` that:
- Resolves the `DataSource` token from `app.get(DataSource)`.
- Calls `dataSource.initialize()` (idempotent — TypeORM returns the existing connection if already initialized).
- Awaits `dataSource.runMigrations({ transaction: 'each' })` (TypeORM runs them in order; both our migrations are idempotent).
- Logs the migration count and seed result.
2. Expose via `DatabaseModule` `exports` so `main.ts` can `app.get(DatabaseInitService).init()`.
3. `main.ts` flow:
```ts
const app = await NestFactory.create(AppModule);
await app.get(DatabaseInitService).init();
// ... existing helmet/cors/static/validation/swagger setup unchanged ...
await app.listen(port, '0.0.0.0');
```
4. The existing `migrationsRun: true` flag in `TypeOrmModule.forRootAsync` already runs migrations automatically on `DataSource.initialize()`; the explicit `init()` step is for *visibility*, ordering guarantees, and logging. This satisfies "startup awaits database initialization/migrations/seeds before app.listen".
5. Test: add `database-init.spec.ts` that calls `DatabaseInitService.init()` against `:memory:` and asserts the schema + seeded categories/settings are present.
### 3.3 Angular auth interceptor (confirm and finalize)
1. `frontend/src/main.ts` registers providers in this order:
```ts
provideHttpClient(withInterceptors([csrfInterceptor, authInterceptor]))
```
2. `frontend/src/app/core/interceptors/auth.interceptor.ts` already calls `auth.getAccessToken()` and clones with `Authorization: Bearer ${token}`. No code change needed — verify by a unit test:
- `tests/frontend/auth-interceptor.spec.ts` uses `provideHttpClientTesting()` to capture outbound requests; sets a token on `AuthService.setSession(...)` and asserts the cloned request has the `Authorization` header.
### 3.4 Admin DTOs (Zod + class-validator)
1. Create `backend/src/modules/admin/dto/` with:
- `create-user.dto.ts` — Zod: `{ username: string.min(3).max(64), password: string.min(12), role: enum(['admin','player']) }`.
- `update-user-role.dto.ts` — Zod: `{ role: enum(['admin','player']) }`.
- `user-id.param.dto.ts` — Zod: `{ id: string.uuid() }`.
- `list-users.query.dto.ts` — Zod: `{ limit?: number.int().positive().max(200).default(50), cursor?: string.uuid().optional() }`.
2. Apply via `ZodValidationPipe` on every handler. Also add `@ApiProperty` for Swagger metadata (the project already mixes Zod + Swagger; the Swagger annotations live alongside the DTOs).
3. Admin service: enforce `password` policy (delegate to `validatePassword` from `common/utils/password-policy`).
4. Tests: `admin-validation.spec.ts` — invalid body returns 400; missing path param returns 400; bad role returns 400.
### 3.5 Upload endpoints with safe filenames
1. `backend/src/common/utils/upload.ts` additions:
```ts
export function safeFilename(original: string): string {
// strip directory, lowercase, replace [^a-z0-9._-], collapse, dedupe
const base = path.basename(original || 'file');
const cleaned = base.toLowerCase()
.replace(/[^a-z0-9._-]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 120) || 'file';
const ext = path.extname(cleaned);
const stem = ext ? cleaned.slice(0, -ext.length) : cleaned;
return `${stem}-${randomUUID().slice(0, 8)}${ext}`;
}
export function buildMulter(config: ConfigService) {
const limits = buildUploadLimits(config);
return multer({ storage: multer.diskStorage({
destination: path.resolve(config.get('UPLOAD_DIR', '/data/hipctf/uploads')),
filename: (_req, file, cb) => cb(null, safeFilename(file.originalname)),
}), limits });
}
```
2. New `uploads.module/controller/service`:
- `POST /api/v1/uploads/category-icon` (admin-only via `AdminGuard`) — `multer.single('file')`, global `fileSize` limit applied via `buildMulter(config)`. Returns `{ id, storedPath, originalFilename, size }`.
- `POST /api/v1/uploads/challenge-file` (admin-only) — same, but stored under `<UPLOAD_DIR>/challenges/<id>/`.
3. Serve via existing `app.use('/uploads', express.static(uploadDir))` — already in `main.ts`. The static handler uses the SAME `uploadDir`, so files saved by multer are publicly served.
4. Tests: `uploads.spec.ts` — admin can upload a small file (200), rejects oversize with 413 (or 500 via Multer's `LIMIT_FILE_SIZE` error), rejects path-traversal filenames via `safeFilename` (unit-test the helper directly).
### 3.6 ThemeLoaderService — require + validate the 10 themes
1. After `loadAll()` populates `this.themes`:
- Compute `missing = THEME_IDS.filter(id => !this.themes.has(id))`.
- If `missing.length > 0`:
- Log a `warn` listing missing ids.
- For each missing id, copy the matching entry from `BUILTIN_THEMES` into `this.themes` so `THEME_IDS.length === this.themes.size` after init.
2. Read the configured theme key: `await this.settings.get('themeKey', 'classic')` (SettingsService is `@Global()` already).
3. If `configuredKey` is not in `THEME_IDS` OR not in `this.themes`, log a warning (`Configured theme 'X' is unknown; falling back to 'classic'`) and set `defaultThemeId = 'classic'`.
4. Otherwise set `defaultThemeId = configuredKey`.
5. `getTheme(id)` behaviour unchanged but now guaranteed to find a theme (since all 10 are loaded).
6. Test: `theme-required.spec.ts` — `theme-loader.spec.ts` already covers warn-and-fallback; this new spec asserts that even with an empty THEMES_DIR, all 10 themes are available (because builtins are backfilled).
### 3.7 Tests added
- `database-init.spec.ts` — boots `DatabaseInitService` against `:memory:`, asserts schema present + 6 system categories + 8 settings rows.
- `admin-validation.spec.ts` — covers all four admin DTOs (create, update, param, query).
- `uploads.spec.ts` — admin upload happy-path + safeFilename unit + oversize rejection.
- `theme-required.spec.ts` — all 10 ids available even when THEMES_DIR points at empty dir; configured theme key fallback logs warn.
- `auth-interceptor.spec.ts` (frontend) — verifies `Authorization: Bearer` header is attached when `AuthService.getAccessToken()` returns a token.
## 4. Test Strategy
- New unit/integration specs listed above.
- No new heavyweight suites; all are pure-Jest with no extra containers.
- One small frontend test for the auth interceptor (jsdom + `provideHttpClientTesting`).
- All tests runnable with `npm test` from repo root.
## 5. Persistent `/data` Usage
No change to existing `/data` layout. Upload tests use `UPLOAD_DIR=/tmp/...` via env, mirroring the existing convention.
## 6. Definition of Done
- `git log -1 --stat` shows the new commit containing the full scaffold diff on the `feature-820-1784637300954` branch.
- `npm test` passes (46 existing + new suites green).
- `npm run build` succeeds.
- Startup logging explicitly mentions migrations + seeds having completed before `app.listen` resolves.
- `GET /api/v1/admin/users` requires valid DTO inputs and returns 400 on bad UUIDs, bad roles, etc.
- `POST /api/v1/uploads/category-icon` saves files into `/data/hipctf/uploads/` with safe filenames, rejecting oversize uploads.
- `ThemeLoaderService.listThemes().length === 10` regardless of disk state; configured theme key validated.
- Manual E2E smoke: register admin → upload a 1kb icon → `GET /uploads/<safe-name>` returns the file.