|
| 1 | +From 6e88d05061d78b85121c3fc920570d8e7becf404 Mon Sep 17 00:00:00 2001 |
| 2 | +From: Riajul Islam <riajul@kahf.co> |
| 3 | +Date: Tue, 21 Apr 2026 15:11:38 +0000 |
| 4 | +Subject: [PATCH] fix: scope org selection persistence per authenticated user |
| 5 | + |
| 6 | +--- |
| 7 | + .../shared/hooks/useOrganizationSelection.ts | 13 ++++++++++- |
| 8 | + .../src/shared/stores/useOrganizationStore.ts | 22 ++++++++++++++++++- |
| 9 | + 2 files changed, 33 insertions(+), 2 deletions(-) |
| 10 | + |
| 11 | +diff --git a/packages/web-core/src/shared/hooks/useOrganizationSelection.ts b/packages/web-core/src/shared/hooks/useOrganizationSelection.ts |
| 12 | +index 9e0b5274f..eaed39adb 100644 |
| 13 | +--- a/packages/web-core/src/shared/hooks/useOrganizationSelection.ts |
| 14 | ++++ b/packages/web-core/src/shared/hooks/useOrganizationSelection.ts |
| 15 | +@@ -3,7 +3,11 @@ import type { |
| 16 | + OrganizationWithRole, |
| 17 | + ListOrganizationsResponse, |
| 18 | + } from 'shared/types'; |
| 19 | +-import { useOrganizationStore } from '@/shared/stores/useOrganizationStore'; |
| 20 | ++import { useAuth } from '@/shared/hooks/auth/useAuth'; |
| 21 | ++import { |
| 22 | ++ hydrateOrganizationSelectionForUser, |
| 23 | ++ useOrganizationStore, |
| 24 | ++} from '@/shared/stores/useOrganizationStore'; |
| 25 | + |
| 26 | + interface UseOrganizationSelectionOptions { |
| 27 | + organizations: ListOrganizationsResponse | undefined; |
| 28 | +@@ -13,6 +17,7 @@ interface UseOrganizationSelectionOptions { |
| 29 | + export function useOrganizationSelection( |
| 30 | + options: UseOrganizationSelectionOptions |
| 31 | + ) { |
| 32 | ++ const { isLoaded, userId } = useAuth(); |
| 33 | + const { organizations, onSelectionChange } = options; |
| 34 | + const selectedOrgId = useOrganizationStore((s) => s.selectedOrgId); |
| 35 | + const setSelectedOrgId = useOrganizationStore((s) => s.setSelectedOrgId); |
| 36 | +@@ -22,6 +27,12 @@ export function useOrganizationSelection( |
| 37 | + [organizations] |
| 38 | + ); |
| 39 | + |
| 40 | ++ // Scope persisted selection by signed-in user to avoid cross-user leakage. |
| 41 | ++ useEffect(() => { |
| 42 | ++ if (!isLoaded) return; |
| 43 | ++ hydrateOrganizationSelectionForUser(userId); |
| 44 | ++ }, [isLoaded, userId]); |
| 45 | ++ |
| 46 | + // Default to first available organization if none selected or selection is invalid |
| 47 | + useEffect(() => { |
| 48 | + if (orgList.length === 0) return; |
| 49 | +diff --git a/packages/web-core/src/shared/stores/useOrganizationStore.ts b/packages/web-core/src/shared/stores/useOrganizationStore.ts |
| 50 | +index 893a242de..f537a96ea 100644 |
| 51 | +--- a/packages/web-core/src/shared/stores/useOrganizationStore.ts |
| 52 | ++++ b/packages/web-core/src/shared/stores/useOrganizationStore.ts |
| 53 | +@@ -8,6 +8,11 @@ type State = { |
| 54 | + clearSelectedOrgId: () => void; |
| 55 | + }; |
| 56 | + |
| 57 | ++const ORGANIZATION_SELECTION_KEY_PREFIX = 'organization-selection'; |
| 58 | ++const SIGNED_OUT_SCOPE = 'signed-out'; |
| 59 | ++ |
| 60 | ++let activePersistKey: string | null = null; |
| 61 | ++ |
| 62 | + export const useOrganizationStore = create<State>()( |
| 63 | + persist( |
| 64 | + (set) => ({ |
| 65 | +@@ -16,12 +21,27 @@ export const useOrganizationStore = create<State>()( |
| 66 | + clearSelectedOrgId: () => set({ selectedOrgId: null }), |
| 67 | + }), |
| 68 | + { |
| 69 | +- name: 'organization-selection', |
| 70 | ++ name: ORGANIZATION_SELECTION_KEY_PREFIX, |
| 71 | + partialize: (state) => ({ selectedOrgId: state.selectedOrgId }), |
| 72 | ++ skipHydration: true, |
| 73 | + } |
| 74 | + ) |
| 75 | + ); |
| 76 | + |
| 77 | ++export function hydrateOrganizationSelectionForUser(userId: string | null) { |
| 78 | ++ const userScope = userId ?? SIGNED_OUT_SCOPE; |
| 79 | ++ const nextPersistKey = `${ORGANIZATION_SELECTION_KEY_PREFIX}:${userScope}`; |
| 80 | ++ |
| 81 | ++ if (activePersistKey === nextPersistKey) { |
| 82 | ++ return; |
| 83 | ++ } |
| 84 | ++ |
| 85 | ++ activePersistKey = nextPersistKey; |
| 86 | ++ useOrganizationStore.persist.setOptions({ name: nextPersistKey }); |
| 87 | ++ useOrganizationStore.setState({ selectedOrgId: null }); |
| 88 | ++ void useOrganizationStore.persist.rehydrate(); |
| 89 | ++} |
| 90 | ++ |
| 91 | + // Sync org store changes into the UI preferences store for server persistence |
| 92 | + useOrganizationStore.subscribe((state) => { |
| 93 | + useUiPreferencesStore.getState().setSelectedOrgId(state.selectedOrgId); |
| 94 | +-- |
| 95 | +2.50.1 |
| 96 | + |
0 commit comments