Stockage local : vieux mots de passe effacés au démarrage, compte effacé à la déconnexion

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 <noreply@anthropic.com>
This commit is contained in:
IliesAllaliandClaude Opus 5.5 committed 2026-09-30 17:57:57 +02:00
1 parent f14af72c8d
commit 63c38e79d2
5 files changed
+105 -18

No files matched your search

-17
View File
@@ -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
+4 -1
View File
@@ -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
}
+2
View File
@@ -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()
+45
View File
@@ -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_<arbre>_<rôle>).
// 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)
}
+54
View File
@@ -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()
})
})