fix(auth): prevent unexpected session logout

This commit is contained in:
shuaiplus
2026-07-13 02:11:12 +08:00
parent e25ec159bb
commit b731a014f1
24 changed files with 489 additions and 191 deletions
+47 -2
View File
@@ -253,6 +253,8 @@ export default function App() {
const [lockTimeoutMinutes, setLockTimeoutMinutesState] = useState<LockTimeoutMinutes>(() => readLockTimeoutMinutes());
const [sessionTimeoutAction, setSessionTimeoutActionState] = useState<SessionTimeoutAction>(() => readSessionTimeoutAction());
const [unlockPreparing, setUnlockPreparing] = useState(() => initialBootstrap.phase === 'locked' && !initialBootstrap.session?.email);
const [lockedSessionRefreshError, setLockedSessionRefreshError] = useState('');
const [lockedSessionRetryKey, setLockedSessionRetryKey] = useState(0);
const [confirm, setConfirm] = useState<AppConfirmState | null>(null);
const [mobileLayout, setMobileLayout] = useState(false);
@@ -269,6 +271,7 @@ export default function App() {
const [vaultDecryptError, setVaultDecryptError] = useState('');
const [sendsDecryptDone, setSendsDecryptDone] = useState(false);
const sessionRef = useRef<SessionState | null>(initialBootstrap.session);
const lockedSessionRetryAttemptRef = useRef(0);
const silentRefreshVaultRef = useRef<() => Promise<void>>(async () => {});
const refreshAuthorizedDevicesRef = useRef<() => Promise<void>>(async () => {});
const refreshPendingAuthRequestsRef = useRef<() => Promise<void>>(async () => {});
@@ -503,13 +506,15 @@ export default function App() {
if (phase !== 'locked' || !session) return;
if (IS_DEMO_MODE) return;
let cancelled = false;
let retryTimerId: number | null = null;
void (async () => {
const result = await hydrateLockedSession(session, profile);
if (cancelled) return;
if (!result.session) {
if (result.kind === 'expired') {
setSession(null);
setProfile(null);
setUnlockPreparing(false);
setLockedSessionRefreshError('');
setPhase('login');
if (location !== '/login') navigate('/login');
return;
@@ -518,11 +523,43 @@ export default function App() {
if (result.profile) {
setProfile(stripProfileSecrets(result.profile));
}
if (result.kind === 'transient') {
setUnlockPreparing(false);
setLockedSessionRefreshError(result.message || t('txt_session_refresh_temporarily_unavailable'));
const retrySchedule = [2_000, 5_000, 15_000, 30_000, 60_000];
const scheduledDelay = retrySchedule[Math.min(lockedSessionRetryAttemptRef.current, retrySchedule.length - 1)];
lockedSessionRetryAttemptRef.current += 1;
const retryAfterMs = Math.min(60_000, Math.max(scheduledDelay, result.retryAfterMs || 0));
retryTimerId = window.setTimeout(() => {
setLockedSessionRetryKey((value) => value + 1);
}, retryAfterMs);
return;
}
lockedSessionRetryAttemptRef.current = 0;
setLockedSessionRefreshError('');
})();
return () => {
cancelled = true;
if (retryTimerId !== null) window.clearTimeout(retryTimerId);
};
}, [phase, session?.email, location, navigate]);
}, [phase, session?.email, location, navigate, lockedSessionRetryKey]);
useEffect(() => {
if (!lockedSessionRefreshError || phase !== 'locked') return;
const retryNow = () => {
lockedSessionRetryAttemptRef.current = 0;
setLockedSessionRetryKey((value) => value + 1);
};
const handleVisibility = () => {
if (document.visibilityState === 'visible') retryNow();
};
window.addEventListener('online', retryNow);
document.addEventListener('visibilitychange', handleVisibility);
return () => {
window.removeEventListener('online', retryNow);
document.removeEventListener('visibilitychange', handleVisibility);
};
}, [lockedSessionRefreshError, phase]);
async function finalizeLogin(login: CompletedLogin) {
loginScopedBackupRepairAuthRef.current =
@@ -536,6 +573,7 @@ export default function App() {
setSession(login.session);
setProfile(login.profile);
setUnlockPreparing(false);
setLockedSessionRefreshError('');
setPendingTotp(null);
setPendingTotpMode(null);
setPendingPasskeyPassword(null);
@@ -884,6 +922,7 @@ export default function App() {
setPendingTotpMode(null);
setTotpCode('');
setUnlockPreparing(false);
setLockedSessionRefreshError('');
setPhase('locked');
navigate('/lock');
}
@@ -2221,6 +2260,7 @@ export default function App() {
unlockPlaceholder={IS_DEMO_MODE ? t('txt_demo_unlock_placeholder') : undefined}
unlockReady={!!session?.email}
unlockPreparing={unlockPreparing}
sessionRefreshError={lockedSessionRefreshError}
loginValues={loginValues}
pendingPasskeyPasswordEmail={pendingPasskeyPassword?.email || null}
passkeyPassword={passkeyPassword}
@@ -2261,6 +2301,11 @@ export default function App() {
onLogout={logoutNow}
onTogglePasswordHint={() => void handleTogglePasswordHint()}
onShowLockedPasswordHint={handleShowLockedPasswordHint}
onRetrySessionRefresh={() => {
lockedSessionRetryAttemptRef.current = 0;
setLockedSessionRefreshError('');
setLockedSessionRetryKey((value) => value + 1);
}}
/>
<AppGlobalOverlays
toasts={toasts}