fix(auth): Session-Wiederherstellung nicht an active_userid koppeln

Trennt hasUnlockedLocalCrypto (Master-Key + Username) von
hasUnlockedLocalSession (+ userId für API), damit ein gültiges
Server-Cookie ohne userId in der Antwort keinen fälschlichen Logout auslöst.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-31 11:35:17 +02:00
parent 15f2172a38
commit 72cbad8d5e
3 changed files with 49 additions and 11 deletions
+8 -5
View File
@@ -15,7 +15,11 @@ import InvitationAcceptance from './components/InvitationAcceptance.tsx'
import AppTourOverlay from './components/AppTourOverlay.tsx'
import { AppTourProvider, useAppTour, type AppTab } from './context/AppTourContext.tsx'
import { UnsavedChangesProvider, useUnsavedChangesContext } from './context/UnsavedChangesContext.tsx'
import { logoutUser, checkServerSession, hasUnlockedLocalSession } from './services/auth.js'
import {
logoutUser,
checkServerSession,
hasUnlockedLocalCrypto
} from './services/auth.js'
import AppErrorBoundary from './components/AppErrorBoundary.tsx'
import { PlausibleEvents, trackPlausibleEvent } from './services/analytics.js'
import {
@@ -221,7 +225,7 @@ 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 && !hasUnlockedLocalSession()) {
if (isAuthenticated && !hasUnlockedLocalCrypto()) {
clearAuthenticatedAppState()
}
}, [
@@ -267,7 +271,7 @@ function App() {
localStorage.setItem('active_userid', session.userId)
}
if (session.authenticated && hasUnlockedLocalSession()) {
if (session.authenticated && hasUnlockedLocalCrypto()) {
setIsAuthenticated(true)
const savedLogbookId = localStorage.getItem('active_logbook_id')
const savedLogbookTitle = localStorage.getItem('active_logbook_title')
@@ -275,9 +279,8 @@ function App() {
setActiveLogbookId(savedLogbookId)
setActiveLogbookTitle(savedLogbookTitle)
}
} else if (session.authenticated) {
clearAuthenticatedAppState()
}
// authenticated without local crypto: stay on login (cookie alone is not enough)
} catch (err) {
if (!cancelled) {
console.warn('Session restore failed:', err)
+7 -6
View File
@@ -46,13 +46,14 @@ export async function checkServerSession(): Promise<{ authenticated: boolean; us
}
}
/** Master key is memory-only; after process kill the HTTP session may outlive local crypto state. */
/** Master key + username in memory/storage — enough to stay in the unlocked UI. */
export function hasUnlockedLocalCrypto(): boolean {
return !!(getActiveMasterKey() && localStorage.getItem('active_username'))
}
/** Crypto unlock plus user id for authenticated API calls (userId may already be in localStorage). */
export function hasUnlockedLocalSession(): boolean {
return !!(
getActiveMasterKey() &&
localStorage.getItem('active_username') &&
localStorage.getItem('active_userid')
)
return hasUnlockedLocalCrypto() && !!localStorage.getItem('active_userid')
}
export async function reauthWithPasskey(): Promise<boolean> {
+34
View File
@@ -0,0 +1,34 @@
import { beforeEach, describe, expect, it } from 'vitest'
import {
hasUnlockedLocalCrypto,
hasUnlockedLocalSession,
setActiveMasterKey
} from './auth.js'
describe('local session unlock checks', () => {
beforeEach(() => {
localStorage.clear()
setActiveMasterKey(null)
})
it('hasUnlockedLocalCrypto with master key and username only', () => {
setActiveMasterKey(new ArrayBuffer(32))
localStorage.setItem('active_username', 'skipper')
expect(hasUnlockedLocalCrypto()).toBe(true)
expect(hasUnlockedLocalSession()).toBe(false)
})
it('hasUnlockedLocalSession when userId is present', () => {
setActiveMasterKey(new ArrayBuffer(32))
localStorage.setItem('active_username', 'skipper')
localStorage.setItem('active_userid', 'user-1')
expect(hasUnlockedLocalCrypto()).toBe(true)
expect(hasUnlockedLocalSession()).toBe(true)
})
it('hasUnlockedLocalCrypto false without master key', () => {
localStorage.setItem('active_username', 'skipper')
localStorage.setItem('active_userid', 'user-1')
expect(hasUnlockedLocalCrypto()).toBe(false)
})
})