Preserve native sessions during temporary server outages
Client checks / checks (push) Canceled after 0s
Client dependency and security checks / audit (push) Canceled after 0s

This commit is contained in:
2026-10-25 17:51:10 +01:00
parent 58df8d06e1
commit 7aef4171af
5 changed files with 79 additions and 6 deletions
+36 -6
View File
@@ -1,6 +1,11 @@
<script lang="ts">
import { onMount } from 'svelte';
import { invoke } from '@tauri-apps/api/core';
import { sessionResponseState, sessionRetryDelay } from './session-recovery.js';
let sessionRestoreTimer: ReturnType<typeof setTimeout> | undefined;
let sessionRestoreAttempt = 0;
let sessionRestoreGeneration = 0;
type ClientSettings = { server_address: string; username: string };
@@ -63,30 +68,51 @@
async function restoreNativeSession() {
if (!nativeClientAvailable() || !username || !serverAddress) return;
const generation = ++sessionRestoreGeneration;
clearTimeout(sessionRestoreTimer);
let address = serverAddress;
const account = username;
const isCurrentRestore = () => generation === sessionRestoreGeneration && address === serverAddress && account === username;
try {
const safeServerAddress = validatedServerAddress(serverAddress);
serverAddress = safeServerAddress;
address = safeServerAddress;
const settings: ClientSettings = { server_address: safeServerAddress, username };
const storedSession = await invoke<string | null>('load_server_session', { settings });
if (!storedSession) return;
if (!storedSession || !isCurrentRestore()) return;
const response = await fetch(`${safeServerAddress}/api/v1/auth/session`, {
headers: { authorization: `Bearer ${storedSession}` },
});
if (!response.ok) {
if (!isCurrentRestore()) return;
const state = sessionResponseState(response.status);
if (state === 'revoked') {
await invoke('clear_server_session', { settings });
if (!isCurrentRestore()) return;
sessionToken = '';
authenticated = false;
authError = 'Authentication revoked. Log in again.';
return;
}
if (state !== 'valid') throw new Error('Session verification unavailable');
const result: { must_replace_credentials: boolean } = await response.json();
if (!isCurrentRestore()) return;
sessionRestoreAttempt = 0;
authError = '';
sessionToken = storedSession;
authenticated = true;
mustReplaceCredentials = result.must_replace_credentials;
connectionState = 'CONNECTED';
await loadAuthenticatedData();
await loadAuthenticatedData().catch(() => {
authError = 'Authenticated, but dashboard data could not be loaded.';
});
if (mustReplaceCredentials) newUsername = username;
} catch {
// A stale keyring session must not prevent the normal login form from opening.
sessionToken = '';
authenticated = false;
if (!isCurrentRestore()) return;
// Transport/server failures are not proof of revocation: preserve the keyring.
authError = 'Server unavailable. Retrying saved session.';
sessionRestoreTimer = setTimeout(() => {
if (isCurrentRestore()) void restoreNativeSession();
}, sessionRetryDelay(sessionRestoreAttempt++));
}
}
@@ -336,6 +362,8 @@
}
async function login() {
sessionRestoreGeneration += 1;
clearTimeout(sessionRestoreTimer);
if (connectionState === 'INCOMPATIBLE') {
authError = 'This client version is not compatible with the server.';
return;
@@ -449,6 +477,8 @@
}
async function logout() {
sessionRestoreGeneration += 1;
clearTimeout(sessionRestoreTimer);
realtimeSocket?.close();
realtimeSocket = undefined;
realtimeState = 'DISCONNECTED';