From 63c38e79d2ae0ff95826c4eab796818fca616326 Mon Sep 17 00:00:00 2001 From: IliesAllali Date: Wed, 30 Sep 2026 17:57:57 +0200 Subject: [PATCH] =?UTF-8?q?Stockage=20local=20:=20vieux=20mots=20de=20pass?= =?UTF-8?q?e=20effac=C3=A9s=20au=20d=C3=A9marrage,=20compte=20effac=C3=A9?= =?UTF-8?q?=20=C3=A0=20la=20d=C3=A9connexion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Toutes les copies invite_password_* partent au lancement de l'app, plus seulement celle de l'arbre rouvert. À la déconnexion et à la suppression du compte, les brouillons d'édition, la fiche « c'est moi » et le profil sont effacés. Le prénom du contributeur, la langue et les accueils vus restent. Co-Authored-By: Claude Opus 5.5 --- frontend/src/App.jsx | 17 -------- frontend/src/hooks/useAuth.js | 5 ++- frontend/src/main.jsx | 2 + frontend/src/utils/localHygiene.js | 45 +++++++++++++++++++++ frontend/src/utils/localHygiene.test.js | 54 +++++++++++++++++++++++++ 5 files changed, 105 insertions(+), 18 deletions(-) create mode 100644 frontend/src/utils/localHygiene.js create mode 100644 frontend/src/utils/localHygiene.test.js diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0889073..9449fbe 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -429,22 +429,6 @@ function buildContribChangesList(draft = {}) { return items } -// Anciennes copies locales du mot de passe (avant qu'il soit gardé par le serveur) : on les efface, -// elles pouvaient montrer un mot de passe changé depuis un autre appareil. -function clearLegacyInvitePasswords(treeId) { - if (!treeId) { - return - } - - try { - for (const role of ['share', 'contributor', 'visitor']) { - localStorage.removeItem(`invite_password_${treeId}_${role}`) - } - } catch { - // Ignore localStorage failures - } -} - function App() { const auth = useAuth() const tree = useTreeAccess() @@ -1284,7 +1268,6 @@ function App() { const handleOpenContributionPanel = useCallback(async () => { const treeId = tree.treeContext.treeId - clearLegacyInvitePasswords(treeId) await resolveContributionShareUrl() let share = '' const accessToken = auth.userAuth.token || tree.treeContext.accessToken diff --git a/frontend/src/hooks/useAuth.js b/frontend/src/hooks/useAuth.js index 7dcad74..8358e6a 100644 --- a/frontend/src/hooks/useAuth.js +++ b/frontend/src/hooks/useAuth.js @@ -10,6 +10,7 @@ import { import { listTrees } from '../api/treeApi' import * as errorMessages from '../utils/errorMessages' import { t } from '../i18n/index.js' +import { clearAccountLocalData } from '../utils/localHygiene.js' const USER_TOKEN_STORAGE_KEY = 'user_auth_token' const USER_PROFILE_STORAGE_KEY = 'user_auth_profile' @@ -127,7 +128,8 @@ export function useAuth() { // ignore logout errors and clear local session anyway } finally { localStorage.removeItem(USER_TOKEN_STORAGE_KEY) - localStorage.removeItem(USER_PROFILE_STORAGE_KEY) + // Brouillons, fiche « c'est moi » et profil : rien du compte ne reste pour la personne suivante + clearAccountLocalData() setUserAuth({ token: '', user: null }) setAccountTrees([]) setLastOpenedTree(null) @@ -159,6 +161,7 @@ export function useAuth() { } clearStoredUserToken() + clearAccountLocalData() return null } diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 59bb491..a599348 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -7,6 +7,7 @@ import App from './App.jsx' import ErrorBoundary from './components/ErrorBoundary.jsx' import BootLoader from './components/BootLoader.jsx' import { initAppAnalytics } from './utils/analytics.js' +import { sweepLegacyLocalData } from './utils/localHygiene.js' const setFavicon = (href) => { if (typeof document === 'undefined') return @@ -23,6 +24,7 @@ const setFavicon = (href) => { upsertLink(`link[rel="apple-touch-icon"]`, 'apple-touch-icon') } +sweepLegacyLocalData() setFavicon(faviconUrl) initAppAnalytics() diff --git a/frontend/src/utils/localHygiene.js b/frontend/src/utils/localHygiene.js new file mode 100644 index 0000000..5db7c31 --- /dev/null +++ b/frontend/src/utils/localHygiene.js @@ -0,0 +1,45 @@ +// Ménage du stockage local de l'appareil. +// +// Ce qui reste volontairement : le prénom de la personne qui contribue, la langue choisie, les +// accueils déjà vus, les accès aux arbres partagés. + +// Le mot de passe de partage était autrefois recopié ici (invite_password__). +// Il est gardé par le serveur désormais : toute copie restante est effacée au démarrage. +const LEGACY_PREFIXES = ['invite_password_'] + +// Ce qui appartient au compte connecté et ne doit pas rester pour la personne suivante sur +// l'appareil : brouillons d'édition, fiche « c'est moi » par arbre, profil du compte. +const ACCOUNT_PREFIXES = ['edit-draft-', 'account_self_person_'] +const ACCOUNT_KEYS = ['user_auth_profile'] + +function defaultStorage() { + try { + return globalThis.localStorage || null + } catch { + return null + } +} + +function removeMatchingKeys(storage, prefixes, exactKeys = []) { + if (!storage) return + try { + const doomed = [] + for (let i = 0; i < storage.length; i += 1) { + const key = storage.key(i) + if (key && (exactKeys.includes(key) || prefixes.some((prefix) => key.startsWith(prefix)))) { + doomed.push(key) + } + } + doomed.forEach((key) => storage.removeItem(key)) + } catch { + // Stockage indisponible (navigation privée, quota) : rien à nettoyer + } +} + +export function sweepLegacyLocalData(storage = defaultStorage()) { + removeMatchingKeys(storage, LEGACY_PREFIXES) +} + +export function clearAccountLocalData(storage = defaultStorage()) { + removeMatchingKeys(storage, ACCOUNT_PREFIXES, ACCOUNT_KEYS) +} diff --git a/frontend/src/utils/localHygiene.test.js b/frontend/src/utils/localHygiene.test.js new file mode 100644 index 0000000..9c7491f --- /dev/null +++ b/frontend/src/utils/localHygiene.test.js @@ -0,0 +1,54 @@ +import { describe, expect, it } from 'vitest' +import { clearAccountLocalData, sweepLegacyLocalData } from './localHygiene' + +function memoryStorage(initial) { + const map = new Map(Object.entries(initial)) + return { + get length() { return map.size }, + key: (i) => [...map.keys()][i] ?? null, + getItem: (k) => (map.has(k) ? map.get(k) : null), + setItem: (k, v) => map.set(k, String(v)), + removeItem: (k) => map.delete(k), + keys: () => [...map.keys()].sort(), + } +} + +const KEPT = { + parenthese_contributor_name: 'Camille', + 'pz-locale': 'en', + family_welcome_seen_t1: '1', + tree_access_token_t1: 'share-token', + user_auth_token: 'tok', +} + +describe('local storage hygiene', () => { + it('removes every legacy share password copy and nothing else', () => { + const storage = memoryStorage({ + ...KEPT, + invite_password_t1_share: 'secret', + invite_password_t2_contributor: 'secret', + invite_password_t3_visitor: 'secret', + 'edit-draft-t1': '{}', + }) + sweepLegacyLocalData(storage) + expect(storage.keys()).toEqual([...Object.keys(KEPT), 'edit-draft-t1'].sort()) + }) + + it('clears account data on logout but keeps device preferences', () => { + const storage = memoryStorage({ + ...KEPT, + 'edit-draft-t1': '{}', + 'edit-draft-t2': '{}', + 'account_self_person_a@b.fr_t1': 'p1', + user_auth_profile: '{"email":"a@b.fr"}', + }) + clearAccountLocalData(storage) + expect(storage.keys()).toEqual(Object.keys(KEPT).sort()) + }) + + it('survives a missing or broken storage', () => { + expect(() => sweepLegacyLocalData(null)).not.toThrow() + const broken = { get length() { throw new Error('denied') } } + expect(() => clearAccountLocalData(broken)).not.toThrow() + }) +})