feat(portal-shell): wire SPA auth state to BFF /me + header login/logout widget #113
Reference in New Issue
Block a user
Delete Branch "feat/portal-shell/auth-me-header-state"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
First user-visible piece of the auth track. The portal-shell SPA now consumes the BFF auth surface (
/api/auth/me,/api/auth/login,/api/auth/logout) and the header reflects sign-in state.libs/feature/authships anAuthServicethat fetches/auth/meon first injection, holds a signal-backedAuthState(loading/anonymous/authenticated/error) and exposescurrentUser+isLoadingcomputed signals pluslogin()/logout()/refresh()methods.JDfor "Jane Doe") + display name + sign-out button when authenticated, a loading dot before/meresolves, and a "Can't reach the server" chip on non-401 failures.login()/logout()go through an injectedAUTH_NAVIGATORtoken whose default callswindow.location.assign(url). Specs override it withvi.fn()— nowindow.locationmocking required.Notable choices
Auto-bootstrap on construction, not via
provideAppInitializer. The service fires/mefrom its constructor (unawaited) so consuming components transition through the explicitloadingstate. Blocking app boot on the round-trip would push the first paint behind the network call — bad for TTFB, especially on slow links. The header handlesloadingas a first-class state.Discriminated
AuthStateover flat fields. A single source of truth (state()) with fourkinds lets templatesswitchand narrow automatically.currentUserandisLoadingare computed conveniences but never out of sync withstate.AUTH_BFF_BASE_URL+AUTH_NAVIGATORinjection tokens. Decouples the lib from the host'senvironment.tsshape and keeps tests free ofwindow.locationredefinition (which jsdom resists across multiple specs in the same file — first redefine works, second throws "Cannot redefine property"). The host wires both inapp.config.ts.Curated public user type.
CurrentUsermirrors the BFF's/meresponse (oid,tid,username,displayName) — noamr, no internal claims. The shape lives inauth.types.tsso feature code can import it without depending on Angular HTTP details.Distinct
errorstate. A network failure / 5xx surfaces a different UI than "not signed in" — "Can't reach the server" chip vs. sign-in button. Avoids the trap of treating any/mefailure as "anonymous".Out of scope (next PRs)
/auth/loginon a 401 from any other BFF call.Test plan
pnpm nx test feature-auth→ 8/8 pass.pnpm nx test portal-shell→ 32/32 pass (was 27 before).pnpm nx lint portal-shell feature-auth→ clean.pnpm nx build portal-shell→ main bundle 488 kB raw / 129.94 kB transfer; well under the 300 kB gzip budget from ADR-0017.first user-visible piece of the auth track. portal-shell now consumes the bff auth surface and the header reflects sign-in state. libs/feature/auth replaces the empty nx scaffold with an AuthService that: - fetches /api/auth/me on first injection (unawaited so the first paint isn't blocked behind the round-trip). - holds a signal-backed discriminated AuthState — loading / anonymous / authenticated / error — so templates switch on .kind and the type narrows automatically. - exposes currentUser + isLoading computed signals on top. - provides login() / logout() / refresh() methods. navigation goes through an injected AUTH_NAVIGATOR token whose default calls window.location.assign(url) — tests substitute a vi.fn() instead of redefining window.location (which jsdom resists across multiple specs in the same file). distinct error state vs. anonymous: a network failure surfaces a "Can't reach the server" chip; a 401 surfaces the sign-in button. treating any /me failure as anonymous would silently hide outages. curated CurrentUser type mirrors the bff's /me response (oid, tid, username, displayName) — no amr, no internal claims. consumers import it without pulling in angular http types. the host wires AUTH_BFF_BASE_URL from environment.ts (per adr-0018) and AUTH_NAVIGATOR is providedIn:'root' with the window.location default, so apps don't need to touch it. tsconfig.app.json picks up the new lib reference via `nx sync`. header user-widget renders four states with i18n message ids (header.signIn / header.signOut / header.userMenu.loading / header.authError). avatar shows the user's initials computed from displayName ("JD" for "Jane Doe"); display name shows on sm: and up so the rail stays compact on narrow viewports. out of scope (next prs): - route guards - auto-refresh before idle timeout - http interceptor that redirects to /auth/login on bff 401s