mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-12 08:16:09 +00:00
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:
1 parent
f14af72c8d
commit
63c38e79d2
5 files changed
+105
-18
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user