From 03ac99e555eb42ba6a93a4d85f64d39ba55eec84 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Mon, 25 May 2026 17:35:27 +0200 Subject: [PATCH] fix(spa): don't log user out on permalink reload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit App.svelte's onMount used to call api.logout() whenever it detected an existing server session at boot, on the theory that "we can't decrypt without the DEK so the session is half-broken anyway." That destroyed the user's session on every full-page load — including clicking the plain permalink in ActivityRow, which navigates the browser instead of routing client-side. Symptom reported by the user: clicking a permalink for a private activity returned "fant ikke aktiviteten" (because the now-anonymous caller can't read private rows), and the back button left them logged out (because session.user was never re-hydrated). Fix: keep the server session on reload and re-hydrate session.user from /me. The DEK is still intentionally absent (it never persists), so private rows that the SPA can't decrypt now show a clear "logg inn på nytt med passordet ditt for å vise det" message instead of a stuck "Dekrypterer …" spinner. Public / semi / friends content keeps working without re-authentication. Co-Authored-By: Claude Opus 4.7 --- frontend/src/App.svelte | 17 +++++++++++------ frontend/src/components/ActivityRow.svelte | 7 +++++++ frontend/src/lib/session.svelte.ts | 10 ++++++++++ 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index f37ecbb..1c2167f 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -2,7 +2,7 @@ import { onMount } from 'svelte'; import { ready } from './lib/crypto'; import { api, ApiError } from './lib/api'; - import { session } from './lib/session.svelte'; + import { session, setSessionUserOnly } from './lib/session.svelte'; import { logout } from './lib/auth'; import Login from './components/Login.svelte'; import Signup from './components/Signup.svelte'; @@ -112,14 +112,19 @@ .then((s) => { selfRegistryEnabled = s.self_registry_enabled; }) .catch(() => { /* default open if the call fails */ }); - // Probe the server for an existing session, but it doesn't change which - // URL we're rendering — only what content we'd show on /home. + // Probe the server for an existing session and rehydrate client-side + // state. The DEK is intentionally NOT persisted (it lives only in + // memory), so after a reload private rows can't be decrypted until the + // user re-enters their password. We used to log out here to avoid the + // "logged in but can't decrypt" state, but that broke permalink + // navigation: clicking `` causes a full reload, this + // handler ran, and the session was destroyed — kicking the user out + // mid-click. Now: keep the server session (so non-private content, + // hearts, bookmarks, etc. all keep working) and just leave dek=null. try { const me = await api.me(); defaultEmail = me.email; - // Reloaded with a server session but no DEK. Drop the server session; - // we can't decrypt anything without the password anyway. - await api.logout(); + setSessionUserOnly(me); } catch { // No session — fine. } diff --git a/frontend/src/components/ActivityRow.svelte b/frontend/src/components/ActivityRow.svelte index 6ea08f4..f98c88e 100644 --- a/frontend/src/components/ActivityRow.svelte +++ b/frontend/src/components/ActivityRow.svelte @@ -224,6 +224,13 @@ {@render locationLine(decrypted.loc_label, null, null)} {/if} {#if decrypted.scheduled_at}

🕒 {formatDate(decrypted.scheduled_at)}

{/if} + {:else if !session.dek} +

+ Privat +

+

+ Innholdet er kryptert. Logg inn på nytt med passordet ditt for å vise det. +

{:else}

Dekrypterer …

{/if} diff --git a/frontend/src/lib/session.svelte.ts b/frontend/src/lib/session.svelte.ts index f2814a6..c03bdc9 100644 --- a/frontend/src/lib/session.svelte.ts +++ b/frontend/src/lib/session.svelte.ts @@ -19,6 +19,16 @@ export function setSession(user: MeResponse, dek: Uint8Array): void { session.dek = dek; } +/** + * Rehydrate the user identity from /me on page load. The DEK is intentionally + * left null — it never persists across reloads. Callers needing private + * decryption must run the full login flow to re-derive it. + */ +export function setSessionUserOnly(user: MeResponse): void { + session.user = user; + session.dek = null; +} + export function clearSession(): void { if (session.dek) { // Best-effort zeroisation. The buffer may have been copied internally by