Files
HIPCTF2/.kilo/plans/844.md
T

218 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 96102 (`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 4860 + `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 99102 (`this.form.markAllAsTouched(); if (this.form.invalid) return;`) |
| Submit button enabled while invalid (only disabled while in-flight) | `setup-create-admin.component.html` lines 5466 (`[disabled]="submitting()"`) |
| Non-dismissible modal (backdrop click swallowed, Esc swallowed) | `setup-create-admin.component.ts` lines 8997 (`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 1727)
serves the SPA only when an `index.html` is found inside either
`<FRONTEND_DIST>/index.html` or `<FRONTEND_DIST>/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
(`<app-root></app-root>`), 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.