10 KiB
10 KiB
Implementation Plan: Authenticated Shell, Quick Tabs and Change Password 1.01
1. Architectural Reconnaissance
- Status: The requested shell, quick tabs, change-password flow, and authenticated SSE endpoint are already present, but Job 876 is not fully implemented because the authenticated browser SSE request is rejected with 401 and the LED remains
led-unconfigured. Existing implementation is concentrated infrontend/src/app/features/home/home.component.ts:127-135,frontend/src/app/core/services/event-status.store.ts:51-76,frontend/src/app/features/shell/header/shell-header.component.ts:25-35, andbackend/src/modules/system/system.controller.ts:77-113. - Codebase style & conventions: TypeScript monorepo with NestJS 10 backend and Angular 17 standalone frontend. Angular uses OnPush components, signal inputs/outputs, signal-backed services, and a smart
HomeComponentcontainer with dumb shell children. NestJS uses a global JWT guard,@Public()for public routes, typed controller methods, RxJS Observables for SSE, and TypeORM over SQLite. - Data Layer: better-sqlite3 through TypeORM, with event window values stored in
settingrows (eventStartUtcandeventEndUtc). No schema change is indicated.EventStatusService.getState()is the authoritative four-state event state machine. - Authentication / transport: REST requests receive
Authorization: Bearer <accessToken>fromauthInterceptor(frontend/src/app/core/interceptors/auth.interceptor.ts:5-11). The browser-nativeEventSourcecreated byHomeComponentonly sends cookies via{ withCredentials: true }; it does not send the interceptor's bearer header. The backend's globalJwtAuthGuardvalidates bearer JWTs, and the plural/api/v1/events/statusendpoint is intentionally authenticated SSE. - Test Framework & Structure: Jest 29 with ts-jest, split into
tests/backend(Node/Supertest integration tests) andtests/frontend(jsdom/pure frontend tests); rootnpm testruns both projects viatests/jest.config.js. Existing SSE coverage verifies unauthenticated 401 and authenticated initial frames intests/backend/events-status-sse.spec.ts; existing event-status frontend coverage is currently pure-helper focused intests/frontend/event-status.store.spec.ts. - Required Tools & Dependencies: No new dependency should be required. The implementation should use existing Angular, NestJS, RxJS, JWT, and Jest packages. No database migration or
/datafixture is needed.setup.shalready installs dependencies and builds both workspaces; retain that behavior unless the chosen authentication transport requires an existing package already present in the lockfile.
2. Impacted Files
- To Modify:
frontend/src/app/features/home/home.component.ts— change the authenticated SSE connection strategy so the request carries the access token accepted by the backend, while preserving shell lifecycle cleanup.frontend/src/app/core/services/event-status.store.tsand/orfrontend/src/app/core/services/event-status.pure.ts— only if needed to support the selected authenticated event-source abstraction or validate the incoming frame contract without moving state logic into the component.frontend/src/app/core/interceptors/auth.interceptor.ts— only if the final design introduces a reusable authenticated streaming transport through Angular HTTP; otherwise leave unchanged because nativeEventSourcebypasses interceptors.backend/src/modules/system/system.controller.ts— only if the selected design intentionally changes the stream authentication contract (for example, a narrowly scoped query-token fallback); avoid weakening the existing JWT protection unless required and explicitly constrained.tests/frontend/event-status.store.spec.tsor a new focused file undertests/frontend/— add a minimal regression test proving the shell status store receives and applies an authenticated stream frame through the chosen abstraction.tests/backend/events-status-sse.spec.ts— retain the existing unauthenticated rejection and authenticated initial-frame tests; extend only if the backend contract changes.docs/api/system.mdanddocs/guides/event-window.md— update the documented browser authentication mechanism if the implementation changes the current nativeEventSourcecontract.
- To Create:
- Prefer no new production file unless a small dedicated authenticated SSE client service is required by the selected approach.
- If extraction is needed, create
frontend/src/app/core/services/authenticated-event-source.service.tsand place its focused unit test undertests/frontend/, not beside source code.
3. Proposed Changes
Explain the exact implementation logic step-by-step:
-
Database / Schema Migration:
- No migration. Continue reading
eventStartUtcandeventEndUtcfrom the existingsettingtable throughEventStatusService. - Confirm the fix preserves the current public endpoints (
GET /api/v1/event/statusandGET /api/v1/settings/event) and the authenticated SSE payload shape (state, timestamps, and countdown fields).
- No migration. Continue reading
-
Backend Logic & APIs:
- Treat the observed 401 as an authentication-transport mismatch, not an event-state calculation problem:
HomeComponentuses nativeEventSource, while the backend JWT strategy extracts only an Authorization bearer token. - Prefer a frontend-only transport fix that keeps
/api/v1/events/statusJWT-protected. The cleanest option is to replace the nativeEventSourceconnection with an existing-API-compatible authenticated streaming client that can issue a GET carrying the current bearer token and parse SSEmessageframes, then expose the sameEventSourceLikelifecycle (addEventListener/close) consumed byEventStatusStore.start(). - Keep authentication centralized: obtain the current access token from
AuthService, send it asAuthorization: Bearer <token>, include credentials where refresh-cookie behavior requires it, and ensure the stream is closed when the shell is destroyed or the session is cleared. - Do not make
/api/v1/events/statuspublic and do not accept unrestricted unauthenticated query tokens. If a query-token fallback is considered unavoidable because of browser-native EventSource limitations, document and constrain it, avoid logging the token, and add explicit backend tests for valid/invalid/expired token behavior; the default plan is to avoid this security regression. - Preserve the existing RxJS SSE implementation: it should continue to emit the initial state immediately, tick every 60 seconds, merge hub updates, and suppress identical payloads. No changes to event-state computation are needed.
- Treat the observed 401 as an authentication-transport mismatch, not an event-state calculation problem:
-
Frontend UI Integration:
- In
HomeComponent.ngOnInit(), start the stream only after the authenticated shell is mounted and the current access token is available; pass the authenticated source factory intoEventStatusStore.start(). - Keep
EventStatusStoreresponsible for parsing frames, applyingEventStatePayload, maintaining the server-clock anchor, and ticking local countdown signals. Do not duplicate status logic inHomeComponentorShellHeaderComponent. - Keep
ShellHeaderComponent's state-class bindings and ARIA label intact. Once the first authenticated frame arrives,eventStatus.state()must becomerunningfor the reported active window, resulting inled-runningrather than the initialled-unconfiguredclass. - Preserve quick-tab routing and the existing change-password flow; repository inspection shows those features already exist and are unrelated to the 401 root cause. Avoid reworking them.
- Ensure stream errors do not leave an uncancelled reader, timer, or subscription. Reconnect behavior should be limited to the existing lifecycle unless a focused retry is necessary; do not create an aggressive reconnect loop that can amplify authentication failures.
- Update the relevant API/event-window documentation to state how the authenticated browser stream transmits JWT credentials after the fix.
- In
4. Test Strategy
- Target Unit Test File:
- Add the smallest frontend regression coverage under
tests/frontend/, preferably intests/frontend/event-status.store.spec.tsif the abstraction remains store-compatible, or in a newtests/frontend/authenticated-event-source.spec.tsif a dedicated client is extracted. - Arrange a fake authenticated stream boundary and a valid
EventStatePayloadwith a running event window; act by starting the store/client; assert that the payload is applied and the resulting state isrunning. Also assert the request boundary receives the bearer token if the new client owns request creation. - Keep the existing backend tests in
tests/backend/events-status-sse.spec.ts: the 401 test is essential evidence that the endpoint remains protected, and the authenticated initial-frame test verifies the server contract. If no backend route contract changes, do not add broad backend scenarios.
- Add the smallest frontend regression coverage under
- Mocking Strategy:
- Mock only external boundaries: the browser stream/request implementation,
AuthServicetoken lookup where needed, timers/Date only if required for deterministic countdown behavior, and network responses. Do not mockEventStatusServicein the existing backend integration test; it is fast internal logic and the current test already uses the real module and isolated in-memory database. - Use the repository's Jest configuration and dedicated
tests/folders. Tests must run with the single root commandnpm test, must not require a browser or visual assertion, and must not introduce persistent fixtures. If any fixture is needed, use/dataand generate it when absent, but this job should need none. - Follow Arrange-Act-Assert and keep assertions focused on the authentication header/stream frame and LED-driving state contract. Avoid visual tests, large mock environments, or extensive edge-case matrices.
- Mock only external boundaries: the browser stream/request implementation,
- Verification: After implementation, run the repository's available root test command plus the workspace build/type validation commands used by the project. There is no declared lint or typecheck npm script in
package.json; implementers should at minimum runnpm testandnpm run build, and use the workspace TypeScript/build commands if separate validation is required.