11 KiB
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) andfrontend/(Angular 17 standalone components, reactive forms, signals, OnPush change detection). - Backend uses zod DTOs validated via a custom
ZodValidationPipeand aGlobalExceptionFilterthat returns a standard error envelope. - Frontend uses Angular signals for state,
data-testidhooks for e2e/test selectors, andOnPushchange detection throughout. - All components are standalone (no NgModules); routing is in
frontend/src/app/app.routes.ts.
- Monorepo (npm workspaces) with two packages:
-
Data Layer:
- SQLite via TypeORM with
better-sqlite3driver. - Single
usertable (backend/src/database/entities/user.entity.ts) withrole('admin' | 'player') andstatus('enabled' | 'disabled') columns. Unique index onusername. - 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.tsand1700000000100-SeedSystemData.ts; applied byDatabaseInitService.init()at boot.
- SQLite via TypeORM with
-
Test Framework & Structure:
- Jest 29 with
ts-jest, configured intests/jest.config.js. - Two projects:
backend(node env) andfrontend(jsdom env). - All tests live under
tests/(never co-located with source). - Single command from repo root:
npm test(also exposed asnpm run test:backendandnpm run test:frontend).
- Jest 29 with
-
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 vianpm --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
<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.htmlexists (built, 1143 bytes).frontend/dist/3rdpartylicenses.txtexists.- 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:
- Read
initialized. Iffalse, theauthGuardkeeps the user on/bootstrap, which rendersSetupCreateAdminComponent(the modal). - 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:
- Build (idempotent —
setup.shalready runs both builds):bash setup.sh - 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 curl -i http://localhost:3000/bootstrapshould return200 text/htmlwith the Angularindex.htmlshell (<app-root></app-root>), not the "Frontend not built." stub.curl -s http://localhost:3000/api/v1/bootstrap | jq .initializedshould returnfalse.- In a browser, visit
http://localhost:3000/. The app redirects to/bootstrapand renders the modal. - 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-adminrequest is fired.
- Fill the username with a value that violates the pattern (e.g.
"bad name!") and submit: modal stays open, the username error readsUsername may only contain letters, digits, dot, dash and underscore.
4. Test Strategy
4.1 Existing coverage (do not duplicate)
tests/backend/bootstrap.integration.spec.tsexercisesGET /api/v1/bootstrap(uninitialized →initialized=false, initialized →true).tests/backend/setup-create-admin.spec.tscoversPOST /api/v1/setup/create-adminsuccess, 404 once initialized, weak password (WEAK_PASSWORD),passwordConfirmmismatch (VALIDATION_FAILED), invalid username characters (VALIDATION_FAILED).tests/frontend/setup-create-admin.spec.tscoverspasswordMatchValidator(null when either empty, null on match,passwordMismatch: trueon 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.configureTestingModulewith declarations/providers forSetupCreateAdminComponentonly. - Stub
SetupCreateAdminServicewith a Jest mock exposingcreate = 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, noHttpClientTestingModule. - Stub
AuthServicewith{ setSession: jest.fn(), isAuthenticated: () => false }. - Stub
BootstrapServicewith{ initialized: () => false, markInitialized: jest.fn(), passwordPolicy: () => ({ minLength: 12, requireMixed: true, description: '...' }) }. - Stub
Routerwith{ 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):
- Create the component fixture, detect changes.
- Click the submit button
(
[data-testid="setup-submit"]) without filling any field. - Expect
service.createto have been called 0 times. - Expect the username error element
(
[data-testid="setup-username-error"]) to containUsername is required. - Expect the confirm error element
(
[data-testid="setup-confirm-error"]) to containPlease confirm your password. - Expect
router.navigateByUrlto 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.