9.7 KiB
9.7 KiB
Implementation Plan: Job 881 — Cross-Tab Authenticated Shell Invalidation
1. Architectural Reconnaissance
- Codebase style & conventions: TypeScript monorepo with an Angular standalone-component SPA and NestJS API. Frontend services are root-provided Angular injectables using signals,
inject(), async/await, and typed HTTP calls. Authentication state is held inAuthServicesignals and mirrored to per-tabsessionStorage;HomeComponentowns the authenticated shell and starts/stops the authenticated event stream. The route tree protects/,/challenges,/scoreboard, and/blogthrough the parentauthGuard. - Data Layer: No database/schema change is required. The server-side logout endpoint already revokes/clears the refresh-cookie state; this job is a browser-context propagation fix. Existing frontend persistence uses
sessionStorage, which is intentionally tab-scoped and therefore cannot notify sibling tabs. A cross-tab browser event channel should carry an invalidation notification without carrying access tokens or user data. - Test Framework & Structure: Root Jest 29 with two projects in
tests/jest.config.js; frontend tests run in jsdom throughnpm testornpm run test:frontend. Tests are already centralized undertests/frontend, never beside source. Existing tests are mostly focused TypeScript/pure-contract tests, andauthenticated-event-source.spec.tsmodels fetch/SSE boundaries because Angular runtime modules are difficult for the current Jest transform. Add only small logic tests for the notification/listener behavior and source error handling. - Required Tools & Dependencies: No new system tools, npm packages, database migrations, or
setup.shchanges are expected.BroadcastChanneland/or thestorageevent are browser platform APIs. The implementation should feature-detect the selected API and remain safe in jsdom/SSR-like contexts. Existing Node/npm/Jest/TypeScript setup is sufficient.
2. Impacted Files
- To Modify:
frontend/src/app/core/services/auth.service.ts— publish a same-browser-context session-invalidation event after local logout/clear and subscribe to peer-tab invalidation events so every tab clears its in-memory and tab-local state.frontend/src/app/core/services/authenticated-event-source.service.ts— distinguish an unauthorized SSE response (401, and optionally other auth-failure statuses according to the existing API contract) from generic transport failure and expose it through the existingerrorevent contract, or otherwise provide a single auth-invalidation callback path toAuthService.frontend/src/app/core/services/event-status.store.ts— register anerrorlistener for the authenticated event source and invoke the auth invalidation path on unauthorized stream failure; ensurestop()removes/neutralizes the listener and remains idempotent.frontend/src/app/features/home/home.component.ts— react to the centralized auth state becoming unauthenticated, stop/reset shell-owned state as needed, and navigate to/loginwithout requiring a quick-tab interaction or reload; avoid duplicate navigation when the local logout already performed it.
- To Create:
tests/frontend/auth-cross-tab.spec.ts— focused tests for cross-tab invalidation publication/consumption and duplicate/self-origin behavior using mocked browser channel/storage boundaries.- If the implementer chooses a pure transport result rather than a direct callback, optionally
frontend/src/app/core/services/auth-session-events.pure.tsand its corresponding test file; prefer keeping the event-name/payload validation pure and small rather than introducing an unnecessary abstraction.
- Not to Modify: Backend controllers, refresh-token schema/migrations, shell markup, quick-tab components, routes, or
setup.sh; the server invalidation and route protection already exist.
3. Proposed Changes
-
Database / Schema Migration:
- No migration. Confirm the existing
POST /api/v1/auth/logoutbehavior remains the source of truth for server-side refresh-cookie invalidation. - Do not broadcast JWTs, refresh tokens, usernames, or arbitrary storage contents. Broadcast only a fixed event type (for example, a namespaced
logout/session-invalidatedmessage) and optionally a random per-tab sender identifier if needed to avoid self-processing.
- No migration. Confirm the existing
-
Backend Logic & APIs:
- No backend endpoint changes. The existing authenticated logout endpoint is called by
AuthService.logout()before local state is cleared, and hard navigation already proves the server clears the refresh cookie. - Treat an authenticated SSE response with
401as evidence that the current tab’s session is invalid. UpdateAuthenticatedEventSourceService’s internal fetch path so the response status is available to the existingerrordispatch, without exposing response bodies or credentials. Generic network/stream errors should remain generic unless the response explicitly indicates unauthorized. - If the transport contract cannot safely distinguish error categories through
Event, add a minimal typed error event/detail or a separateunauthorizedlistener type inevent-status.pure.ts; update all implementations and tests consistently. Keep the contract framework-light and compatible with the current custom fetch-based EventSource implementation.
- No backend endpoint changes. The existing authenticated logout endpoint is called by
-
Frontend UI Integration:
- Centralize cross-tab handling in
AuthService, because it owns the authoritativeisAuthenticated, access-token, and session-storage state. Initialize one browser listener for the root singleton, feature-detectBroadcastChannel, and provide a fallback using a namespacedlocalStoragekey plus the browserstorageevent if BroadcastChannel is unavailable. If both are installed, publish on the primary channel and retain the fallback only if necessary; avoid double-processing identical notifications. - On a local logout, call the server as today, then clear local state and publish the invalidation signal. Ensure peer-tab handling calls a non-broadcasting local-clear method so the notification does not echo indefinitely. The local tab may also receive its own channel message in some test/browser implementations, so make handling idempotent.
- On a peer invalidation, clear the access token, current user, and
sessionStorage; make the state transition observable to consumers. IfAuthService.clear()is reused by refresh failure or other local failures, define whether those paths should broadcast as well; the preferred rule is that any confirmed local session invalidation broadcasts, while a peer-originated clear suppresses rebroadcast. - Wire the authenticated SSE lifecycle to the same invalidation path.
EventStatusStore.start()should attach both message and error listeners. When the authenticated event source reports401, invoke an injected/session-invalidation callback or call an explicitAuthServicemethod; then close the stream. Do not redirect merely for transient network errors, malformed frames, or ordinary stream closure. - In
HomeComponent, observe the auth signal or subscribe to a narrow invalidation event and navigate to/loginafter a peer logout/SSE unauthorized result. ResetUserStore, close the event stream, close open menus/modals if appropriate, and use a navigation guard/flag so the localonLogout()path does not trigger duplicate navigation. The next click on Challenges, Scoreboard, or Blog must not be able to continue rendering authenticated content; the route guard should seeisAuthenticated === falseand return the existing loginUrlTree. - Ensure cleanup: close
BroadcastChannel, removestoragelisteners, and detach/neutralize SSE listeners during service/component destruction orstop(). Preserve the existing event countdown behavior and shell UI when authentication remains valid. - Keep browser API access guarded for non-browser/test environments. Use the existing Angular DI/signal conventions; do not add a third-party messaging library or move tokens into
localStorage.
- Centralize cross-tab handling in
4. Test Strategy
- Target Unit Test File:
tests/frontend/auth-cross-tab.spec.tsfor the core event protocol and AuthService behavior. Extendtests/frontend/authenticated-event-source.spec.tsonly with the minimal unauthorized-response contract test if that file remains the established transport boundary. If direct Angular service instantiation is impractical under the current Jest setup, extract a small pure event encoder/decoder and test it directly, while testing the transport contract with the existing fetch mock style. - Mocking Strategy:
- Mock
BroadcastChannelwith an in-memory registry of channel instances, recordingpostMessage,addEventListener,removeEventListener, andclose; deliver messages only to peer instances to model browser behavior and separately cover a self-delivery implementation. - Mock
window.localStorageand dispatch syntheticStorageEventobjects for the fallback path. Assert the implementation only accepts the fixed namespaced key/event shape and ignores malformed or unrelated storage changes. - Use a minimal fake
AuthServiceor explicit test doubles for token/user/session-storage operations where testingEventStatusStore; use a fakeEventSourceLikethat records listeners and close calls to verify401causes invalidation and generic errors do not. - Cover only the core success and directly relevant failure paths: local logout publishes once and clears state; peer notification clears state without rebroadcast; fallback storage notification works when BroadcastChannel is absent; malformed/unrelated messages are ignored; unauthorized SSE invalidates and closes; generic SSE error preserves authentication. Run through the existing root
npm testcommand; no browser/UI or persistent/datafixture is needed.
- Mock