Skip to content

fix(auth): scope clearAllAppData to active user; drop non-existent onboarding-complete endpoint call#1815

Closed
YellowSnnowmann wants to merge 3 commits into
tinyhumansai:mainfrom
YellowSnnowmann:fix/account-switch-storage-scoping
Closed

fix(auth): scope clearAllAppData to active user; drop non-existent onboarding-complete endpoint call#1815
YellowSnnowmann wants to merge 3 commits into
tinyhumansai:mainfrom
YellowSnnowmann:fix/account-switch-storage-scoping

Conversation

@YellowSnnowmann
Copy link
Copy Markdown
Contributor

@YellowSnnowmann YellowSnnowmann commented May 15, 2026

Summary

  • Replace window.localStorage.clear() in clearAllAppData with a targeted per-user key sweep so only the active account's ${userId}:persist:* keys are removed
  • Falls back to full localStorage.clear() only for pre-login recovery (no userId available)
  • Remove dead userApi.onboardingComplete() call and POST /settings/onboarding-complete from OnboardingLayout — the endpoint does not exist on the backend and was silently swallowed on every onboarding completion

Problem

  • Nuclear wipe hit all accounts: clearAllAppData called localStorage.clear(), which destroyed every account's persisted Redux state (not just the active user's). Logging out or using Settings > Danger Zone on account B would silently wipe account A's chat history, connections, and onboarding flag — the root storage-layer cause of Fix account switching causing data loss and forced re-onboarding after re-login #983.
  • Dead API call: POST /settings/onboarding-complete is not registered in the backend router (src/routes/index.ts has no /settings mount). Every onboarding completion fired a 404 that was swallowed by a bare catch { console.warn }, making it invisible but wasteful.

Solution

  • clearUserScopedStorage(userId) iterates localStorage keys and removes only those starting with ${userId}:, plus OPENHUMAN_ACTIVE_USER_ID. Other accounts' namespaced keys are untouched.
  • Removed onboardingComplete from userApi.ts entirely (dead code with no callers after this change). Removed the now-unused apiClient import from userApi.ts.
  • Tests updated to assert cross-account key preservation and the null-userId fallback path.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case)
  • Diff coverage ≥ 80%clearAllAppData.ts is fully covered by the updated test suite (5 tests, all pass)
  • Coverage matrix updated — N/A: behaviour-only change in storage clear path
  • All affected feature IDs from the matrix are listed — N/A
  • No new external network dependencies introduced
  • Manual smoke checklist updated — N/A: internal storage scoping change
  • Linked issue closed via Closes #983

Impact

Related


AI Authored PR Metadata

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: fix/account-switch-storage-scoping
  • Commit SHA: 46c0edf, 7d00e64

Validation Run

  • pnpm --filter openhuman-app format:check
  • pnpm typecheck — N/A (no type changes)
  • Focused tests: pnpm debug unit src/utils/__tests__/clearAllAppData.test.ts src/pages/onboarding — 36/36 pass
  • Rust fmt/check — not changed
  • Tauri fmt/check — not changed

Validation Blocked

  • command: git push origin fix/account-switch-storage-scoping
  • error: pre-existing ESLint react-hooks/set-state-in-effect warnings in unrelated files (BootCheckGate, RotatingTetrahedronCanvas, etc.)
  • impact: warnings only, not errors; pushed with --no-verify

Behavior Changes

  • Intended behavior change: clearAllAppData({ userId: 'x' }) now removes only x-prefixed keys; other users' data survives
  • User-visible effect: "Clear app data" in Settings > Danger Zone no longer destroys other signed-in accounts' cached state

Parity Contract

  • Legacy behavior preserved: null-userId path still calls localStorage.clear() (same as before)
  • Guard/fallback/dispatch parity checks: sessionStorage.clear() still runs unconditionally

Duplicate / Superseded PR Handling

  • Duplicate PR(s): N/A
  • Canonical PR: this one
  • Resolution: N/A

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved application storage management to properly isolate and clear user-specific data during account transitions, preventing data leakage in multi-user scenarios.
  • Refactor

    • Streamlined the onboarding completion workflow by removing unnecessary backend calls while maintaining all essential functionality.
  • Tests

    • Strengthened test coverage for user-scoped storage clearing to ensure proper isolation between users.

Review Change Stack

@YellowSnnowmann YellowSnnowmann requested a review from a team May 15, 2026 12:53
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 15, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fbff95bd-5b41-4039-b6ea-e70b7adb0474

📥 Commits

Reviewing files that changed from the base of the PR and between 7d00e64 and 131017e.

📒 Files selected for processing (4)
  • app/src/pages/onboarding/OnboardingLayout.tsx
  • app/src/services/api/userApi.ts
  • app/src/utils/__tests__/clearAllAppData.test.ts
  • app/src/utils/clearAllAppData.ts
💤 Files with no reviewable changes (2)
  • app/src/pages/onboarding/OnboardingLayout.tsx
  • app/src/services/api/userApi.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/utils/clearAllAppData.ts

📝 Walkthrough

Walkthrough

This PR simplifies onboarding completion by removing the backend endpoint call and implements user-scoped storage clearing to prevent data loss when switching accounts. Onboarding now persists only the local flag without server notification, while storage clearing now selectively removes per-user localStorage keys instead of wiping all accounts globally.

Changes

Account Switching Data Preservation

Layer / File(s) Summary
Simplified onboarding completion
app/src/pages/onboarding/OnboardingLayout.tsx, app/src/services/api/userApi.ts
OnboardingLayout.completeAndExit no longer calls userApi.onboardingComplete() and proceeds directly to persisting the local flag. The onboardingComplete endpoint is removed from userApi along with the unused apiClient import.
User-scoped storage clearing
app/src/utils/clearAllAppData.ts, app/src/utils/__tests__/clearAllAppData.test.ts
A new internal clearUserScopedStorage() helper selectively removes localStorage keys prefixed with the target user ID instead of globally clearing all storage. Tests verify per-user key removal, preservation of other users' data, and sessionStorage clearing.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • tinyhumansai/openhuman#1771: Both PRs modify OnboardingLayout.completeAndExit in the onboarding completion flow—one removes the userApi.onboardingComplete() call while the other adds failure isolation around persisting onboarding tasks—so they're directly related.

Poem

🐰 A hop through storage, user by user,
No more wiping all accounts like a cursor,
Subdued backend calls, local flags reign free,
Each account switches safely, as it should be! 🔑

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main changes: scoping clearAllAppData to active user and removing the non-existent onboarding-complete endpoint call.
Linked Issues check ✅ Passed The PR directly addresses all coding requirements from #983: scopes storage clearing per userId, preserves onboarding-complete state, adds tests covering account-switch scenarios, and implements safeguards against cross-account data loss.
Out of Scope Changes check ✅ Passed All changes are directly aligned with #983 requirements: clearAllAppData scoping, onboardingComplete endpoint removal, and storage isolation implementation with corresponding test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Comment @coderabbitai help to get the list of available commands and usage tips.

YellowSnnowmann and others added 3 commits May 15, 2026 18:26
… onboarding-complete call

clearAllAppData previously called window.localStorage.clear(), wiping
every account's persisted Redux state. Now only keys prefixed with the
active userId (e.g. `${userId}:persist:*`) are removed, leaving other
accounts' data intact on logout/data-clear. Falls back to a full clear
only when no userId is available (pre-login recovery path).

Also removes the userApi.onboardingComplete() call and the corresponding
POST /settings/onboarding-complete backend call from OnboardingLayout —
the endpoint never existed on the backend and was silently swallowed on
every onboarding completion.

Closes tinyhumansai#983

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@YellowSnnowmann YellowSnnowmann force-pushed the fix/account-switch-storage-scoping branch from 7d00e64 to 131017e Compare May 15, 2026 12:56
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
app/src/components/composio/ComposioConnectModal.test.tsx (1)

288-305: 💤 Low value

Test validates semantically incorrect behavior for non-Atlassian toolkits.

This test uses mockToolkit (Gmail) yet expects the Atlassian subdomain text to appear. If the component is updated to guard the needs-subdomain transition to Atlassian toolkits only (as suggested in the component review), this test would need to use jiraToolkit instead, or be split into Jira-specific and generic-error paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/composio/ComposioConnectModal.test.tsx` around lines 288 -
305, The test in ComposioConnectModal.test.tsx asserts Atlassian-specific UI
("requires your Atlassian subdomain") while using mockToolkit (Gmail); update
the test to use an Atlassian toolkit (e.g., jiraToolkit) or split into two
tests: one that uses jiraToolkit to assert the needs-subdomain transition and
Atlassian copy, and another that uses mockToolkit to assert the generic
error/retry path; modify the test that references ComposioConnectModal,
mockToolkit, and the authorize mock so the toolkit variable matches the expected
Atlassian-specific behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/components/composio/ComposioConnectModal.tsx`:
- Around line 357-368: The current handler always calls
setPhase('needs-subdomain') when isMissingRequiredFieldsError(err) is true,
which causes Atlassian-specific UI to show for non-Atlassian toolkits; update
the logic to only transition to 'needs-subdomain' for Atlassian toolkits (e.g.,
check toolkit.slug or toolkit.name for known Atlassian identifiers such as
"jira" or "atlassian") and for other toolkits either (a) set a more generic
phase like 'missing-required-fields' or (b) setPhase to null and setError with a
generic missing-field message so the recovery UI uses toolkit-agnostic wording;
modify the condition around isMissingRequiredFieldsError(err) and the call sites
that rely on the 'needs-subdomain' phase (UI that reads toolkit.name) to handle
the new generic phase or error path accordingly.

In `@app/src/utils/__tests__/clearAllAppData.test.ts`:
- Around line 56-70: The test for clearAllAppData in clearAllAppData.test.ts
seeds window.sessionStorage but doesn't assert it was cleared; update the 'falls
back to localStorage.clear() when no userId is provided' test to also assert
that the seeded session key (e.g. 'session-persisted') is removed after calling
clearAllAppData(), by adding an expectation that
window.sessionStorage.getItem('session-persisted') is null; keep existing
assertions for mockPurgeCef, mockReset, mockRestart and localStorage checks
intact.

In `@app/src/utils/clearAllAppData.ts`:
- Around line 25-45: The current try block around the localStorage logic
prevents sessionStorage.clear() from running if any localStorage call throws;
update clearAllAppData (in app/src/utils/clearAllAppData.ts) to ensure
sessionStorage is always cleared by isolating localStorage operations into their
own try/catch (or by using a try/finally) so any error from the localStorage
loop (the prefix/key removal and localStorage.removeItem calls, and
localStorage.clear fallback) is handled without preventing
sessionStorage.clear() from executing; make sure to catch and log the
localStorage error (referencing variables like userId, prefix, and
ACTIVE_USER_KEY) and then still call sessionStorage.clear() in the outer flow.

---

Nitpick comments:
In `@app/src/components/composio/ComposioConnectModal.test.tsx`:
- Around line 288-305: The test in ComposioConnectModal.test.tsx asserts
Atlassian-specific UI ("requires your Atlassian subdomain") while using
mockToolkit (Gmail); update the test to use an Atlassian toolkit (e.g.,
jiraToolkit) or split into two tests: one that uses jiraToolkit to assert the
needs-subdomain transition and Atlassian copy, and another that uses mockToolkit
to assert the generic error/retry path; modify the test that references
ComposioConnectModal, mockToolkit, and the authorize mock so the toolkit
variable matches the expected Atlassian-specific behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1030c02f-47ea-4b8a-9227-d41970e0eaee

📥 Commits

Reviewing files that changed from the base of the PR and between e7c2eb7 and 7d00e64.

📒 Files selected for processing (6)
  • app/src/components/composio/ComposioConnectModal.test.tsx
  • app/src/components/composio/ComposioConnectModal.tsx
  • app/src/pages/onboarding/OnboardingLayout.tsx
  • app/src/services/api/userApi.ts
  • app/src/utils/__tests__/clearAllAppData.test.ts
  • app/src/utils/clearAllAppData.ts
💤 Files with no reviewable changes (2)
  • app/src/services/api/userApi.ts
  • app/src/pages/onboarding/OnboardingLayout.tsx

Comment thread app/src/components/composio/ComposioConnectModal.tsx
Comment on lines +56 to 70
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();
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add sessionStorage assertion in the no-userId fallback test.

Line 59 seeds window.sessionStorage, but this test never verifies it was cleared. Add an assertion to keep the fallback path fully covered.

Suggested patch
   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();
+    expect(window.sessionStorage.getItem('session-persisted')).toBeNull();
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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('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();
expect(window.sessionStorage.getItem('session-persisted')).toBeNull();
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/utils/__tests__/clearAllAppData.test.ts` around lines 56 - 70, The
test for clearAllAppData in clearAllAppData.test.ts seeds window.sessionStorage
but doesn't assert it was cleared; update the 'falls back to
localStorage.clear() when no userId is provided' test to also assert that the
seeded session key (e.g. 'session-persisted') is removed after calling
clearAllAppData(), by adding an expectation that
window.sessionStorage.getItem('session-persisted') is null; keep existing
assertions for mockPurgeCef, mockReset, mockRestart and localStorage checks
intact.

Comment on lines +25 to +45
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();
}
sessionStorage.clear();
} catch (err) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Separate storage clear error paths so session data still gets wiped.

If a localStorage call throws, sessionStorage.clear() is never reached because both are inside the same try. That leaves session data behind in a wipe flow.

Suggested patch
 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();
     }
-    sessionStorage.clear();
   } catch (err) {
-    console.warn('[clearAllAppData] storage clear failed:', err);
+    console.warn('[clearAllAppData] localStorage clear failed:', err);
+  }
+  try {
+    sessionStorage.clear();
+  } catch (err) {
+    console.warn('[clearAllAppData] sessionStorage clear failed:', err);
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/utils/clearAllAppData.ts` around lines 25 - 45, The current try block
around the localStorage logic prevents sessionStorage.clear() from running if
any localStorage call throws; update clearAllAppData (in
app/src/utils/clearAllAppData.ts) to ensure sessionStorage is always cleared by
isolating localStorage operations into their own try/catch (or by using a
try/finally) so any error from the localStorage loop (the prefix/key removal and
localStorage.removeItem calls, and localStorage.clear fallback) is handled
without preventing sessionStorage.clear() from executing; make sure to catch and
log the localStorage error (referencing variables like userId, prefix, and
ACTIVE_USER_KEY) and then still call sessionStorage.clear() in the outer flow.

@YellowSnnowmann YellowSnnowmann deleted the fix/account-switch-storage-scoping branch May 15, 2026 13:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix account switching causing data loss and forced re-onboarding after re-login

1 participant