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

11 KiB
Raw Blame History

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 /bootstrapSetupCreateAdminComponent) + 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.