fix(admin): scope remembered list state to the signed-in admin and recover from bad restored queries

- Store list queries under summer-admin.list:<admin id>:<controller>; me()
  and setCurrentUser set the owner, which removes other admins' keys and
  the unscoped keys of older builds; nothing is read or written without one
- clearUser removes every stored query, so logout, session expiry and an
  unrecoverable 401 (expireSession, now used by main.ts) all clear them
- A list request built from a restored query that fails for any reason
  (422, 500, network) forgets the query and loads the plain list once
- A failing URL query keeps the error and offers a reset button
  (backend::lang.list.reset_list, en and pl)
- Document remembered list state in docs/backend/lists-and-filters.md
- Rebuild modules/boardwalk/dist
This commit is contained in:
2026-10-09 23:53:19 +02:00
parent 5be98a0c83
commit cd25af0878
15 changed files with 594 additions and 75 deletions

View File

@@ -1,10 +1,10 @@
// Admin session state. The JWT travels in an HttpOnly cookie (D-19): the SPA
// keeps only the profile and the access lifetime, never a token.
import { readonly, ref } from 'vue'
import type { Router } from 'vue-router'
import type { RouteLocationNormalizedLoaded, Router } from 'vue-router'
import { api, onRefreshed, refreshSession } from '../api/client'
import type { AdminProfile } from '../api/types'
import { clearListStates } from './listState'
import { setListStateOwner } from './listState'
import { setNavigation } from './useNavigation'
import { setSettings } from './useSettings'
@@ -48,10 +48,15 @@ export async function login(identifier: string, password: string): Promise<boole
return true
}
/** Loads the signed-in admin; null when the session is missing or expired. */
/**
* Loads the signed-in admin; null when the session is missing or expired.
* The stored list queries follow the answer: a profile keeps only that
* admin's (another admin's are removed, T-16.1-03), no profile removes all.
*/
export async function me(): Promise<AdminProfile | null> {
const { data, response } = await api.GET('/auth/me')
user.value = response.ok && data ? data.data : null
setListStateOwner(user.value?.id ?? null)
return user.value
}
@@ -73,12 +78,35 @@ export function profileInitials(user: Pick<AdminProfile, 'first_name' | 'last_na
/** Replaces the signed-in profile, e.g. with the one PUT /account returned. */
export function setCurrentUser(profile: AdminProfile): void {
user.value = profile
setListStateOwner(profile.id)
}
/**
* Forgets the session on this device: the refresh timer, the profile and
* every stored list query (search terms can hold personal data and never
* outlive the session, T-16.1-03).
*/
export function clearUser(): void {
cancelRefresh()
user.value = null
expiresIn.value = null
setListStateOwner(null)
}
/**
* The session ended on its own (expiry, or a 401 the refresh could not
* recover): clears the local session, stored list queries included, and
* sends the admin to the login screen with the current path as redirect.
*/
export function expireSession(router: {
currentRoute: { value: Pick<RouteLocationNormalizedLoaded, 'name' | 'fullPath'> }
push: Router['push']
}): void {
clearUser()
const current = router.currentRoute.value
if (current.name !== 'login') {
void router.push({ name: 'login', query: { redirect: current.fullPath } }).catch(() => undefined)
}
}
/**
@@ -95,7 +123,6 @@ export async function logout(router?: Pick<Router, 'replace'>): Promise<void> {
// Network failure: the session still ends on this device.
} finally {
clearUser()
clearListStates()
setNavigation([])
setSettings([])
}
@@ -110,6 +137,7 @@ export function useAuth() {
expiresIn: readonly(expiresIn),
login,
logout,
expireSession,
me,
setCurrentUser,
clearUser,