From 57f63ad486c0049541ca8c8a63e4db0e199e9b78 Mon Sep 17 00:00:00 2001 From: elpatron Date: Sun, 31 May 2026 11:37:50 +0200 Subject: [PATCH] =?UTF-8?q?fix(auth):=20Session-Restore=20erst=20mit=20vol?= =?UTF-8?q?lst=C3=A4ndiger=20lokaler=20Session?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stellt hasUnlockedLocalSession für UI-Wiederherstellung und enforceUnlockedSession wieder her; persistSessionUserId setzt userId nur bei Angabe in der Server-Antwort. Co-authored-by: Cursor --- client/src/App.tsx | 15 +++++++++------ client/src/services/auth.ts | 7 +++++++ client/src/services/authSession.test.ts | 19 +++++++++++++++++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 23a3949..667b077 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -18,7 +18,8 @@ import { UnsavedChangesProvider, useUnsavedChangesContext } from './context/Unsa import { logoutUser, checkServerSession, - hasUnlockedLocalCrypto + hasUnlockedLocalSession, + persistSessionUserId } from './services/auth.js' import AppErrorBoundary from './components/AppErrorBoundary.tsx' import { PlausibleEvents, trackPlausibleEvent } from './services/analytics.js' @@ -225,7 +226,8 @@ function App() { /** After PWA/bfcache resume, React state may still say "logged in" while the master key is gone. */ const enforceUnlockedSession = useCallback(() => { if (isViewerMode || isDemoMode || isAcceptingInvite) return - if (isAuthenticated && !hasUnlockedLocalCrypto()) { + // Require full local session (incl. userId) so API calls are not left headless. + if (isAuthenticated && !hasUnlockedLocalSession()) { clearAuthenticatedAppState() } }, [ @@ -267,11 +269,12 @@ function App() { const session = await checkServerSession() if (cancelled) return - if (session.authenticated && session.userId) { - localStorage.setItem('active_userid', session.userId) + if (session.authenticated) { + persistSessionUserId(session.userId) } - if (session.authenticated && hasUnlockedLocalCrypto()) { + // Cookie alone is insufficient — need in-memory master key, username, and userId for API. + if (session.authenticated && hasUnlockedLocalSession()) { setIsAuthenticated(true) const savedLogbookId = localStorage.getItem('active_logbook_id') const savedLogbookTitle = localStorage.getItem('active_logbook_title') @@ -280,7 +283,7 @@ function App() { setActiveLogbookTitle(savedLogbookTitle) } } - // authenticated without local crypto: stay on login (cookie alone is not enough) + // authenticated + crypto but no userId: stay on login (enforceUnlockedSession guards active UI) } catch (err) { if (!cancelled) { console.warn('Session restore failed:', err) diff --git a/client/src/services/auth.ts b/client/src/services/auth.ts index 5c4a4a0..9fd9647 100644 --- a/client/src/services/auth.ts +++ b/client/src/services/auth.ts @@ -56,6 +56,13 @@ export function hasUnlockedLocalSession(): boolean { return hasUnlockedLocalCrypto() && !!localStorage.getItem('active_userid') } +/** Persist server session user id when the /session response includes it. */ +export function persistSessionUserId(userId: string | undefined): void { + if (userId) { + localStorage.setItem('active_userid', userId) + } +} + export async function reauthWithPasskey(): Promise { const options = await apiJson(`${API_BASE}/reauth-options`, { method: 'POST' diff --git a/client/src/services/authSession.test.ts b/client/src/services/authSession.test.ts index 9b8d900..9f1fc07 100644 --- a/client/src/services/authSession.test.ts +++ b/client/src/services/authSession.test.ts @@ -32,3 +32,22 @@ describe('local session unlock checks', () => { expect(hasUnlockedLocalCrypto()).toBe(false) }) }) + +describe('persistSessionUserId', () => { + beforeEach(() => { + localStorage.clear() + }) + + it('stores userId when provided', async () => { + const { persistSessionUserId } = await import('./auth.js') + persistSessionUserId('user-42') + expect(localStorage.getItem('active_userid')).toBe('user-42') + }) + + it('does not clear existing userId when omitted', async () => { + const { persistSessionUserId } = await import('./auth.js') + localStorage.setItem('active_userid', 'user-1') + persistSessionUserId(undefined) + expect(localStorage.getItem('active_userid')).toBe('user-1') + }) +})