mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-29 22:23:01 +00:00
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:
co-authored by
Claude Sonnet 4.6
Steven Enamakel
parent
05451f5c33
commit
b4c19b105b
@@ -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 () => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user