From 15bacacafab0ee7b900a81b0145687712d108428 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Fri, 18 Sep 2026 21:48:19 +0700 Subject: [PATCH] web: logging out ends the studio session, not just the cookie MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A session followed the browser, not the account: log in, open a frame, log out, come back as a guest — the same photo stood on the stage, because the studio's own store (localStorage knobs + the photo in IndexedDB) outlived the cookie with nothing to clear it. clearSession() now drops both, and the three log-out buttons call it. The studio's own button reloads after the delete has committed — a reload mid- delete aborts the transaction, so the promise resolves on tx.oncomplete, not on the request. The account's frames are untouched: they reopen from MY PHOTOS. --- docker/frontend/src/App.tsx | 7 +++++-- docker/frontend/src/Landing.tsx | 2 ++ docker/frontend/src/ProfilePage.tsx | 2 ++ docker/frontend/src/engine/session.ts | 28 +++++++++++++++++++++++++++ 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/docker/frontend/src/App.tsx b/docker/frontend/src/App.tsx index d73361a..cda119c 100644 --- a/docker/frontend/src/App.tsx +++ b/docker/frontend/src/App.tsx @@ -12,7 +12,7 @@ import { useI18n } from './i18n/I18nProvider'; import { loadEngine } from './engine/assets'; import { renderPhoto, type RenderAssets } from './engine/exportEngine'; import { download, fileToBytes, readGps, readIso, resizedJpeg } from './engine/imageOps'; -import { loadPhoto, loadState, savePhoto, saveState } from './engine/session'; +import { clearSession, loadPhoto, loadState, savePhoto, saveState } from './engine/session'; import { api, type Photo, type SavedRecipe, type User } from './api'; import { TIERS } from './config/tiers'; import { DEFAULT_ADJUSTMENTS, DEFAULT_RECIPES, FILM_SIMS, filmSimToRecipe } from '../shared/utils/defaultRecipes'; @@ -1740,7 +1740,10 @@ export function Workspace() { onSignup={() => setAuthMode('signup')} onLogout={async () => { await api.logout().catch(() => undefined); - setUser(null); + await clearSession(); + // the stage still holds the old photo in memory; a reload is the one + // line that empties it and every knob with it. + window.location.reload(); }} /> diff --git a/docker/frontend/src/Landing.tsx b/docker/frontend/src/Landing.tsx index 174044b..cd1a3f4 100644 --- a/docker/frontend/src/Landing.tsx +++ b/docker/frontend/src/Landing.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { api, type Photo, type PhotoSlot, type Rating, type User } from './api'; +import { clearSession } from './engine/session'; import { useI18n } from './i18n/I18nProvider'; import { ACCENTS, FONT_SETS, useTheme, type FontSetId } from './theme/ThemeProvider'; import { AuthModal } from './ui/AuthModal'; @@ -308,6 +309,7 @@ export function Landing() { const signOut = useCallback(async () => { await api.logout().catch(() => undefined); + void clearSession(); setUser(null); setAccountMenu(false); }, []); diff --git a/docker/frontend/src/ProfilePage.tsx b/docker/frontend/src/ProfilePage.tsx index 2db4dad..5324857 100644 --- a/docker/frontend/src/ProfilePage.tsx +++ b/docker/frontend/src/ProfilePage.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; import { api, type User } from './api'; +import { clearSession } from './engine/session'; import { useI18n } from './i18n/I18nProvider'; import { Profile } from './ui/Profile'; import { Avatar } from './ui/Avatar'; @@ -36,6 +37,7 @@ export function ProfilePage() { data-key="profile-logout" onClick={async () => { await api.logout().catch(() => undefined); + await clearSession(); window.location.assign('/'); }} > diff --git a/docker/frontend/src/engine/session.ts b/docker/frontend/src/engine/session.ts index 7cd1d10..94d4a88 100644 --- a/docker/frontend/src/engine/session.ts +++ b/docker/frontend/src/engine/session.ts @@ -88,3 +88,31 @@ export async function loadPhoto(): Promise<{ name: string; bytes: Uint8Array } | return null; } } + +// Signing out ends the session: drop the knobs and the working photo, so the +// next visit — guest or another account — opens the studio empty. The account's +// own photos stay in MY PHOTOS, ready to reopen. +// +// Async on purpose: the caller reloads the moment this returns, and a reload +// aborts a delete still in flight. Resolve on the transaction's completion, not +// on the request's success, or the bytes outlive the logout. +export async function clearSession(): Promise { + try { + localStorage.removeItem(STATE_KEY); + } catch { + // Nothing to clear. + } + try { + const db = await openDb(); + await new Promise((resolve, reject) => { + const tx = db.transaction(STORE, 'readwrite'); + tx.objectStore(STORE).delete(PHOTO_KEY); + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + tx.onabort = () => reject(tx.error); + }); + db.close(); + } catch { + // Private mode / no IndexedDB: there is nothing stored to forget. + } +}