fix(auth): Session-Restore erst mit vollständiger lokaler Session
Stellt hasUnlockedLocalSession für UI-Wiederherstellung und enforceUnlockedSession wieder her; persistSessionUserId setzt userId nur bei Angabe in der Server-Antwort. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+9
-6
@@ -18,7 +18,8 @@ import { UnsavedChangesProvider, useUnsavedChangesContext } from './context/Unsa
|
|||||||
import {
|
import {
|
||||||
logoutUser,
|
logoutUser,
|
||||||
checkServerSession,
|
checkServerSession,
|
||||||
hasUnlockedLocalCrypto
|
hasUnlockedLocalSession,
|
||||||
|
persistSessionUserId
|
||||||
} from './services/auth.js'
|
} 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'
|
||||||
@@ -225,7 +226,8 @@ 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 && !hasUnlockedLocalCrypto()) {
|
// Require full local session (incl. userId) so API calls are not left headless.
|
||||||
|
if (isAuthenticated && !hasUnlockedLocalSession()) {
|
||||||
clearAuthenticatedAppState()
|
clearAuthenticatedAppState()
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
@@ -267,11 +269,12 @@ function App() {
|
|||||||
const session = await checkServerSession()
|
const session = await checkServerSession()
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
|
|
||||||
if (session.authenticated && session.userId) {
|
if (session.authenticated) {
|
||||||
localStorage.setItem('active_userid', session.userId)
|
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)
|
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')
|
||||||
@@ -280,7 +283,7 @@ function App() {
|
|||||||
setActiveLogbookTitle(savedLogbookTitle)
|
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) {
|
} catch (err) {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
console.warn('Session restore failed:', err)
|
console.warn('Session restore failed:', err)
|
||||||
|
|||||||
@@ -56,6 +56,13 @@ export function hasUnlockedLocalSession(): boolean {
|
|||||||
return hasUnlockedLocalCrypto() && !!localStorage.getItem('active_userid')
|
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<boolean> {
|
export async function reauthWithPasskey(): Promise<boolean> {
|
||||||
const options = await apiJson<any>(`${API_BASE}/reauth-options`, {
|
const options = await apiJson<any>(`${API_BASE}/reauth-options`, {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
|
|||||||
@@ -32,3 +32,22 @@ describe('local session unlock checks', () => {
|
|||||||
expect(hasUnlockedLocalCrypto()).toBe(false)
|
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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user