fix(global-store): load site/user reactively from auth state#36190
Conversation
GlobalStore is instantiated at app bootstrap (provideAppInitializer), before the session cookie is valid (e.g. on the login page). Its one-shot onInit loads (withSite.loadCurrentSite / withUser.loadLoggedUser) therefore fired pre-auth, got a 401, and never retried. Because login navigates via the SPA router with no full page reload, nothing re-triggered them, leaving siteDetails/loggedUser null until a manual refresh (the header showed 'Select a site' and Pages crashed with 'Cannot read properties of null (reading userId)'). - withSite/withUser: load reactively via LoginService.watchUser(), which fires immediately when a session already exists (refresh, once AuthGuard resolves auth via loadAuth()) and on every login. Mirrors the legacy SiteService pattern and covers initial login, refresh and re-login uniformly. - dot-page-actions: resolve userId via getCurrentUser() instead of reading the GlobalStore loggedUser signal synchronously at construction time. - store.spec: mock LoginService and cover the auth-reactive load behavior.
|
Claude finished @nicobytes's task in 1m 4s —— View job Rollback Safety Analysis
Result: ✅ Safe To RollbackAll 5 changed files are pure frontend Angular/TypeScript code — no backend, no database, no Elasticsearch, no REST API contract changes:
Against every category in the reference document:
The label |
🤖 Bedrock Review —
|
Legal RiskThe following dependencies were released under a license that RecommendationWhile merging is not directly blocked, it's best to pause and consider what it means to use this license before continuing. If you are unsure, reach out to your security team or Semgrep admin to address this issue. GPL-2.0 MPL-2.0
|
- store.spec: reset shared mock call counts in beforeEach (clearAllMocks) instead of an in-test mockClear, removing the test-order dependency. - store.spec: add error-path coverage — getCurrentUser/getCurrentSite failing after auth fires must leave loggedUser/siteDetails null without crashing. - withSite/withUser: clarify the watchUser() comment to distinguish the synchronous branch (auth already set) from the async auth$ branch (refresh via AuthGuard loadAuth(), and every login via setAuth()).
Address AI-review feedback: watchUser fired loadCurrentSite/loadLoggedUser on every auth emission (login, login-as, logoutAs, loadAuth), causing redundant reloads even when the user did not change. Subscribe to loginService.auth$ (seeded with the current auth via startWith), map to user.userId and distinctUntilChanged, so the reactive load only runs when the authenticated user actually changes — login-as (which only sets loginAsUser) no longer triggers a redundant site/user reload. Behavior on login/refresh/re-login is unchanged. Tests updated to drive auth$ and cover dedupe (same user) vs reload (different user).
Add takeUntilDestroyed(destroyRef) to the auth-reactive subscriptions in withSite/withUser so they are torn down with the store. No leak today (root singleton + root LoginService share the app lifetime), but this makes the teardown explicit and safe if the feature is ever composed into a scoped store.
|
Re: Bedrock AI review — addressing the three findings:
|
What
Make
GlobalStoreload the current site and user reactively from the authentication state instead of a one-shot load at app bootstrap.Fixes #36189
Why
GlobalStore(providedIn: 'root') is instantiated at app bootstrap viaprovideAppInitializer(DotAppLifecycleEffectinjects it) — before the session cookie is valid (e.g. while on the login page). Its featureonInithooks ran a one-shot load (withSite.loadCurrentSite(),withUser.loadLoggedUser()) that fired pre-auth, got a401, logged aconsole.warn, and never retried.Because login navigates through the SPA router (
router.navigate(['/']), no full page reload), nothing re-triggered the loads. Result: after an initial login the header showed "Select a site" and Pages crashed withCannot read properties of null (reading 'userId'). A manual refresh recreated the store with a valid session, masking the bug.How
withSite/withUser: load reactively vialoginService.watchUser(() => store.loadX()).watchUser()fires immediately when a session already exists (refresh, onceAuthGuardresolves auth vialoadAuth()) and again on every login — covering initial login, refresh and re-login uniformly. This mirrors the reactive pattern the legacySiteServicealready used (loginService.watchUser(() => this.loadCurrentSite())), which the modern store had dropped.DotPageActionsService: resolveuserIdviagetCurrentUser()instead of reading theGlobalStore.loggedUser()signal synchronously at construction time (defensive — reading a store signal synchronously in a constructor is inherently racy).store.spec: mockLoginServiceand add coverage for the auth-reactive load behavior (no load before auth; load on auth notification; reload on re-login).Testing
nx test global-store— 177 passing (incl. new auth-reactive cases)nx test dotcms-ui— passing (incl.dot-page-actions,main-legacy)nx lint global-store dotcms-ui— passing (Nx module boundaries OK:global-store → @dotcms/dotcms-jsalready used bywithWebSocket)Manual
/starter, do not refresh.userIdnull error; menus build with correct permissions.