diff --git a/web/src/lib/components/auth/LegalFooter.svelte b/web/src/lib/components/auth/LegalFooter.svelte new file mode 100644 index 00000000..7b728f8c --- /dev/null +++ b/web/src/lib/components/auth/LegalFooter.svelte @@ -0,0 +1,51 @@ + + +{#if cloudMode} + +{/if} + + diff --git a/web/src/lib/stores/auth.svelte.ts b/web/src/lib/stores/auth.svelte.ts index 0fd7f86d..92d0086f 100644 --- a/web/src/lib/stores/auth.svelte.ts +++ b/web/src/lib/stores/auth.svelte.ts @@ -2,6 +2,16 @@ import { api, type AuthSession } from '$lib/api/client'; let session = $state(null); let loading = $state(false); +// Coalesces concurrent load() calls so the root layout and auth-page onMount +// (register, forgot-password) can both request the session without firing +// duplicate /auth/session requests — or, worse, having a late failure from a +// duplicate fetch overwrite a successful fetch's session=null. +let inflight: Promise | null = null; +// Bumps on clear(). Any fetch started in a previous generation is stale: its +// success must not resurrect a logged-out session, and its finally must not +// clobber a new inflight that started after clear(). Readers only write +// state when the generation they captured at fetch-start still matches. +let generation = 0; export const authStore = { get session() { return session; }, @@ -12,20 +22,48 @@ export const authStore = { get loading() { return loading; }, async load() { + if (inflight) return inflight; loading = true; - try { - session = await api.auth.session(); - } catch (err) { - session = null; - loading = false; - throw err; // Re-throw so callers can distinguish fetch errors from "not authenticated". - } finally { - loading = false; - } - return session; + const myGeneration = generation; + const isCurrent = () => generation === myGeneration; + inflight = api.auth.session() + .then((s) => { + if (isCurrent()) session = s; + return session; + }) + .catch((err) => { + if (isCurrent()) session = null; + throw err; // Re-throw so callers can distinguish fetch errors from "not authenticated". + }) + .finally(() => { + if (isCurrent()) { + loading = false; + inflight = null; + } + }); + return inflight; + }, + + // ensureLoaded returns the cached session when one exists, otherwise fetches it. + // Use this on auth pages (register, forgot-password, etc.) that navigate in via + // SPA routing after the user has logged out — logout clears the store, and the + // root layout's one-shot onMount doesn't re-run on subsequent navigation, so a + // page that relies on session fields (e.g. cloud_mode) would otherwise see + // stale nulls and render the self-hosted branch on Pad Cloud. Concurrent calls + // coalesce through load()'s in-flight promise. + async ensureLoaded() { + if (session) return session; + return this.load(); }, clear() { session = null; + generation++; + // Drop the in-flight promise reference so the next ensureLoaded()/load() + // call fires a fresh fetch rather than attaching to a pre-logout request. + // The old promise may still resolve/reject in the background; the + // generation guard above prevents it from writing to any state. + inflight = null; + loading = false; } }; diff --git a/web/src/routes/forgot-password/+page.svelte b/web/src/routes/forgot-password/+page.svelte index e26208d8..c8b71ef5 100644 --- a/web/src/routes/forgot-password/+page.svelte +++ b/web/src/routes/forgot-password/+page.svelte @@ -1,11 +1,23 @@