feat: Authenticated Shell: Header, Quick Tabs and Change Password
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
# Implementation Plan: Job 857 — Authenticated Shell (Header, Quick Tabs, Change Password, SSE-driven LED/Countdown)
|
||||
|
||||
## Status: IN PROGRESS — Build error to fix
|
||||
|
||||
Most of the implementation has been applied (backend service extensions, new endpoints, frontend stores, dumb components, smart `HomeComponent`, and tests). The Angular production build now fails with a discriminated-union narrowing error in `home.component.ts:185`. See **Section 6** for the exact, surgical fix required.
|
||||
|
||||
---
|
||||
|
||||
## 0. Already-Implemented Check
|
||||
|
||||
A scan of the current repository shows that Job 857 is **NOT** fully implemented:
|
||||
|
||||
* `frontend/src/app/features/home/home.component.ts` already renders a minimal header (page title + "Signed in as" line + admin nav link) but it does **not** include the LED, countdown, quick-jump tab menu, user dropdown, or change-password modal.
|
||||
* `frontend/src/app/app.routes.ts` has only `/`, `/login`, `/bootstrap`, `/admin`, plus the `/` shell child route for admin. There are no `/challenges`, `/scoreboard`, `/blog` child routes under the protected shell.
|
||||
* `backend/src/modules/auth/auth.controller.ts` exposes no `GET /api/v1/me` and no `POST /api/v1/auth/change-password` endpoint.
|
||||
* `backend/src/modules/system/system.controller.ts` exposes `GET /api/v1/event/stream` (Public), but no authenticated `GET /api/v1/events/status` (plural) protected-by-auth SSE.
|
||||
* `backend/src/modules/settings/*` does not yet expose `GET /api/v1/settings/event` (a public-readable event-window projection).
|
||||
* No `ChangePasswordModal` component exists; no `UserMenu` dropdown exists; no `QuickTabs` (Challenges/Scoreboard/Blog) component exists; no `EventStatusStore` (signals) exists.
|
||||
* `AuthService` has no `changePassword()` method.
|
||||
|
||||
No part of the new Job is fully done, so the plan proceeds with full implementation.
|
||||
|
||||
---
|
||||
|
||||
## 1. Architectural Reconnaissance
|
||||
|
||||
### Codebase style & conventions
|
||||
* **Backend**: NestJS 10 + TypeORM + better-sqlite3 (WAL). Modules use `@Module({...})`, providers are dependency-injected, services live in `backend/src/modules/<feature>/<feature>.service.ts`. Validation uses `ZodValidationPipe` (zod schemas in `dto/*.dto.ts`). A global `APP_GUARD = JwtAuthGuard` enforces auth by default; routes are opted-out with `@Public()`. CSRF is enforced on unsafe methods via `CsrfMiddleware` mounted in `main.ts`; `@SkipCsrf()` opts out. Errors are standardized via `ApiError` + `ERROR_CODES` and normalized by `GlobalExceptionFilter`.
|
||||
* **Frontend**: Angular 17.3 standalone components, `provideHttpClient(withInterceptors([...]))`, Angular Signals, `OnPush` change detection, `@if`/`@for` control flow. Smart/dumb component split. State lives in services using `signal()`/`computed()` (e.g. `AuthService`, `BootstrapService`). Lazy loading via `loadComponent`. Functional `CanActivateFn` route guards. Cookie-based auth (HttpOnly refresh cookie + Bearer access token in sessionStorage); HTTP requests use `withCredentials: true`.
|
||||
* **Tests**: Jest with two projects (`backend`, `frontend`) in `/repo/tests/jest.config.js`. Tests live in `/repo/tests/backend/**/*.spec.ts` and `/repo/tests/frontend/**/*.spec.ts`. Run via `npm test` from repo root. Backend tests typically bootstrap `AppModule` with `Test.createTestingModule({imports:[AppModule]})` against in-memory SQLite (`process.env.DATABASE_PATH=':memory:'`). Frontend tests use `jsdom` with `jest.setup.ts`.
|
||||
|
||||
### Data Layer
|
||||
* TypeORM + better-sqlite3, `user`, `refresh_token`, `setting`, `solve`, `challenge`, `category`, `challenge_file`, `blog_post` tables.
|
||||
* `setting` rows (`eventStartUtc`, `eventEndUtc`) are already seeded; `EventStatusService` derives `Stopped`/`Running` from `Date.now()` vs those ISO instants, returning `{status, startUtc, endUtc, serverNowUtc, countdownMs}` (all UTC).
|
||||
* No schema migrations are required for Job 857. The existing `solve` table already gives `pointsAwarded` (sum gives total points) and a derived rank is computed as `1 + count(user solves ranking above this user)` from the existing `solve` table.
|
||||
|
||||
### Test Framework & Structure
|
||||
* **Jest** (workspaces: backend + frontend). Single command: `npm test` (wired through root `package.json`).
|
||||
* Backend specs are Nest `INestApplication` integration tests OR plain `new EventStatusService({...})` unit tests; one helper exists (`tests/backend/db-helper.ts`) for test seeding.
|
||||
* Frontend specs use Angular `TestBed`, pure decision-function unit tests live alongside `.decision.ts` files, signal-store specs are pure logic assertions.
|
||||
|
||||
### Required Tools & Dependencies
|
||||
* **No new packages required.** All features rely on already-installed deps: NestJS `@Sse()`/RxJS, Angular Signals (`signal`, `computed`, `effect`), Angular Forms (`ReactiveFormsModule`), `argon2`, `passport-jwt`, `class-validator`-style zod. The existing `EventStatusService`, `SseHubService`, `JwtAuthGuard`, `CsrfMiddleware`, `@Public()`, `@Roles()` and `SettingsService` are reused. `setup.sh` does not need changes.
|
||||
|
||||
---
|
||||
|
||||
## 2. Impacted Files
|
||||
|
||||
### Backend — To Modify
|
||||
* `backend/src/modules/auth/auth.controller.ts` — add `GET /api/v1/me`, `POST /api/v1/auth/change-password`; remove `@Public()` on `POST /api/v1/auth/logout` so it becomes authenticated (the frontend already sends `Authorization` once logged in).
|
||||
* `backend/src/modules/auth/auth.service.ts` — add `getMe(userId) -> {id, username, role, rank, points, passwordPolicy}` (rank + points computed over `solve` table); add `changePassword(userId, oldPassword, newPassword, confirm)` that verifies old with `argon2.verify`, validates new with `validatePassword` + zod refinement, and persists a new Argon2id hash inside a transaction. The existing `logout()` already revokes the refresh token; only the controller decoration changes.
|
||||
* `backend/src/modules/auth/dto/auth.dto.ts` — add `ChangePasswordDtoSchema` (zod) with `oldPassword`, `newPassword` (with policy guards), `confirmNewPassword` refinement (`=== newPassword`).
|
||||
* `backend/src/modules/system/system.controller.ts` — add `GET /api/v1/events/status` SSE endpoint (NOT `@Public()`, authenticated) that emits `EventStatus` via the existing `EventStatusService` + `SseHubService`; add `GET /api/v1/settings/event` (`@Public()`) returning `{eventStartUtc, eventEndUtc}`.
|
||||
* `backend/src/common/services/event-status.service.ts` — extend interface to add an `unconfigured` branch (when `eventStartUtc`/`eventEndUtc` settings are missing/invalid) and a 4-state `state` field with `serverNowUtc`, `eventStartUtc`, `eventEndUtc`, `secondsToStart`, `secondsToEnd` to match the Job's required payload shape. Backward-compatible: existing `status: 'Stopped'|'Running'` callers keep working because `Stopped` is overloaded for both countdown and ended.
|
||||
* `backend/src/common/errors/error-codes.ts` — add `INVALID_OLD_PASSWORD`, `PASSWORD_POLICY`, `PASSWORDS_DO_NOT_MATCH`.
|
||||
* `backend/src/app.module.ts` — none required; the new pieces sit inside `AuthModule`/`SystemModule`.
|
||||
|
||||
### Backend — To Create
|
||||
* `backend/src/modules/users/users-rank.service.ts` — pure provider `rankOfUser(manager, userId)` returning `{ rank, points }` by summing `pointsAwarded` from `solve` and counting distinct users ahead. Registered in `UsersModule`.
|
||||
|
||||
### Frontend — To Modify
|
||||
* `frontend/src/app/app.routes.ts` — keep `/`, `/bootstrap`, `/login`. Inside the `''` shell route add lazy `loadChildren` for `challenges/`, `scoreboard/`, `blog/` placeholder pages.
|
||||
* `frontend/src/app/core/services/auth.service.ts` — add `changePassword({oldPassword, newPassword, confirmNewPassword})` and `logout()`; add `me()` (`GET /api/v1/me`) and extend `CurrentUser` with optional `rank`/`points`.
|
||||
* `frontend/src/app/features/home/home.component.ts` — convert from the existing inline header into a **smart shell container**.
|
||||
|
||||
### Frontend — To Create
|
||||
* `frontend/src/app/core/services/event-status.store.ts` — signals store.
|
||||
* `frontend/src/app/core/services/user.store.ts` — signals store for the authenticated user.
|
||||
* `frontend/src/app/features/shell/header/shell-header.component.ts` — **dumb** presentational component.
|
||||
* `frontend/src/app/features/shell/tabs/quick-tabs.component.ts` — **dumb** presentational component.
|
||||
* `frontend/src/app/features/shell/user-menu/user-menu.component.ts` — **dumb** dropdown component.
|
||||
* `frontend/src/app/features/shell/change-password/change-password-modal.component.ts` — **dumb** modal.
|
||||
* `frontend/src/app/features/shell/change-password/change-password-modal.validators.ts` — pure `passwordMatchValidator`.
|
||||
* `frontend/src/app/features/shell/change-password/password-feedback.ts` — pure `formatChangePasswordError()` helper.
|
||||
* `frontend/src/app/features/challenges/challenges.page.ts` — stub placeholder page.
|
||||
* `frontend/src/app/features/scoreboard/scoreboard.page.ts` — stub placeholder page.
|
||||
* `frontend/src/app/features/blog/blog.page.ts` — stub placeholder page.
|
||||
|
||||
### Test Files — To Create
|
||||
* `tests/backend/event-status-state.spec.ts` — 4-state derivation coverage.
|
||||
* `tests/backend/change-password.spec.ts` — integration: wrong-old / mismatched / weak / unauth.
|
||||
* `tests/backend/events-status-sse.spec.ts` — SSE initial payload + 401.
|
||||
* `tests/backend/me-endpoint.spec.ts` — `/api/v1/auth/me` shape + 401.
|
||||
* `tests/backend/rank-points.spec.ts` — pure unit test for `rankOfUser()`.
|
||||
* `tests/frontend/event-status.store.spec.ts` — pure store unit test (DD:HH:mm formatting + SSE message apply).
|
||||
* `tests/frontend/quick-tabs.spec.ts` — pure render test for the dumb quick-tabs component.
|
||||
* `tests/frontend/change-password-modal.spec.ts` — pure render test for the dumb modal.
|
||||
* `tests/frontend/shell-active-section.spec.ts` — pure helper unit test for `deriveActiveSectionFromUrl`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Proposed Changes
|
||||
|
||||
### 3.1 Database / Schema Migration
|
||||
No DB changes. Reuses existing `user`, `setting`, `solve`, `refresh_token` rows.
|
||||
|
||||
### 3.2 Backend Logic & APIs
|
||||
|
||||
#### a. Extend `EventStatusService`
|
||||
File: `backend/src/common/services/event-status.service.ts`
|
||||
* New `getState(now)` returning `EventStatePayload = {state: 'countdown'|'running'|'stopped'|'unconfigured', serverNowUtc, eventStartUtc, eventEndUtc, secondsToStart, secondsToEnd}`.
|
||||
* `getStatus(now)` is preserved for backward compatibility and computes its legacy `status`/`countdownMs` from the new `state` (`Stopped` for countdown, `Running` for running, `Stopped` with `0` for stopped/unconfigured).
|
||||
|
||||
#### b. Add `GET /api/v1/me`
|
||||
File: `backend/src/modules/auth/auth.controller.ts`
|
||||
* Authenticated (no `@Public()`). Returns `{id, username, role, rank, points}`.
|
||||
* `rank`/`points` computed via `UsersRankService`.
|
||||
|
||||
#### c. Add `POST /api/v1/auth/change-password`
|
||||
* Authenticated, CSRF enforced.
|
||||
* Body via `ZodValidationPipe(ChangePasswordDtoSchema)`.
|
||||
* Returns 204 on success, 400 `PASSWORDS_DO_NOT_MATCH` / 400 `PASSWORD_POLICY` / 401 `INVALID_OLD_PASSWORD`.
|
||||
|
||||
#### d. Make `POST /api/v1/auth/logout` authenticated
|
||||
* Remove `@Public()` on the `logout()` method.
|
||||
|
||||
#### e. `AuthService.changePassword()` method
|
||||
File: `backend/src/modules/auth/auth.service.ts`
|
||||
* `changePassword(userId, dto)`:
|
||||
1. Validate `newPassword === confirmNewPassword`.
|
||||
2. Reject when `oldPassword === newPassword` with `PASSWORD_POLICY`.
|
||||
3. Verify old via `argon2.verify`.
|
||||
4. Validate new via `validatePassword()` (rethrow with `PASSWORD_POLICY` code).
|
||||
5. Re-hash with Argon2id and persist.
|
||||
6. Revoke all existing refresh tokens for the user.
|
||||
|
||||
#### f. Add public `GET /api/v1/settings/event`
|
||||
File: `backend/src/modules/system/system.controller.ts`
|
||||
* `@Public()` endpoint returning `{eventStartUtc, eventEndUtc}`.
|
||||
|
||||
#### g. Add authenticated `GET /api/v1/events/status` SSE
|
||||
* NOT `@Public()`.
|
||||
* Emits initial state immediately on connect, plus 60s tick and hub pushes.
|
||||
* Each `data` frame: `{state, serverNowUtc, eventStartUtc, eventEndUtc, secondsToStart, secondsToEnd}`.
|
||||
|
||||
### 3.3 Frontend UI Integration
|
||||
|
||||
#### a. Routes
|
||||
File: `frontend/src/app/app.routes.ts`
|
||||
* Keep `bootstrap`, `login`, `**` fallback.
|
||||
* Shell `''` route gains lazy children: `'' → challenges`, `scoreboard`, `blog`, `admin` (already admin-gated).
|
||||
* Default child redirects to `/challenges`.
|
||||
|
||||
#### b. `AuthService` additions
|
||||
File: `frontend/src/app/core/services/auth.service.ts`
|
||||
* `logout()`, `me()`, `changePassword(dto)` with discriminated `ChangePasswordResult = ChangePasswordSuccess | ChangePasswordFailure`.
|
||||
* Extend `CurrentUser` with optional `rank?: number | null` and `points?: number`.
|
||||
|
||||
#### c. `EventStatusStore`
|
||||
File: `frontend/src/app/core/services/event-status.store.ts`
|
||||
* Signals `state`, `serverNowUtc`, `eventStartUtc`, `eventEndUtc`, private `anchorDeltaMs`.
|
||||
* `computed countdownText` formats as `DD:HH:mm` (`"Event ended"` while stopped; `""` while unconfigured).
|
||||
* `applyServerStatus(payload)`, `start(createSource)`, `stop()` lifecycle. Cleanup via `inject(DestroyRef)`.
|
||||
|
||||
#### d. `UserStore`
|
||||
File: `frontend/src/app/core/services/user.store.ts`
|
||||
* Signals `user`, `loading`, `error`. `computed rankText` formats `"Rank: <r> - Points: <p>"`.
|
||||
* `loadMe()` calls `AuthService.me()`.
|
||||
|
||||
#### e. `HomeComponent` smart container
|
||||
File: `frontend/src/app/features/home/home.component.ts`
|
||||
* Injects `EventStatusStore`, `UserStore`, `AuthService`, `Router`.
|
||||
* Subscribes to `NavigationEnd` and exposes `currentUrl` signal → `activeSection = computed(deriveActiveSectionFromUrl(currentUrl))`.
|
||||
* `ngOnInit()` calls `UserStore.loadMe()` and starts the SSE consumer via `EventStatusStore.start(() => new EventSource('/api/v1/events/status', { withCredentials: true }))`.
|
||||
* `ngOnDestroy()` calls `EventStatusStore.stop()`.
|
||||
|
||||
#### f. Dumb components
|
||||
* `ShellHeaderComponent` — signal inputs, outputs; LED via host bindings; user menu inline.
|
||||
* `QuickTabsComponent` — signal inputs `tabs`/`active`, `tabChange` output.
|
||||
* `ChangePasswordModalComponent` — three reactive controls with `passwordMatchValidator`; emits `submit({oldPassword,newPassword,confirmNewPassword})` and `cancel`; mode `'self'|'admin-reset'` input.
|
||||
|
||||
---
|
||||
|
||||
## 4. Test Strategy
|
||||
|
||||
### Target unit test files
|
||||
* **Backend** (`tests/backend/`):
|
||||
* `event-status-state.spec.ts` — covers countdown / running / stopped / unconfigured.
|
||||
* `change-password.spec.ts` — covers wrong-old / mismatched / weak / unauth (HTTP surface).
|
||||
* `events-status-sse.spec.ts` — covers 401 + initial payload shape.
|
||||
* `me-endpoint.spec.ts` — covers 401 + payload shape.
|
||||
* `rank-points.spec.ts` — covers points sum + 1-based rank.
|
||||
* **Frontend** (`tests/frontend/`):
|
||||
* `event-status.store.spec.ts` — DD:HH:mm formatting + SSE message apply.
|
||||
* `change-password-modal.spec.ts` — form validation, submit/cancel emission, error rendering.
|
||||
* `quick-tabs.spec.ts` — active class + tabChange emission.
|
||||
* `shell-active-section.spec.ts` — pure helper unit test.
|
||||
|
||||
### Mocking Strategy
|
||||
* **Backend SSE**: same supertest `.buffer(true).parse(...)` pattern already used by `tests/backend/sse-flatten.spec.ts`. Auth via `Authorization: Bearer <token>`.
|
||||
* **Backend DB**: in-memory SQLite + `initDb(app)`.
|
||||
* **Frontend HTTP**: `TestBed.configureTestingModule({ imports: [...] })` + `setInput()` for inputs. No HTTP needed for these pure-logic specs.
|
||||
* **Frontend SSE consumer**: a `FakeEventSource` implementing `EventSourceLike`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Cross-cutting Notes
|
||||
|
||||
* **Reuse over reinvention**: The Job's "single source of truth for state derivation" maps cleanly to the existing `EventStatusService`. We **extend** it (do not bypass it) and surface the SSE through `SseHubService` so the existing 1-second tick (and any future scheduler) drives both the public `event/stream` AND the new authenticated `events/status`.
|
||||
* **UX-only route guards** + **backend enforcement** are already true on `/` (existing `authGuard` + global `JwtAuthGuard`). All new routes inherit the same.
|
||||
* **Cookie auth**: No tokens in localStorage. The change-password endpoint uses the same HttpOnly refresh cookie + Bearer access token pattern as `login`/`register`. Frontend uses `withCredentials: true` on every call.
|
||||
* **Error codes**: `INVALID_OLD_PASSWORD`, `PASSWORD_POLICY`, `PASSWORDS_DO_NOT_MATCH` are added to `ERROR_CODES`; mapped to stable HTTP statuses (`401` / `400` / `400`).
|
||||
* **Persistence**: No new data on `/data` is required for this job. The `/data/hipctf` directory created by `setup.sh` continues to host the SQLite DB; settings live inside the DB.
|
||||
* **Conformance to skills**: Standalone components, OnPush, signal inputs/outputs, dumb component split, lazy-loaded routes, functional `CanActivateFn` — all already enforced on existing code and applied to new code. SSE side: backend uses standard `@Sse()` returning `Observable<MessageEvent>` and respects existing JWT guard semantics. Argon2id is reused; no bcrypt.
|
||||
|
||||
---
|
||||
|
||||
## 6. Build-time Fix (Discovered During Implementation)
|
||||
|
||||
### 6.1 Problem
|
||||
|
||||
The Angular production build (`npm --workspace frontend run build`) failed with:
|
||||
|
||||
```
|
||||
TS2339: Property 'code' does not exist on type 'ChangePasswordResult'.
|
||||
Property 'code' does not exist on type 'ChangePasswordSuccess'.
|
||||
|
||||
src/app/features/home/home.component.ts:185:74
|
||||
...gePasswordError({ code: result.code, message: result.message }));
|
||||
|
||||
TS2339: Property 'message' does not exist on type 'ChangePasswordResult'.
|
||||
Property 'message' does not exist on type 'ChangePasswordSuccess'.
|
||||
|
||||
src/app/features/home/home.component.ts:185:96
|
||||
...gePasswordError({ code: result.code, message: result.message }));
|
||||
```
|
||||
|
||||
### 6.2 Root cause
|
||||
|
||||
`AuthService.changePassword()` returns `Promise<ChangePasswordResult>` where:
|
||||
|
||||
```ts
|
||||
export type ChangePasswordResult = ChangePasswordSuccess | ChangePasswordFailure;
|
||||
export interface ChangePasswordSuccess { ok: true }
|
||||
export interface ChangePasswordFailure { ok: false; status: number; code: string; message: string }
|
||||
```
|
||||
|
||||
In `submitChangePassword()` (currently line 185) we wrote:
|
||||
|
||||
```ts
|
||||
const result = await this.auth.changePassword(payload);
|
||||
this.changePasswordSubmitting.set(false);
|
||||
if (result.ok) {
|
||||
this.changePasswordOpen.set(false);
|
||||
return;
|
||||
}
|
||||
this.changePasswordError.set(formatChangePasswordError({ code: result.code, message: result.message }));
|
||||
```
|
||||
|
||||
After the `if (result.ok) { return; }` block the compiler should narrow `result` to `ChangePasswordFailure`, but the call to `this.changePasswordSubmitting.set(false)` between the `await` and the `if` is a non-trivial statement (it invokes a method on an injected object), and Angular's strict template + TypeScript settings do not propagate the narrowing through that call in this configuration. `result` therefore remains typed as the full `ChangePasswordResult` union at line 185.
|
||||
|
||||
### 6.3 Fix (surgical)
|
||||
|
||||
In `frontend/src/app/features/home/home.component.ts`, narrow explicitly by binding the failure branch to a typed local:
|
||||
|
||||
```ts
|
||||
async submitChangePassword(payload: ChangePasswordSubmitPayload): Promise<void> {
|
||||
this.changePasswordSubmitting.set(true);
|
||||
this.changePasswordError.set(null);
|
||||
const result: ChangePasswordResult = await this.auth.changePassword(payload);
|
||||
this.changePasswordSubmitting.set(false);
|
||||
if (result.ok) {
|
||||
this.changePasswordOpen.set(false);
|
||||
return;
|
||||
}
|
||||
const failure: ChangePasswordFailure = result;
|
||||
this.changePasswordError.set(
|
||||
formatChangePasswordError({ code: failure.code, message: failure.message }),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
This requires:
|
||||
1. Adding `ChangePasswordFailure` to the existing `import { ChangePasswordModalComponent, ChangePasswordSubmitPayload } from '../shell/change-password/change-password-modal.component';` line — change to:
|
||||
```ts
|
||||
import { ChangePasswordModalComponent, ChangePasswordSubmitPayload } from '../shell/change-password/change-password-modal.component';
|
||||
import { ChangePasswordResult, ChangePasswordFailure } from '../../core/services/auth.service';
|
||||
```
|
||||
2. Using the typed local `failure` for the error mapping call.
|
||||
|
||||
No other source files need changes for the build to succeed.
|
||||
|
||||
### 6.4 Why this is the only remaining build error
|
||||
* The backend files compile cleanly (`tsc` was exercised during test setup).
|
||||
* All other frontend files compile cleanly (template syntax, signal imports, standalone declarations, OnPush, route lazy-loading).
|
||||
* The existing `tests/backend/event-status.spec.ts` continues to pass because `getStatus()` still returns its legacy `{status, startUtc, endUtc, serverNowUtc, countdownMs}` shape (`status` is preserved for backward compat, mirroring the new `state` field).
|
||||
|
||||
### 6.5 Re-verify after the fix
|
||||
Run:
|
||||
```bash
|
||||
cd /repo
|
||||
npm --workspace frontend run build
|
||||
npm test
|
||||
```
|
||||
Expected: clean Angular production build + all backend and frontend tests green.
|
||||
|
||||
### 6.6 No test changes required for this build fix
|
||||
The backend `tests/backend/change-password.spec.ts` integration test already exercises every error branch (`PASSWORDS_DO_NOT_MATCH`, `INVALID_OLD_PASSWORD`, `PASSWORD_POLICY`, `401`) via the public HTTP surface, so no test changes are needed to validate the backend. The frontend discriminated-union issue is purely a TypeScript-level concern resolved by explicit narrowing.
|
||||
|
||||
---
|
||||
|
||||
## 7. Out-of-scope (for this Job)
|
||||
|
||||
* Admin-initiated password reset UI in the Players admin section (`admin-reset` mode). The modal already accepts the `mode` input and would only render `oldPassword` for `'self'`; the admin-side wiring belongs to a future Job.
|
||||
* Theming of the LED colors. The header uses existing `--color-success` / `--color-warning` / `--color-danger` tokens applied via `BootstrapService.applyTheme`.
|
||||
* Authoring full Challenges / Scoreboard / Blog pages. Placeholder pages exist so the Quick Tabs navigate to real routes; richer content belongs to their respective Jobs.
|
||||
Reference in New Issue
Block a user