feat(logout): improve logout flow

This commit is contained in:
Edouard Vanbelle
2026-08-09 14:09:54 +02:00
parent 321f3ad733
commit 2eb1e8a1d5
4 changed files with 37 additions and 10 deletions
+17
View File
@@ -17,3 +17,20 @@ export function getCsrfHeaders(): Record<string, string> {
const token = getCsrfToken();
return token ? { 'X-CSRF-Token': token } : {};
}
/**
* Best-effort "does the browser think it has a session?" hint. The server
* sets `oxicloud_csrf` (non-HttpOnly, JS-visible) alongside the session
* cookies on every login and clears it on logout, so its ABSENCE is a
* reliable proof of "no session" — cheaper than a network probe that
* would 401 → refresh 401 → 401 on first landing with no cookies.
*
* Its PRESENCE is only a hint: the session cookies (HttpOnly) may have
* been revoked server-side while the CSRF cookie lingers. Callers that
* see `true` must still probe /api/auth/me — this helper just lets a
* fresh no-cookie bootstrap skip the doomed 2× /me + /refresh burst.
*/
export function hasSessionHint(): boolean {
if (typeof document === 'undefined') return false;
return document.cookie.split('; ').some((row) => row.startsWith('oxicloud_csrf='));
}
+1 -5
View File
@@ -524,11 +524,7 @@
// landing from `?source=session_expired` (auto-divert on 401 →
// refresh 401). The login page reads the flag, shows the success
// notice, and skips its existing-session probe.
// `resolve()` from `$app/paths` normalises a route-id (adds base
// path prefix); it does NOT accept a query string. Concatenate
// the `?source=logged_out` marker AFTER resolve so the login
// page's onMount branch actually sees it.
await goto(resolve('/login') + '?source=logged_out');
await goto(resolve('/login?source=logged_out'));
}
</script>
+11
View File
@@ -8,6 +8,7 @@
*/
import { bindDpopIfPossible, fetchMe, tryRefresh } from '$lib/api/endpoints/auth';
import { setLogoutInProgress } from '$lib/api/client';
import { hasSessionHint } from '$lib/api/csrf';
import { drives } from '$lib/stores/drives.svelte';
import type { User } from '$lib/api/types';
import { ensureActiveUser } from '$lib/utils/localStoragePrefs';
@@ -41,6 +42,16 @@ class SessionStore {
*/
async load(): Promise<User | null> {
if (this.loaded) return this.user;
// No JS-visible session hint ⇒ nothing to probe. The server sets
// `oxicloud_csrf` alongside the HttpOnly session cookies and clears
// it on logout, so a missing hint means no session. Skips the
// doomed 2× /me + /refresh burst that would otherwise fire on
// every first landing / post-logout re-mount with no cookies.
if (!hasSessionHint()) {
this.user = null;
this.loaded = true;
return null;
}
try {
let me = await fetchMe();
if (!me && (await tryRefresh())) {
+8 -5
View File
@@ -22,6 +22,7 @@
} from '$lib/api/endpoints/auth';
import { i18n, SUPPORTED_LOCALES, setLocale, t, type Locale } from '$lib/i18n/index.svelte';
import { session } from '$lib/stores/session.svelte';
import { hasSessionHint } from '$lib/api/csrf';
type Mode = 'login' | 'register' | 'setup';
let mode = $state<Mode>('login');
@@ -409,11 +410,13 @@
}
// 2) Existing-session probe: if already authenticated, skip the form.
// Skipped when we KNOW the session is gone (explicit logout or
// interceptor-detected expiry) — probing would 401, the
// interceptor would retry via /refresh (also 401), and the
// sessionExpiredHandler would clobber this landing.
if (!skipExistingSessionProbe) {
// Skipped when we KNOW the session is gone: explicit logout /
// interceptor-detected expiry (both set `skipExistingSessionProbe`),
// OR the CSRF hint cookie is absent (fresh browser, no cookies at
// all — a probe would just 401). Probing anyway would trip the
// apiFetch → 401 → refresh → 401 → sessionExpiredHandler chain
// that clobbers whatever notice we're about to paint.
if (!skipExistingSessionProbe && hasSessionHint()) {
try {
const me = await fetchMe();
if (me) {