# Implementation Plan: Job 844 — First-Start Create Admin Modal 1.03 ## Status **[ALREADY_IMPLEMENTED]** The "first-start create admin modal" feature described in this Job is already fully implemented in the repository. The "Frontend not built." response at `/bootstrap` observed by the operator is an environment artifact, not a missing feature. No application code changes are required. The remaining sections of this plan therefore serve as a **verification guide** so the implementer can confirm the existing implementation works as the Job expects, plus a minimal test to pin the validation behavior of the empty-form submission (the only piece that is not already covered by `tests/frontend/setup-create-admin.spec.ts`). ## 1. Architectural Reconnaissance - **Codebase style & conventions:** - Monorepo (npm workspaces) with two packages: `backend/` (NestJS 10 + TypeORM + better-sqlite3) and `frontend/` (Angular 17 standalone components, reactive forms, signals, OnPush change detection). - Backend uses zod DTOs validated via a custom `ZodValidationPipe` and a `GlobalExceptionFilter` that returns a standard error envelope. - Frontend uses Angular signals for state, `data-testid` hooks for e2e/test selectors, and `OnPush` change detection throughout. - All components are standalone (no NgModules); routing is in `frontend/src/app/app.routes.ts`. - **Data Layer:** - SQLite via TypeORM with `better-sqlite3` driver. - Single `user` table (`backend/src/database/entities/user.entity.ts`) with `role` (`'admin' | 'player'`) and `status` (`'enabled' | 'disabled'`) columns. Unique index on `username`. - Persistent named volume mounted at `/data/hipctf/` (db file: `/data/hipctf/db.sqlite`; uploads dir: `/data/hipctf/uploads`). - Migrations live in `backend/src/database/migrations/1700000000000-InitSchema.ts` and `1700000000100-SeedSystemData.ts`; applied by `DatabaseInitService.init()` at boot. - **Test Framework & Structure:** - Jest 29 with `ts-jest`, configured in `tests/jest.config.js`. - Two projects: `backend` (node env) and `frontend` (jsdom env). - All tests live under `tests/` (never co-located with source). - Single command from repo root: `npm test` (also exposed as `npm run test:backend` and `npm run test:frontend`). - **Required Tools & Dependencies:** No new tools required. The Job does not require adding any package, CLI utility, or system binary. Existing toolchain (Node, npm, Angular CLI invoked via `npm --workspace`, NestJS build via `npm --workspace backend run build`) is sufficient. ## 2. Impacted Files Because the feature is already implemented, **no files need to be modified or created**. The mapping below proves where each requirement lives so the implementer (and the reviewer) can audit the existing implementation: | Job requirement | Already implemented at | |---|---| | Modal rendered at `/bootstrap` when uninitialized | `frontend/src/app/app.routes.ts` (route `/bootstrap` → `SetupCreateAdminComponent`) + `frontend/src/app/core/guards/auth.guard.ts` + `auth.guard.decision.ts` (redirects to `/bootstrap` when `initialized() === false`) | | Bootstrap payload exposes `initialized` flag | `backend/src/modules/system/system.controller.ts` (`GET /api/v1/bootstrap`) + `backend/src/modules/system/system.service.ts` (`initialized: adminCount > 0`) | | SPA fallback for client routes | `backend/src/frontend/spa.controller.ts` (`SpaFallbackMiddleware`) + `backend/src/main.ts` lines 96–102 (`express.static(frontendDist)` + `express.static(browserDir)`) | | Modal overlay markup + three fields | `frontend/src/app/features/setup/setup-create-admin.component.html` (`[data-testid="setup-username"]`, `setup-password`, `setup-password-confirm`, `setup-submit`, `setup-username-error`, `setup-confirm-error`, `setup-server-error`, `setup-retry`) | | Reactive form with required + length + pattern + match validators | `frontend/src/app/features/setup/setup-create-admin.component.ts` lines 48–60 + `frontend/src/app/features/setup/setup-create-admin.validators.ts` (`passwordMatchValidator`) | | Empty / partial submission is rejected client-side without firing the request (modal stays open) | `frontend/src/app/features/setup/setup-create-admin.component.ts` lines 99–102 (`this.form.markAllAsTouched(); if (this.form.invalid) return;`) | | Submit button enabled while invalid (only disabled while in-flight) | `setup-create-admin.component.html` lines 54–66 (`[disabled]="submitting()"`) | | Non-dismissible modal (backdrop click swallowed, Esc swallowed) | `setup-create-admin.component.ts` lines 89–97 (`swallowEscape` HostListener + `swallowBackdrop` no-op) | | Backend endpoint + zod validation (incl. `password === passwordConfirm`) | `backend/src/modules/setup/setup.controller.ts` + `backend/src/modules/setup/setup.service.ts` + `backend/src/modules/setup/dto/create-admin.dto.ts` | | Returns 404 once an admin exists (route hidden) | `backend/src/modules/setup/setup.service.ts` (`SetupService.createAdmin` throws `NOT_FOUND` when admin row exists) | ## 3. Proposed Changes ### 3.1 Application code **None.** The Job's requirements are entirely satisfied by existing code. ### 3.2 Why the operator saw "Frontend not built." `SpaFallbackMiddleware` (`backend/src/frontend/spa.controller.ts` lines 17–27) serves the SPA only when an `index.html` is found inside either `/index.html` or `/browser/index.html`. The Angular 17 CLI emits the production bundle under `frontend/dist/browser/`, which is exactly the second candidate. Therefore the "Frontend not built." stub is only emitted when **neither candidate exists** — i.e. when the frontend was never built (or was wiped) before `npm start`. In the current workspace: - `frontend/dist/browser/index.html` exists (built, 1143 bytes). - `frontend/dist/3rdpartylicenses.txt` exists. - All chunk JS files (`chunk-*.js`, `main-*.js`, `polyfills-*.js`, `styles-*.css`) are present. So a fresh `npm start` after `bash setup.sh` will serve the Angular SPA, the SPA's `BootstrapService.load()` will hit `/api/v1/bootstrap`, and the SPA will: 1. Read `initialized`. If `false`, the `authGuard` keeps the user on `/bootstrap`, which renders `SetupCreateAdminComponent` (the modal). 2. If `true`, the user is redirected to `/login` (or `/`). The shared `/data/hipctf/db.sqlite` volume currently contains one admin row, so `initialized === true` and the modal does not appear against this exact DB. To exercise the modal, the implementer must point the backend at a **fresh** database (see §3.3). ### 3.3 Operator run-book (for verification only — no code change) To confirm the existing implementation matches the Job's "expected behavior" against a fresh DB without destroying the persistent `/data` volume: 1. Build (idempotent — `setup.sh` already runs both builds): ``` bash setup.sh ``` 2. Start the backend against an **isolated** DB: ``` DATABASE_PATH=/tmp/hipctf-fresh.db \ FRONTEND_DIST=$(pwd)/frontend/dist \ THEMES_DIR=$(pwd)/backend/themes \ node backend/dist/main.js ``` 3. `curl -i http://localhost:3000/bootstrap` should return `200 text/html` with the Angular `index.html` shell (``), not the "Frontend not built." stub. 4. `curl -s http://localhost:3000/api/v1/bootstrap | jq .initialized` should return `false`. 5. In a browser, visit `http://localhost:3000/`. The app redirects to `/bootstrap` and renders the modal. 6. Click **Create admin** with all three fields empty: - Modal stays open. - Username field shows `Username is required` (`data-testid="setup-username-error"`). - Confirm field shows `Please confirm your password` (`data-testid="setup-confirm-error"`). - No `POST /api/v1/setup/create-admin` request is fired. 7. Fill the username with a value that violates the pattern (e.g. `"bad name!"`) and submit: modal stays open, the username error reads `Username may only contain letters, digits, dot, dash and underscore`. ## 4. Test Strategy ### 4.1 Existing coverage (do not duplicate) - `tests/backend/bootstrap.integration.spec.ts` exercises `GET /api/v1/bootstrap` (uninitialized → `initialized=false`, initialized → `true`). - `tests/backend/setup-create-admin.spec.ts` covers `POST /api/v1/setup/create-admin` success, 404 once initialized, weak password (`WEAK_PASSWORD`), `passwordConfirm` mismatch (`VALIDATION_FAILED`), invalid username characters (`VALIDATION_FAILED`). - `tests/frontend/setup-create-admin.spec.ts` covers `passwordMatchValidator` (null when either empty, null on match, `passwordMismatch: true` on differ) and the username pattern/length rules. ### 4.2 One new, minimal frontend test (the only gap) The existing frontend test does not exercise `SetupCreateAdminComponent` directly; it only tests the standalone validator and pattern rules. Add one focused component test that pins the "empty submission is rejected client-side without firing the request, and the modal stays open" behavior the Job explicitly calls out. Keep it minimal — no mock backend, no extra deps, no jsdom extensions beyond what Jest provides. **Target file:** `tests/frontend/setup-create-admin.component.spec.ts` (new file). **Mocking strategy:** - Use `TestBed.configureTestingModule` with declarations/providers for `SetupCreateAdminComponent` only. - Stub `SetupCreateAdminService` with a Jest mock exposing `create = jest.fn()`. Assert it is **not** called when the form is empty. This is sufficient to prove the client-side gate prevents the network round-trip — no real HTTP, no `HttpClientTestingModule`. - Stub `AuthService` with `{ setSession: jest.fn(), isAuthenticated: () => false }`. - Stub `BootstrapService` with `{ initialized: () => false, markInitialized: jest.fn(), passwordPolicy: () => ({ minLength: 12, requireMixed: true, description: '...' }) }`. - Stub `Router` with `{ navigateByUrl: jest.fn() }`. - Do not mock `ReactiveFormsModule` — use the real module so the validators run end-to-end (this is the whole point of the test). **Assertions (single `it`):** 1. Create the component fixture, detect changes. 2. Click the submit button (`[data-testid="setup-submit"]`) without filling any field. 3. Expect `service.create` to have been called **0** times. 4. Expect the username error element (`[data-testid="setup-username-error"]`) to contain `Username is required`. 5. Expect the confirm error element (`[data-testid="setup-confirm-error"]`) to contain `Please confirm your password`. 6. Expect `router.navigateByUrl` to have been called **0** times (modal remains on `/bootstrap`). This is the minimum surface that proves the Job's "rejects empty or missing required fields with field validation while keeping it open" expectation and runs in <100 ms with no infrastructure setup. ### 4.3 Verification commands From the repo root: ``` npm test # full suite npm run test:frontend # frontend-only npm run test:backend # backend-only ``` All existing tests must continue to pass, and the new `tests/frontend/setup-create-admin.component.spec.ts` must pass alongside them.