fix: portal shows stale events from localStorage after user_id unlinked

The portal store merged events from the API with localStorage events
without ever pruning stale entries. When /auth/me returned empty
portal_events (e.g. after a person's user_id was cleared), localStorage
events persisted, causing "registratie niet ophalen" when /portal/me
correctly returned 404.

Now when /auth/me succeeds, API data is the source of truth — stored
events not confirmed by the API are dropped. localStorage fallback is
only used when the API call fails (network error).

Also adds an end-to-end test covering the full register → approve →
portal/me flow including festival hierarchy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 02:54:36 +02:00
parent 67ce1e9d9d
commit fcab30e5e8
2 changed files with 335 additions and 12 deletions

View File

@@ -46,16 +46,42 @@ function writeStoredActiveEventId(id: string | null): void {
else localStorage.removeItem(STORAGE_ACTIVE_EVENT)
}
function mergeEvents(apiEvents: PortalEvent[], stored: PortalEvent[]): PortalEvent[] {
/**
* Merge API events with locally stored events.
*
* When the API call succeeded (`apiSucceeded = true`), the API is the source
* of truth: stored events that are NOT confirmed by the API are dropped.
* This prevents stale localStorage entries from showing events the user no
* longer has access to (e.g. after user_id was cleared).
*
* When the API call failed (`apiSucceeded = false`), we fall back to stored
* events as a best-effort cache.
*/
function mergeEvents(apiEvents: PortalEvent[], stored: PortalEvent[], apiSucceeded: boolean): PortalEvent[] {
const map = new Map<string, PortalEvent>()
for (const e of stored) map.set(e.event_id, { ...e })
for (const e of apiEvents) {
const prev = map.get(e.event_id)
map.set(e.event_id, {
...prev,
...e,
organisation_name: e.organisation_name || prev?.organisation_name || '',
})
if (apiSucceeded) {
// API is source of truth — start with API events only
for (const e of apiEvents) {
const prev = stored.find(s => s.event_id === e.event_id)
map.set(e.event_id, {
...prev,
...e,
organisation_name: e.organisation_name || prev?.organisation_name || '',
})
}
}
else {
// API failed — merge stored + whatever API returned (likely empty)
for (const e of stored) map.set(e.event_id, { ...e })
for (const e of apiEvents) {
const prev = map.get(e.event_id)
map.set(e.event_id, {
...prev,
...e,
organisation_name: e.organisation_name || prev?.organisation_name || '',
})
}
}
return Array.from(map.values()).sort((a, b) => b.start_date.localeCompare(a.start_date))
@@ -84,7 +110,7 @@ export const usePortalStore = defineStore('portal', () => {
* TODO: replace with `portal_events` from GET /auth/me when the API exposes it.
*/
function savePendingEventFromRegistration(event: PortalEvent): void {
const merged = mergeEvents([], [...readStoredEvents(), ...userEvents.value, event])
const merged = mergeEvents([], [...readStoredEvents(), ...userEvents.value, event], false)
userEvents.value = merged
persistEvents()
if (!activeEventId.value || !merged.some(e => e.event_id === activeEventId.value)) {
@@ -99,14 +125,16 @@ export const usePortalStore = defineStore('portal', () => {
try {
const stored = readStoredEvents()
let apiEvents: PortalEvent[] = []
let apiSucceeded = false
try {
const { data } = await apiClient.get<{ success: boolean; data: AuthMeUser }>('/auth/me')
apiEvents = data.data.portal_events ?? []
apiSucceeded = true
}
catch {
// /auth/me failed — still show locally stored registrations
}
userEvents.value = mergeEvents(apiEvents, stored)
userEvents.value = mergeEvents(apiEvents, stored, apiSucceeded)
persistEvents()
}
catch (e) {
@@ -154,7 +182,10 @@ export const usePortalStore = defineStore('portal', () => {
)
persistEvents()
}
catch {
catch (err) {
if (import.meta.env.DEV) {
console.warn('[portal] fetchCurrentPerson failed for event_id:', eid, err)
}
currentPerson.value = null
}
finally {