feat(auth): post-login landing route resolution per context

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>
This commit is contained in:
2026-05-05 21:40:32 +02:00
parent 209e0ef682
commit 38a94c78e9
7 changed files with 127 additions and 67 deletions

View File

@@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest'
import { mount } from '@vue/test-utils'
const mockSetLastContext = vi.fn()
const mockResolveLandingRoute = vi.fn(() => ({ path: '/portal/evenementen' }))
const mockResolveLandingRoute = vi.fn(() => '/portal/evenementen')
const mockPush = vi.fn()
const authStoreState: Record<string, unknown> = {
@@ -67,6 +67,6 @@ describe('ContextSwitcher', () => {
expect(mockSetLastContext).toHaveBeenCalledWith('portal')
expect(mockResolveLandingRoute).toHaveBeenCalledWith('portal')
expect(mockPush).toHaveBeenCalledWith({ path: '/portal/evenementen' })
expect(mockPush).toHaveBeenCalledWith('/portal/evenementen')
})
})