diff --git a/client/src/App.tsx b/client/src/App.tsx
index d316d83..6a6d6bd 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -97,6 +97,8 @@ function App() {
const [isDemoMode, setIsDemoMode] = useState(() => window.location.pathname === '/demo')
const [isAdminRoute, setIsAdminRoute] = useState(() => window.location.pathname.startsWith('/admin'))
const [isAdminUser, setIsAdminUser] = useState(false)
+ const [sessionChecked, setSessionChecked] = useState(false)
+ const [serverSessionActive, setServerSessionActive] = useState(false)
const syncQueueCount = useLiveQuery(
() => activeLogbookId ? db.syncQueue.where({ logbookId: activeLogbookId }).count() : db.syncQueue.count(),
@@ -316,6 +318,8 @@ function App() {
const session = await checkServerSession()
if (cancelled) return
+ setServerSessionActive(session.authenticated)
+
if (session.authenticated) {
persistSessionUserId(session.userId)
}
@@ -335,6 +339,10 @@ function App() {
if (!cancelled) {
console.warn('Session restore failed:', err)
}
+ } finally {
+ if (!cancelled) {
+ setSessionChecked(true)
+ }
}
})()
@@ -618,7 +626,17 @@ function App() {
if (!isAuthenticated) {
return (
-
+ {!sessionChecked ? (
+
+
{t('auth.restore_checking')}
+
+ ) : (
+
+ )}
)
}
diff --git a/client/src/components/AuthOnboarding.tsx b/client/src/components/AuthOnboarding.tsx
index f0f983e..3262678 100644
--- a/client/src/components/AuthOnboarding.tsx
+++ b/client/src/components/AuthOnboarding.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react'
+import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { cycleAppLanguage, getNextLanguage } from '../utils/i18nLanguages.js'
import {
@@ -12,7 +12,8 @@ import {
getKnownUsernames,
forgetUsername,
hasUnlockedLocalSession,
- logoutUser
+ logoutUser,
+ resolveRestoreUsername
} from '../services/auth.js'
import { KeyRound, ShieldAlert, Languages, HelpCircle, UserRound, X } from 'lucide-react'
import RegistrationDisclaimer from './RegistrationDisclaimer.tsx'
@@ -27,9 +28,15 @@ import {
interface AuthOnboardingProps {
onAuthenticated: () => void
onOpenDemo?: () => void
+ /** Server session cookie is valid but the in-memory master key was lost (e.g. after reload). */
+ restoreSession?: boolean
}
-export default function AuthOnboarding({ onAuthenticated, onOpenDemo }: AuthOnboardingProps) {
+export default function AuthOnboarding({
+ onAuthenticated,
+ onOpenDemo,
+ restoreSession = false
+}: AuthOnboardingProps) {
const { t, i18n } = useTranslation()
const [username, setUsername] = useState('')
const [loading, setLoading] = useState(false)
@@ -60,7 +67,10 @@ export default function AuthOnboarding({ onAuthenticated, onOpenDemo }: AuthOnbo
const [isNewRegistration, setIsNewRegistration] = useState(false)
const [showDisclaimer, setShowDisclaimer] = useState(false)
const [showHelp, setShowHelp] = useState(false)
+ const [showStandardLogin, setShowStandardLogin] = useState(false)
+ const autoUnlockAttempted = useRef(false)
+ const isRestoreFlow = restoreSession && !showStandardLogin
const passkeyHostOk = isPasskeyCompatibleLocation()
const passkeyCompatibleUrl = passkeyHostOk ? null : toPasskeyCompatibleUrl(window.location.href)
@@ -144,6 +154,23 @@ export default function AuthOnboarding({ onAuthenticated, onOpenDemo }: AuthOnbo
}
}
+ useEffect(() => {
+ if (!isRestoreFlow || autoUnlockAttempted.current) return
+
+ const user = resolveRestoreUsername()
+ if (user && hasLocalPin(user)) {
+ autoUnlockAttempted.current = true
+ setUsername(user)
+ setShowPinLogin(true)
+ return
+ }
+
+ if (user && passkeyHostOk) {
+ autoUnlockAttempted.current = true
+ void handleLogin(user)
+ }
+ }, [isRestoreFlow, passkeyHostOk])
+
const handleRecoverySubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!recoveryInput.trim() || !encryptedPayloads) return
@@ -347,10 +374,10 @@ export default function AuthOnboarding({ onAuthenticated, onOpenDemo }: AuthOnbo
-
{t('auth.enter_pin_title')}
+ {isRestoreFlow ? t('auth.restore_title') : t('auth.enter_pin_title')}
- {t('auth.enter_pin_warning')}
+ {isRestoreFlow ? t('auth.restore_pin_warning') : t('auth.enter_pin_warning')}