mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 00:14:12 +00:00
fix: forget delegate names on sign-out and when their account is removed
This commit is contained in:
@@ -17,7 +17,7 @@ describe('purgeSignedOutData', () => {
|
||||
it("removes the accounts' leftovers and keeps device preferences", () => {
|
||||
for (const key of [
|
||||
'email-snapshot', 'pro-tabs', 'search-history-storage', 'calendar-storage',
|
||||
'files-favorites', 'files-recent-files', 'files-path-stack',
|
||||
'files-favorites', 'files-recent-files', 'files-path-stack', 'bulwark-delegate-names',
|
||||
'settings-storage', 'theme-storage', 'locale-storage', 'template-storage', 'files-view-mode',
|
||||
]) localStorage.setItem(key, 'x');
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { forgetPrincipals, rememberPrincipals, rememberedPrincipals } from '../known-principals';
|
||||
import type { Principal } from '@/lib/jmap/types';
|
||||
|
||||
const person = (id: string): Principal => ({ id, type: 'individual', name: `${id}@example.org`, email: `${id}@example.org` });
|
||||
|
||||
beforeEach(() => localStorage.clear());
|
||||
|
||||
describe('known principals', () => {
|
||||
it('keeps names per account', () => {
|
||||
rememberPrincipals('acc-1', [person('ck')]);
|
||||
rememberPrincipals('acc-2', [person('cj')]);
|
||||
expect([...rememberedPrincipals('acc-1').keys()]).toEqual(['ck']);
|
||||
expect([...rememberedPrincipals('acc-2').keys()]).toEqual(['cj']);
|
||||
});
|
||||
|
||||
it('forgets one account and leaves the others', () => {
|
||||
rememberPrincipals('acc-1', [person('ck')]);
|
||||
rememberPrincipals('acc-2', [person('cj')]);
|
||||
forgetPrincipals('acc-1');
|
||||
expect(rememberedPrincipals('acc-1').size).toBe(0);
|
||||
expect([...rememberedPrincipals('acc-2').keys()]).toEqual(['cj']);
|
||||
});
|
||||
|
||||
it('leaves no key behind when the last account goes', () => {
|
||||
rememberPrincipals('acc-1', [person('ck')]);
|
||||
forgetPrincipals('acc-1');
|
||||
expect(localStorage.getItem('bulwark-delegate-names')).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps at most 200 per account, dropping the oldest', () => {
|
||||
rememberPrincipals('acc-1', Array.from({ length: 205 }, (_, i) => person(`p${i}`)));
|
||||
const ids = [...rememberedPrincipals('acc-1').keys()];
|
||||
expect(ids).toHaveLength(200);
|
||||
expect(ids[0]).toBe('p5');
|
||||
});
|
||||
});
|
||||
@@ -3,14 +3,17 @@
|
||||
* browser. Stalwart 1.0 with directory queries off (its default) won't name
|
||||
* a principal the user shares with unless that principal also shares
|
||||
* something back, so the "Who has access" list would otherwise show bare
|
||||
* ids for the very people the user just picked by name. Per account, at
|
||||
* most MAX entries, and only a convenience: losing it costs names, not
|
||||
* access.
|
||||
* ids for the very people the user just picked by name. Only the people
|
||||
* who have access and the one just picked are kept, never the directory;
|
||||
* per account, at most MAX entries, gone when the account is removed or
|
||||
* everyone signs out. A convenience: losing it costs names, not access.
|
||||
*/
|
||||
|
||||
import type { Principal } from '@/lib/jmap/types';
|
||||
|
||||
const STORAGE_KEY = 'bulwark-delegate-names';
|
||||
/** Purged when everyone signs out (lib/sign-out-cleanup.ts). */
|
||||
export const DELEGATE_NAMES_KEY = 'bulwark-delegate-names';
|
||||
const STORAGE_KEY = DELEGATE_NAMES_KEY;
|
||||
const MAX = 200;
|
||||
|
||||
type Entry = Pick<Principal, 'id' | 'name' | 'email' | 'type'>;
|
||||
@@ -50,3 +53,17 @@ export function rememberedPrincipals(accountKey: string): Map<string, Principal>
|
||||
const own = readStore()[accountKey] ?? {};
|
||||
return new Map(Object.values(own).map((entry) => [entry.id, entry as Principal]));
|
||||
}
|
||||
|
||||
/** Forget the names kept for one account (it was removed from this browser). */
|
||||
export function forgetPrincipals(accountKey: string): void {
|
||||
if (!accountKey) return;
|
||||
try {
|
||||
const store = readStore();
|
||||
if (!(accountKey in store)) return;
|
||||
delete store[accountKey];
|
||||
if (Object.keys(store).length === 0) localStorage.removeItem(STORAGE_KEY);
|
||||
else localStorage.setItem(STORAGE_KEY, JSON.stringify(store));
|
||||
} catch {
|
||||
// storage unavailable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ const SIGNED_OUT_STORAGE_KEYS = [
|
||||
'files-recent-files',
|
||||
'files-last-parent-id',
|
||||
'files-path-stack',
|
||||
// Names of the people the user gave access to (Settings > Sharing).
|
||||
'bulwark-delegate-names',
|
||||
];
|
||||
|
||||
const SIGNED_OUT_SIGNAL = 'bulwark:signed-out';
|
||||
|
||||
@@ -59,6 +59,15 @@ describe('removing a login that is not the active one', () => {
|
||||
useEmailStore.setState(initialEmail);
|
||||
});
|
||||
|
||||
it("forgets the names of the people it gave access to, and keeps the other login's", () => {
|
||||
localStorage.setItem('bulwark-delegate-names', JSON.stringify({
|
||||
[a]: { ck: { id: 'ck', name: 'carol@example.org', email: 'carol@example.org', type: 'individual' } },
|
||||
[b]: { cj: { id: 'cj', name: 'bob@example.org', email: 'bob@example.org', type: 'individual' } },
|
||||
}));
|
||||
useAuthStore.getState().removeAccount(b);
|
||||
expect(Object.keys(JSON.parse(localStorage.getItem('bulwark-delegate-names')!))).toEqual([a]);
|
||||
});
|
||||
|
||||
it('leaves its folder open in the Pro sidebar, so deleting the rows shown deletes nothing in the active login', async () => {
|
||||
const row = message();
|
||||
useEmailStore.setState({
|
||||
|
||||
@@ -20,6 +20,7 @@ import { isEmbedded, notifyParent } from '@/lib/iframe-bridge';
|
||||
import { snapshotAccount, restoreAccount, clearAllStores, evictAccount, evictAll, forgetAccountRows } from '@/lib/account-state-manager';
|
||||
import { clearAllPluginStorage, clearPluginStorageForAccount } from '@/lib/plugin-sandbox/storage-scope';
|
||||
import { broadcastSignOut, onSignedOutElsewhere, purgeSignedOutData } from '@/lib/sign-out-cleanup';
|
||||
import { forgetPrincipals } from '@/lib/delegation/known-principals';
|
||||
import { useSearchHistoryStore } from './search-history-store';
|
||||
import { useCalendarNotificationStore } from './calendar-notification-store';
|
||||
import { usePolicyStore } from './policy-store';
|
||||
@@ -2230,6 +2231,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
clients.delete(accountId);
|
||||
evictAccount(accountId);
|
||||
accountStore.removeAccount(accountId);
|
||||
forgetPrincipals(accountId);
|
||||
if (!wasDemoMode) clearPluginStorageForAccount(accountId);
|
||||
}
|
||||
|
||||
@@ -2368,6 +2370,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
evictAccount(accountId);
|
||||
forgetAccountRows(accountId, get().client);
|
||||
accountStore.removeAccount(accountId);
|
||||
forgetPrincipals(accountId);
|
||||
clearPluginStorageForAccount(accountId);
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user