WS-3 PR-B2a: auth + routing consolidation (single SPA, dual axios, context-aware guards) #5
Reference in New Issue
Block a user
Delete Branch "feat/ws-3-pr-b2a-auth-routing-consolidation"
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?
Samenvatting
Consolideert auth + routing voor de single-SPA reality post-PR-B1: één
useAuthStorevoor beide contexts (organizer + portal), één axios factory met aparte default- en
portal-token-instances, context-aware route-guards met
meta-driven role/contextresolution, en een context-switcher voor multi-role users. Onderdeel van de
architectuur-consolidatie sprint (zie
dev-docs/ARCH-CONSOLIDATION-2026-04.md).Architectural decisions (locked tijdens prompt-engineering)
/auth/meadditief uitgebreid:contexts.{available, default}block +promoted
platform.is_super_admin. Geen breaking changes;MeTest.phpblijftgroen onaangeraakt.
availableContextsis backend-driven (single source of truthin
MeResource).registerInterceptors(client, deps)callback-seamuit
53f6a7b(TECH-AXIOS-STORE-COUPLING). Factory exporteert AxiosInstance(s);plugins/3.axios-bindings.tsdoet de deps-wiring. Portal-token Bearer viabindings deps callback
getPortalToken: () => string | null— geen lazy-importvan stores binnen de factory.
usePortalAuthStoregemerged inuseAuthStoremetcontext-aware getters. Dubbele
/auth/mefetch geëlimineerd.volunteer(),orgAdmin(),volunteerAndOrganizer(),superAdmin()— herbruikbaar voor S3b en accreditationtest scenarios.
organisations[].roleemit als 1-element array (roles: [pivotRole]).Pivot-schema onveranderd; volledige multi-role discussie verplaatst naar backlog
als
TECH-PIVOT-ROLES-MULTI(ARCH-discussie over Spatie-permission integratie).useAuthStore.login(credentials)mettyped
LoginResultdiscriminated union (authenticated | mfa-required | must-set-password | failed). Page-component consumeert results en zet UI-state.verifyMfa()als zusteractie.'portal' | 'organizer'blijft string-union in B2a;enterprise-grade fix verplaatst naar dedicated workstream
ARCH-API-RESPONSE-VALIDATION(ziedev-docs/ARCH-API-VALIDATION.mdskeleton +BACKLOG entry, geland in commits
babbbd9/145d0cbop main).Commits (9, chronologisch)
a2760ff13d7b18f2b08ec473b22a209e0ef38a94c73019095eb7f3ebb191fbeTest impact
AuthMeShapeTest.phpvoor decontexts+platformenrichment;
MeTest.phponaangeraakt en groen).portal-store sync-watcher en 4 voor
MfaChallengeCard).Buiten scope (uitdrukkelijk deferred naar PR-B2b)
postLoginRedirect.tsis extension point; huidige logica is veilig maar niet uitputtend gevalideerd.
is mogelijk maar buiten scope om backend untouched te houden in deze PR.
van B2b.
Bonus fix (opgemerkt tijdens cleanup)
hydrateAfterAuth/loadUserEventsFromApiAndStoragewaren dead code — geencallers in de codebase. Portal pages leunden op sessionStorage entries die
alleen door
savePendingEventFromRegistrationtijdens public registrationwerden gevuld. De reactive watch in
eb7f3ebzorgt datuserEventsnudaadwerkelijk vanuit
/auth/mepopuleert. Zie commit body voor details.Self-review checklist
apps/portal/is volledig verwijderd (gebeurd in PR-B1, geen restanten)stores-portal → storesedge bestaat al, geenconfig-wijziging nodig (bevestigd in cleanup-rapport)
ARCH-API-VALIDATION borging, geen overlap met deze branch)
MeTest.phponaangeraakt;AuthMeShapeTest.phpis nieuwapiClient.get('/auth/me')meer inusePortalStoreMeResource::toArray→useAuthStore.setUser→usePortalStorewatchRefs
dev-docs/ARCH-CONSOLIDATION-2026-04.md— sprint scopedev-docs/ARCH-CONSOLIDATION-ADDENDUM-2026-04-24.md— A13 security itemsdev-docs/AUTH_ARCHITECTURE.md—/auth/meshape contractdev-docs/BACKLOG.md—ARCH-API-RESPONSE-VALIDATION,TECH-PIVOT-ROLES-MULTI, B2b itemsThe single axios.ts file becomes a directory with: - factory.ts — createApiClient + the registerDefaultInterceptors / registerPortalTokenInterceptors seam (preserves the TECH-AXIOS-STORE-COUPLING decoupling — no store imports inside) - default.ts — cookie-authenticated client (organizer + cookie-auth portal flows; existing 45 call sites resolve unchanged) - portal-token.ts — Bearer-auth client for the artist-advance / supplier-intake flows (forward-compatible groundwork; no active consumers today) - index.ts — re-exports apiClient + portalApiClient + the register* / createApiClient surface; the existing `import { apiClient } from '@/lib/axios'` continues to work directory-resolved. The bindings plugin (plugins/3.axios-bindings.ts) now wires both clients with a shared deps base + flavour-specific overrides. The `getPortalToken` callback returns null until Phase E surfaces `portalToken` on useAuthStore — no current consumers exercise the Bearer path, so the null-return is intentional placeholder. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Rewrites plugins/1.router/guards.ts per ARCH-CONSOLIDATION §4.3. The B1 portal-context carve-out is removed; portal/organizer routing is now declarative via meta.context, role gates via meta.requiresRole. Guard pipeline: 1. Initialize auth store on first navigation 2. Public routes pass through (authenticated user on guest-only path is bounced to resolveLandingRoute) 3. Auth required → /login?to=<path> 4. MFA setup gate → /account-settings?tab=security 5. requiresRole declarative check (replaces hardcoded /platform path prefix + isSuperAdmin) 6. Context routing — portal returns early, organizer falls through and sets lastContext 7. Org-selection check (organizer routes only) Page meta updates (mechanical, idempotent): - 4 portal pages: removed `requiresAuth: true` (auth is implicit) - 4 pages: replaced `requiresAuth: false` with `meta.public: true` (registreren, wachtwoord-instellen, advance/[token], invitations/[token]) - 22 organizer pages: added `context: 'organizer'` (account-settings, events/**, organisation/form-failures/**, select-organisation, dashboard, events/index, members, organisation/{index,companies,settings}) - 8 platform pages: added `context: 'organizer'` + `requiresRole: 'super_admin'` - 6 organizer pages had no definePage block — one was added with `context: 'organizer'` Adds plugins/1.router/__tests__/guards.spec.ts (11 tests) covering public passthrough, unauthenticated redirect, portal/organizer context branching, declarative requiresRole, org-selection redirect, MFA gate. Test count 178 → 189 (11 new). Lint + typecheck clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>login.vue is rewritten to consume useAuthStore.login()'s discriminated union — no more direct apiClient calls or branching on raw API response shapes. The page maps result.kind to UI/routing decisions only: - mfa-required → swap to MfaChallengeCard with the typed payload - authenticated → resolvePostLoginTarget() (?to= relative, else auth.resolveLandingRoute()) - must-set-password → forward-compatible placeholder route - failed → field-level errors + rate_limit message branch resolveLandingRoute() now returns a string path instead of RouteLocationRaw — the typed router accepts string-paths cleanly, removes the cast at every call site, and lets useAuthStore.spec.ts + guards.spec.ts assert the resolved path directly. A13-3 minimum precaution lives in a new utility: src/utils/postLoginRedirect.ts. The relative-only check (`startsWith('/') && !startsWith('//')`) rejects absolute, protocol- relative, javascript:, and data: schemes. Full domain validation lands in WS-3 PR-B2b. 6 vitest specs in utils/__tests__/postLoginRedirect.spec.ts cover the six rejection / passthrough scenarios. Test count 192 → 198. Lint + typecheck clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>The auth-store merge made portal_events available on the unified /auth/me response (held in useAuthStore.portalEvents). usePortalStore now sources userEvents from the auth store, eliminating the duplicate fetch that the legacy slim usePortalAuthStore had compensated for. Changes: - types/auth.ts: add portal_events?: PortalEvent[] to MeResponse - useAuthStore: add portalEvents ref, populated in setUser from me.portal_events, cleared in clearState - usePortalStore: replace loadUserEventsFromApiAndStorage (which fetched /auth/me) with syncEventsFromAuthStore (which reads authStore.portalEvents). A reactive watch keeps userEvents in sync whenever the auth store updates (login, refresh, logout). The sessionStorage merge stays as offline cache + post-registration bridge. - types/portal.ts: drop the now-unused AuthMeUser type — MeResponse is the canonical shape post-merge. Boundaries: usePortalStore (stores-portal) statically imports useAuthStore (stores) — already allowed by the matrix (stores-portal allow includes stores). Adds 4 vitest specs covering: userEvents reflects auth.portalEvents, no apiClient.get('/auth/me') call from the portal store, sessionStorage fallback when auth has not hydrated, reactive update on auth.portalEvents change. Test count 205 → 209. Lint + typecheck clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>