fix(front): clean localStorage on user change

- fix issue with selected drive and user logout/login via another user
    (was raising a "404 not found")
    - normalize all localStorage to "oxi-" prefix
    - add a specific frontend/AGENTS.md for frontend part (stop increasing the global AGENTS.md)
This commit is contained in:
Edouard Vanbelle
2026-07-07 20:47:51 +02:00
parent 4fb2acf160
commit 3108fed228
14 changed files with 188 additions and 33 deletions
+1 -1
View File
@@ -72,7 +72,7 @@ export type Section =
| 'photos'
| 'music';
const VIEW_KEY = 'oxicloud_view_mode';
const VIEW_KEY = 'oxi-view-mode';
function readViewMode(): ViewMode {
if (typeof localStorage === 'undefined') return 'grid';
+1 -1
View File
@@ -30,7 +30,7 @@ it('shows the owner as "Me" for the current user and a short id otherwise', () =
it('persists the view mode and toggles selection', () => {
files.setViewMode('list');
expect(files.viewMode).toBe('list');
expect(localStorage.getItem('oxicloud_view_mode')).toBe('list');
expect(localStorage.getItem('oxi-view-mode')).toBe('list');
files.setViewMode('grid');
expect(files.viewMode).toBe('grid');
+16 -1
View File
@@ -9,6 +9,7 @@
import { fetchMe, tryRefresh } from '$lib/api/endpoints/auth';
import { drives } from '$lib/stores/drives.svelte';
import type { User } from '$lib/api/types';
import { ensureActiveUser } from '$lib/utils/localStoragePrefs';
class SessionStore {
user = $state<User | null>(null);
@@ -32,7 +33,8 @@ class SessionStore {
if (!me && (await tryRefresh())) {
me = await fetchMe();
}
this.user = me;
if (me) this.setUser(me);
else this.user = null;
} catch {
this.user = null;
}
@@ -40,6 +42,19 @@ class SessionStore {
return this.user;
}
/**
* Set the authenticated user AND run per-user localStorage cleanup
* (see `$lib/utils/localStoragePrefs::ensureActiveUser`). Direct
* `session.user = …` assignments skip the cleanup — always call
* `setUser` on login-flow entry points (form login, OIDC exchange,
* existing-session probe) so a switch-account flow inside the same
* tab observes the wipe.
*/
setUser(user: User): void {
this.user = user;
ensureActiveUser(user.id);
}
/**
* Re-fetch the authenticated user from the server, bypassing the one-shot
* `load()` cache. Call after operations that change server-side user state —
+19 -9
View File
@@ -1,19 +1,29 @@
/**
* Theme store — light / dark / auto.
*
* Mirrors the established behaviour: persists to the `oxicloud_theme` localStorage
* key and reflects the choice on `<html data-color-scheme>`. `auto` removes the
* attribute so the OS `prefers-color-scheme` takes over. The anti-FOUC inline
* script in app.html applies the stored value before first paint; this store
* owns runtime changes from the UI.
* Persists to the `oxi-theme` localStorage key (part of the normalised
* `oxi-*` prefs namespace — see `$lib/utils/localStoragePrefs`) and
* reflects the choice on `<html data-color-scheme>`. `auto` removes the
* attribute so the OS `prefers-color-scheme` takes over. The anti-FOUC
* inline script in app.html applies the stored value before first paint;
* this store owns runtime changes from the UI.
*/
export type Theme = 'light' | 'dark' | 'auto';
const STORAGE_KEY = 'oxicloud_theme';
/**
* localStorage key holding the active theme.
*
* Exported (not just module-private) because `src/app.html`'s anti-FOUC
* inline script also reads it — that script runs before any JS bundle
* loads, so it can't `import` here. The `app.html` copy is a hardcoded
* string kept in sync by `theme.test.ts::app.html theme key matches
* THEME_STORAGE_KEY`, which fails CI on any drift.
*/
export const THEME_STORAGE_KEY = 'oxi-theme';
function readInitial(): Theme {
if (typeof localStorage === 'undefined') return 'auto';
const v = localStorage.getItem(STORAGE_KEY);
const v = localStorage.getItem(THEME_STORAGE_KEY);
return v === 'light' || v === 'dark' ? v : 'auto';
}
@@ -29,8 +39,8 @@ function apply(theme: Theme): void {
export function setTheme(theme: Theme): void {
store.theme = theme;
if (typeof localStorage !== 'undefined') {
if (theme === 'auto') localStorage.removeItem(STORAGE_KEY);
else localStorage.setItem(STORAGE_KEY, theme);
if (theme === 'auto') localStorage.removeItem(THEME_STORAGE_KEY);
else localStorage.setItem(THEME_STORAGE_KEY, theme);
}
apply(theme);
}
+24 -4
View File
@@ -1,5 +1,7 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, it, expect, beforeEach } from 'vitest';
import { theme, setTheme } from './theme.svelte';
import { theme, setTheme, THEME_STORAGE_KEY } from './theme.svelte';
describe('theme store', () => {
beforeEach(() => {
@@ -9,21 +11,39 @@ describe('theme store', () => {
it('sets light/dark, persists, and reflects on <html>', () => {
setTheme('light');
expect(theme.current).toBe('light');
expect(localStorage.getItem('oxicloud_theme')).toBe('light');
expect(localStorage.getItem(THEME_STORAGE_KEY)).toBe('light');
expect(document.documentElement.getAttribute('data-color-scheme')).toBe('light');
setTheme('dark');
expect(document.documentElement.getAttribute('data-color-scheme')).toBe('dark');
expect(localStorage.getItem('oxicloud_theme')).toBe('dark');
expect(localStorage.getItem(THEME_STORAGE_KEY)).toBe('dark');
});
it('auto clears storage and removes the attribute', () => {
setTheme('dark');
setTheme('auto');
expect(theme.current).toBe('auto');
expect(localStorage.getItem('oxicloud_theme')).toBeNull();
expect(localStorage.getItem(THEME_STORAGE_KEY)).toBeNull();
expect(document.documentElement.hasAttribute('data-color-scheme')).toBe(false);
});
it('theme.set is an alias for setTheme', () => {
theme.set('light');
expect(theme.current).toBe('light');
});
// Drift guard: `src/app.html` inlines an anti-FOUC theme reader that
// reads the SAME localStorage key. Because that script runs before
// any JS bundle loads, it can't `import { THEME_STORAGE_KEY }`;
// the key is hardcoded there. This test reads the file verbatim
// and refuses drift.
it('app.html theme key matches THEME_STORAGE_KEY', () => {
// Resolve against Vitest's cwd (the `frontend/` dir per its
// invocation) — jsdom rewrites `import.meta.url` to `http://…`,
// so file-URL conversion doesn't work in this environment.
const appHtmlPath = resolve('src/app.html');
const html = readFileSync(appHtmlPath, 'utf-8');
expect(
html.includes(`localStorage.getItem('${THEME_STORAGE_KEY}')`),
`app.html must call localStorage.getItem('${THEME_STORAGE_KEY}') — ` +
`update the inline script when THEME_STORAGE_KEY changes.`
).toBe(true);
});
});