fix(auth): scope clearAllAppData to active user; fix re-onboarding race; drop dead API call (#1816)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
YellowSnnowmann
2026-05-15 14:57:30 -07:00
committed by GitHub
co-authored by Claude Sonnet 4.6 Steven Enamakel
parent 05451f5c33
commit b4c19b105b
6 changed files with 189 additions and 24 deletions
@@ -23,31 +23,50 @@ describe('clearAllAppData', () => {
mockReset.mockReset().mockResolvedValue(undefined);
mockRestart.mockReset().mockResolvedValue(undefined);
mockPurgeCef.mockReset().mockResolvedValue(undefined);
window.localStorage.setItem('persisted', '1');
window.sessionStorage.setItem('session-persisted', '1');
window.localStorage.clear();
window.sessionStorage.clear();
});
it('runs the full wipe sequence and restarts the app', async () => {
const clearSession = vi.fn().mockResolvedValue(undefined);
// Seed active-user key + user-1-scoped data + another user's data
window.localStorage.setItem('OPENHUMAN_ACTIVE_USER_ID', 'user-1');
window.localStorage.setItem('user-1:persist:accounts', 'a');
window.localStorage.setItem('user-1:persist:coreMode', 'b');
window.localStorage.setItem('user-2:persist:accounts', 'other');
window.sessionStorage.setItem('session-persisted', '1');
await clearAllAppData({ clearSession, userId: 'user-1' });
expect(mockPurgeCef).toHaveBeenCalledWith('user-1');
expect(clearSession).toHaveBeenCalledTimes(1);
expect(mockReset).toHaveBeenCalledTimes(1);
expect(mockPurge).toHaveBeenCalledTimes(1);
expect(window.localStorage.getItem('persisted')).toBeNull();
// user-1's own keys are gone
expect(window.localStorage.getItem('OPENHUMAN_ACTIVE_USER_ID')).toBeNull();
expect(window.localStorage.getItem('user-1:persist:accounts')).toBeNull();
expect(window.localStorage.getItem('user-1:persist:coreMode')).toBeNull();
// user-2's keys are untouched (#983: other accounts must not lose data)
expect(window.localStorage.getItem('user-2:persist:accounts')).toBe('other');
expect(window.sessionStorage.getItem('session-persisted')).toBeNull();
expect(mockRestart).toHaveBeenCalledTimes(1);
});
it('defaults to a null user scope when no userId is provided', async () => {
it('falls back to localStorage.clear() when no userId is provided', async () => {
window.localStorage.setItem('user-1:persist:accounts', 'a');
window.localStorage.setItem('user-2:persist:accounts', 'b');
window.sessionStorage.setItem('session-persisted', '1');
await clearAllAppData();
expect(mockPurgeCef).toHaveBeenCalledWith(null);
// No clearSession was provided — call sequence still completes.
expect(mockReset).toHaveBeenCalledTimes(1);
expect(mockRestart).toHaveBeenCalledTimes(1);
// Without a userId we have no way to scope, so everything is cleared
expect(window.localStorage.getItem('user-1:persist:accounts')).toBeNull();
expect(window.localStorage.getItem('user-2:persist:accounts')).toBeNull();
});
it('continues if scheduleCefProfilePurge fails (best-effort)', async () => {
+50 -4
View File
@@ -5,6 +5,53 @@ import {
scheduleCefProfilePurge,
} from './tauriCommands';
const ACTIVE_USER_KEY = 'OPENHUMAN_ACTIVE_USER_ID';
/**
* Selectively purge localStorage keys belonging to a single user.
*
* Removes:
* - `${userId}:persist:*` — per-user Redux-persist blobs
* - `${userId}:*` — any other user-scoped keys
* - `OPENHUMAN_ACTIVE_USER_ID` — the boot-time user seed (only when a userId
* is supplied so we don't wipe it on pre-login
* recovery where userId is null)
*
* Intentionally leaves other users' scoped keys untouched so that
* "clear my data" on account B does not silently destroy account A's
* persisted state (#983).
*/
function clearUserScopedStorage(userId: string | null): void {
try {
if (userId) {
const prefix = `${userId}:`;
const toRemove: string[] = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(prefix)) {
toRemove.push(key);
}
}
for (const key of toRemove) {
localStorage.removeItem(key);
}
localStorage.removeItem(ACTIVE_USER_KEY);
} else {
// No known user (pre-login recovery) — fall back to clearing everything
// so we don't leave orphaned blobs with no way to scope the deletion.
localStorage.clear();
}
} catch (err) {
console.warn('[clearAllAppData] storage clear failed:', err);
} finally {
try {
sessionStorage.clear();
} catch {
// best-effort
}
}
}
export interface ClearAllAppDataOptions {
// Optional core-side session clear (e.g. `auth_clear_session`). Best-effort —
// skipped silently if the caller cannot/does not provide it (e.g. pre-login
@@ -59,11 +106,10 @@ export const clearAllAppData = async ({
await resetOpenHumanDataAndRestartCore();
// 4. Purge redux-persist + browser storage. `persistor.purge()` wipes the
// persisted backend; localStorage/sessionStorage clear everything else
// (auth flags, theme, etc.).
// persisted backend; `clearUserScopedStorage` removes only the active
// user's localStorage keys so other accounts' data is not destroyed.
await persistor.purge();
window.localStorage.clear();
window.sessionStorage.clear();
clearUserScopedStorage(userId);
// 5. Full app restart so CEF reboots into the fresh pre-login profile.
await restartApp();