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:
+8
-5
@@ -15,7 +15,11 @@ import InvitationAcceptance from './components/InvitationAcceptance.tsx'
|
|||||||
import AppTourOverlay from './components/AppTourOverlay.tsx'
|
import AppTourOverlay from './components/AppTourOverlay.tsx'
|
||||||
import { AppTourProvider, useAppTour, type AppTab } from './context/AppTourContext.tsx'
|
import { AppTourProvider, useAppTour, type AppTab } from './context/AppTourContext.tsx'
|
||||||
import { UnsavedChangesProvider, useUnsavedChangesContext } from './context/UnsavedChangesContext.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 AppErrorBoundary from './components/AppErrorBoundary.tsx'
|
||||||
import { PlausibleEvents, trackPlausibleEvent } from './services/analytics.js'
|
import { PlausibleEvents, trackPlausibleEvent } from './services/analytics.js'
|
||||||
import {
|
import {
|
||||||
@@ -221,7 +225,7 @@ function App() {
|
|||||||
/** After PWA/bfcache resume, React state may still say "logged in" while the master key is gone. */
|
/** After PWA/bfcache resume, React state may still say "logged in" while the master key is gone. */
|
||||||
const enforceUnlockedSession = useCallback(() => {
|
const enforceUnlockedSession = useCallback(() => {
|
||||||
if (isViewerMode || isDemoMode || isAcceptingInvite) return
|
if (isViewerMode || isDemoMode || isAcceptingInvite) return
|
||||||
if (isAuthenticated && !hasUnlockedLocalSession()) {
|
if (isAuthenticated && !hasUnlockedLocalCrypto()) {
|
||||||
clearAuthenticatedAppState()
|
clearAuthenticatedAppState()
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
@@ -267,7 +271,7 @@ function App() {
|
|||||||
localStorage.setItem('active_userid', session.userId)
|
localStorage.setItem('active_userid', session.userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.authenticated && hasUnlockedLocalSession()) {
|
if (session.authenticated && hasUnlockedLocalCrypto()) {
|
||||||
setIsAuthenticated(true)
|
setIsAuthenticated(true)
|
||||||
const savedLogbookId = localStorage.getItem('active_logbook_id')
|
const savedLogbookId = localStorage.getItem('active_logbook_id')
|
||||||
const savedLogbookTitle = localStorage.getItem('active_logbook_title')
|
const savedLogbookTitle = localStorage.getItem('active_logbook_title')
|
||||||
@@ -275,9 +279,8 @@ function App() {
|
|||||||
setActiveLogbookId(savedLogbookId)
|
setActiveLogbookId(savedLogbookId)
|
||||||
setActiveLogbookTitle(savedLogbookTitle)
|
setActiveLogbookTitle(savedLogbookTitle)
|
||||||
}
|
}
|
||||||
} else if (session.authenticated) {
|
|
||||||
clearAuthenticatedAppState()
|
|
||||||
}
|
}
|
||||||
|
// authenticated without local crypto: stay on login (cookie alone is not enough)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
console.warn('Session restore failed:', err)
|
console.warn('Session restore failed:', err)
|
||||||
|
|||||||
@@ -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 {
|
export function hasUnlockedLocalSession(): boolean {
|
||||||
return !!(
|
return hasUnlockedLocalCrypto() && !!localStorage.getItem('active_userid')
|
||||||
getActiveMasterKey() &&
|
|
||||||
localStorage.getItem('active_username') &&
|
|
||||||
localStorage.getItem('active_userid')
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function reauthWithPasskey(): Promise<boolean> {
|
export async function reauthWithPasskey(): Promise<boolean> {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user