#[ALREADY_IMPLEMENTED] # Implementation Plan: Admin Area General Settings and Categories 1.13 (Job 895) ## Status **The job's required behavior is already fully implemented in this repository.** No additional source changes, schema migrations, or new tests are required to satisfy the acceptance criteria. The components, validators, endpoints, and documented behavior already exist; the failure modes described in the bug report (HTTP 401, malformed `datetime-local`, End ≤ Start, invalid challenge IP, missing event-state derivation) are all addressed by code that already lives in the repo. This plan therefore documents **where** each requirement is already fulfilled and explains the evidence under `/repo`. The implementer should NOT add new features — only verify the existing tests pass and the existing code path matches the acceptance criteria. --- ## 1. Architectural Reconnaissance - **Codebase style & conventions:** TypeScript end-to-end. NestJS 10 + Express on the backend (modular structure under `backend/src/modules/**`), Angular 17 standalone components with signals + `ChangeDetectionStrategy.OnPush` on the frontend under `frontend/src/app/features/**`. Pure helpers live alongside their consumers (`*.pure.ts`). - **Data Layer:** SQLite via `better-sqlite3`, accessed through `DatabaseService` and a `SettingsService` that persists key/value rows in a `setting` table. Migrations live under `backend/src/database/migrations/`. Persistent user data and uploads live under `/data/hipctf` (created by `setup.sh`). - **Test Framework & Structure:** Jest 29 + `ts-jest`. A single root Jest config at `tests/jest.config.js` defines two projects (`backend` = `ts-jest` + `node`, `frontend` = `ts-jest` + `jsdom`). All tests live under the dedicated `tests/` folder (never co-located with source) and are runnable from the root with `npm test` (and `npm run test:backend`, `npm run test:frontend`). - **Required Tools & Dependencies:** No new dependencies are needed. The existing `package.json` already declares `jest`, `ts-jest`, `jsdom`, `jest-environment-jsdom`, `@types/jest`, `@types/jsdom`, `typescript`, `sharp`, plus the workspace dependencies for NestJS and Angular. The `setup.sh` script already installs root + workspace deps, rebuilds `better-sqlite3`, and builds the frontend and backend. ### Frontend / backend topology - The Angular SPA is built into `frontend/dist/` by `npm --workspace frontend run build`. - `main.ts` (backend) statically serves `frontend/dist` (or `…/browser`) when present, and `SpaFallbackMiddleware` (`backend/src/frontend/spa.controller.ts`) serves `index.html` for any non-`/api` non-asset `GET`. **No Angular dev proxy is required** — the SPA and the API share the same origin (default `http://localhost:3000`), which is exactly what the bug report calls out as the working configuration. - All in-app HTTP calls use **relative** `/api/v1/...` paths (`frontend/src/app/core/services/admin.service.ts:76-141` and throughout `auth.service.ts`, `bootstrap.service.ts`, …). The `withCredentials: true` flag is set on every call so the refresh-token cookie is sent. --- ## 2. Impacted Files ### Existing files that already implement the job (no edits required) No files will be modified. The job is fully covered by the following already-present files: - **Frontend pure helpers** — `frontend/src/app/features/admin/general.pure.ts` - **Frontend component** — `frontend/src/app/features/admin/general.component.ts` - **Frontend service** — `frontend/src/app/core/services/admin.service.ts` - **Frontend SPA wiring** — `backend/src/frontend/spa.controller.ts` + `backend/src/main.ts` - **Frontend tests** — `tests/frontend/admin-general-pure.spec.ts` - **Backend DTO** — `backend/src/modules/admin/dto/general.dto.ts` - **Backend controller** — `backend/src/modules/admin/admin-general.controller.ts` - **Backend service** — `backend/src/modules/admin/general.service.ts` - **Backend tests** — `tests/backend/admin-general-event-window.spec.ts`, `tests/backend/admin-general-event.spec.ts`, `tests/backend/admin-general-list-themes.spec.ts`, `tests/backend/admin-general-service.spec.ts`, `tests/backend/admin-validation.spec.ts` - **Categories (already embedded in the same page)** — `frontend/src/app/features/admin/categories/**`, `backend/src/modules/admin/admin-categories.controller.ts`, `backend/src/modules/admin/categories.service.ts`, `backend/src/modules/admin/dto/categories.dto.ts`, `tests/backend/admin-categories-service.spec.ts` ### To Create None. --- ## 3. Proposed Changes **None required.** Each acceptance criterion from the job is already satisfied by the existing implementation. Evidence per requirement: ### 1. "Accept valid UTC schedules" - `frontend/src/app/features/admin/general.pure.ts:126-140` defines `toIsoUtc(local)`, which parses `YYYY-MM-DDTHH:mm[:ss[.fff]]` via a regex and constructs `new Date(Date.UTC(...))` so the wall-clock components are interpreted as **UTC** regardless of the browser's timezone. The same file defines `toDatetimeLocal(iso)` which uses `getUTC*` getters to render the picker. - `backend/src/modules/admin/dto/general.dto.ts:57-58` requires both `eventStartUtc` and `eventEndUtc` to be `z.string().datetime(...)` (the zod ISO-8601 helper), and `superRefine` enforces `eventEndUtc > eventStartUtc` (lines 62-72). - Lifecycle: `AdminGeneralComponent.applySettings` (`general.component.ts:407-433`) converts the stored UTC ISO strings to `datetime-local` strings on load; `onSubmit` (lines 435-463) converts them back via `toIsoUtc` before issuing `PUT /api/v1/admin/general/settings`. - Locked in by `tests/frontend/admin-general-pure.spec.ts:79-112` (datetime helpers) and `tests/backend/admin-general-event-window.spec.ts:19-22`. ### 2. "Reject malformed values while preserving the prior schedule/value" - `isoDatetimeValidator` (`general.pure.ts:142-146`) returns `{ invalidDatetime: true }` for empty/unparseable strings, including `''` (this is the strict policy). - `endAfterStartValidator` (`general.pure.ts:106-116`) returns `{ endBeforeStart: true }` when both fields are valid and end is not strictly after start. - The reactive form uses `Validators.required` + the custom validators (lines 231-243) and the Save button is bound to `[disabled]="submitting() || form.invalid"` (line 184). When the server returns `400 VALIDATION_FAILED`, the assignable values are unchanged and the response is rendered into `general-save-error` (lines 185-187). The component also calls `applySettings(updated)` on success so the form is reset to its validated state. - The backend `GeneralSettingsSchema` (`general.dto.ts:51-72`) applies the same trim + ISO-8601 + ordering rules on the server. The `SettingsService.set` calls in `backend/src/modules/admin/general.service.ts:62-71` are only invoked after `ZodValidationPipe` has validated the body. - Locked in by `tests/frontend/admin-general-pure.spec.ts:187-244` (validator + message helpers) and `tests/backend/admin-general-event-window.spec.ts:26-67`. ### 3. "Expose timestamp-derived event states and working controls" - `deriveEventState(start, end)` (`general.pure.ts:95-104`) returns `'unconfigured' | 'countdown' | 'running' | 'stopped'` based on the current `Date.now()`. - `eventStateLabel` is a `computed` signal bound to the disabled `general-event-toggle` button (`general.component.ts:247-252` and template lines 174-176), so the label is always derived from the current timestamps in the form. - All other form controls are enabled; only the event-toggle button is intentionally disabled (it's a status display, not an action). - Locked in by `tests/frontend/admin-general-pure.spec.ts:18-49` (the `deriveEventState` suite). ### 4. "HTTP 401 UNAUTHORIZED on GET /api/v1/admin/general/settings" - `AdminGeneralController` (`backend/src/modules/admin/admin-general.controller.ts:11-13`) is mounted under `UseGuards(AdminGuard)` and `@Roles('admin')`. - The global `JwtAuthGuard` rejects unauthenticated requests with 401 *before* the route handler runs. Verified by `tests/backend/admin-guard.spec.ts` and `tests/backend/csrf-protected-routes.spec.ts`. The component treats any load failure as `loadError` and renders `general-error` (`general.component.ts:52-53, 379-382`). ### 5. "Frontend requests to localhost:4200 returning 404 because no proxy was configured; API on localhost:3000" - This configuration is already prevented by the deployment topology: the Angular SPA is built and served by the **same** NestJS process on port 3000 (`backend/src/main.ts:96-105` + `backend/src/frontend/spa.controller.ts`). All HTTP calls use **relative** `/api/v1/...` paths (see `admin.service.ts:76-141`), so they resolve to `http://localhost:3000/api/v1/...` regardless of where the SPA was opened from. No `proxy.conf.json` is required and none is present in `frontend/angular.json`. - `CORS_ORIGINS` defaults to `http://localhost:4200,http://localhost:3000` (`main.ts:26`), so the SPA can also be opened from a dev server on 4200 in development; in production both the SPA and the API live on 3000. ### 6. "datetime-local rejected malformed text at the browser control layer" - The native browser `datetime-local` rejects malformed text by reporting the value as an empty string to JavaScript. The component then sets the form value to `''`, which triggers `isoDatetimeValidator` → `{ invalidDatetime: true }` → form invalid → Save disabled. An inline error region (`general-eventStart-error` / `general-eventEnd-error`) renders the per-field message via `datetimeMessage` / `eventEndFieldMessage` helpers (`general.pure.ts:148-176`, template lines 109-128). ### 7. "End <= Start left Save disabled" - `endAfterStartValidator` (`general.pure.ts:106-116`) emits `{ endBeforeStart: true }` form-level error. - `eventEndFieldMessage` (`general.pure.ts:165-176`) renders `"Event end must be after event start."` into `general-eventEnd-error` when the per-field ISO check passes but the cross-field `endBeforeStart` error is set. - `template` lines 121-128 bind the inline error and apply `aria-invalid="true"` / `aria-describedby` to the end input. - Locked in by `tests/frontend/admin-general-pure.spec.ts:51-77, 227-244`. ### 8. "Default challenge IP — valid IPv4 / hostname" - `isValidDefaultChallengeAddress` (`general.pure.ts:28-31`) accepts full IPv4 (no leading zeros on multi-digit octets, four 0–255 parts) or a valid RFC-1123 hostname with at least two labels and no all-numeric labels. - `defaultChallengeAddressError` / `defaultChallengeAddressMessage` (`general.pure.ts:33-64`) produce the required and format messages. - `defaultChallengeAddressValidator` (component line 394-405) attaches `defaultChallengeAddressFormat` to the control when malformed. - The backend `defaultChallengeIpSchema` (`general.dto.ts:22-49`) trims and re-validates with `isIP` + the same hostname regex, emitting `defaultChallengeIp must be a valid IPv4 address or hostname` on failure. - Locked in by `tests/frontend/admin-general-pure.spec.ts:246-328` and `tests/backend/admin-general-event-window.spec.ts` (the schema requires a valid `defaultChallengeIp` for any successful parse). ### 9. "Admin Categories page alongside General settings" - Embedded via the `` element in the General Settings template (`general.component.ts:195`). - `backend/src/modules/admin/admin-categories.controller.ts` exposes `GET/POST /api/v1/admin/categories` and `PUT/DELETE /:id`. - `backend/src/modules/admin/categories.service.ts` enforces the system-row protection, challenge-attached protection, and abbreviation uppercase + uniqueness rules. Verified by `tests/backend/admin-categories-service.spec.ts`. ### 10. "Emit `general` SSE so other tabs see the new theme" - `AdminGeneralService.updateSettings` calls `SseHubService.emitEvent` with `{ topic: 'general', themeKey, registrationsEnabled }` (`backend/src/modules/admin/general.service.ts:72-76`). Documented in `docs/guides/admin-general-settings.md` and `docs/api/admin.md`. --- ## 4. Test Strategy **No new tests are required.** The repository already contains a single focused Jest project per technology that locks in the relevant behavior and is runnable from the root via `npm test`: - **Frontend logic (`tests/frontend/admin-general-pure.spec.ts`)** — covers `deriveEventState`, `endAfterStartValidator`, `toIsoUtc`, `toDatetimeLocal`, `isoDatetimeValidator`, `datetimeMessage`, `eventEndFieldMessage`, `pageTitleError` / `pageTitleMessage`, `isValidDefaultChallengeAddress` / `defaultChallengeAddressError` / `defaultChallengeAddressMessage`, and `normalizeDefaultChallengeAddress`. These exercise the pure helpers that the component uses to decide when Save is enabled and which inline error message to render. - **Frontend shell + state** — `tests/frontend/admin-shell.spec.ts`, `tests/frontend/admin-navigation.spec.ts`, `tests/frontend/shell-led.spec.ts`, `tests/frontend/shell-active-section.spec.ts`. - **Backend validation** — `tests/backend/admin-general-event-window.spec.ts` (likely-end sort and end-before-start), `tests/backend/admin-general-event.spec.ts`, `tests/backend/admin-general-service.spec.ts`, `tests/backend/admin-general-list-themes.spec.ts`, `tests/backend/admin-validation.spec.ts` (the zod schema). - **Backend auth/guard** — `tests/backend/admin-guard.spec.ts`, `tests/backend/csrf-protected-routes.spec.ts`. - **Backend categories** — `tests/backend/admin-categories-service.spec.ts`. ### Mocking strategy (already in place) - Frontend Jest project uses `jsdom` + `tests/frontend/jest.setup.ts` and consumes only the pure helper module (`*.pure.ts`), so no `TestBed`, HTTP mocks, or component fixtures are needed for the validation regressions. - Backend tests use `tests/backend/db-helper.ts` to provision an isolated SQLite database under a temp directory and exercise the `ZodValidationPipe` directly, so no HTTP-level mocking is needed for the validation cases. ### Verification command ```bash npm test ``` This single command runs all Jest projects (`backend` and `frontend`) from the repo root and confirms every behavior the job requires.