Improve enterprise tier child tenants access
This commit is contained in:
@@ -7,21 +7,25 @@ import { TENANT_ROLES, GLOBAL_USER_ROLES } from '../types/roles';
|
||||
export interface TenantState {
|
||||
// State
|
||||
currentTenant: TenantResponse | null;
|
||||
parentTenant: TenantResponse | null; // Enterprise parent tenant for child premise navigation
|
||||
availableTenants: TenantResponse[] | null;
|
||||
currentTenantAccess: TenantAccessResponse | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
|
||||
|
||||
// Actions
|
||||
setCurrentTenant: (tenant: TenantResponse) => void;
|
||||
setAvailableTenants: (tenants: TenantResponse[]) => void;
|
||||
switchTenant: (tenantId: string) => Promise<boolean>;
|
||||
switchToChildTenant: (childTenant: TenantResponse) => Promise<boolean>; // Switch to child while remembering parent
|
||||
restoreParentTenant: () => Promise<boolean>; // Restore parent tenant context
|
||||
loadUserTenants: () => Promise<void>;
|
||||
loadChildTenants: () => Promise<void>; // Load child tenants for enterprise users
|
||||
loadCurrentTenantAccess: () => Promise<void>;
|
||||
clearTenants: () => void;
|
||||
clearError: () => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
|
||||
|
||||
// Permission helpers (migrated from BakeryContext)
|
||||
hasPermission: (permission: string) => boolean;
|
||||
canAccess: (resource: string, action: string) => boolean;
|
||||
@@ -32,6 +36,7 @@ export const useTenantStore = create<TenantState>()(
|
||||
(set, get) => ({
|
||||
// Initial state
|
||||
currentTenant: null,
|
||||
parentTenant: null,
|
||||
availableTenants: null,
|
||||
currentTenantAccess: null,
|
||||
isLoading: false,
|
||||
@@ -55,15 +60,15 @@ export const useTenantStore = create<TenantState>()(
|
||||
switchTenant: async (tenantId: string): Promise<boolean> => {
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
|
||||
const { availableTenants } = get();
|
||||
|
||||
|
||||
// Find tenant in available tenants
|
||||
const targetTenant = availableTenants?.find(t => t.id === tenantId);
|
||||
if (!targetTenant) {
|
||||
throw new Error('Tenant not found in available tenants');
|
||||
}
|
||||
|
||||
|
||||
// Switch tenant (frontend-only operation)
|
||||
get().setCurrentTenant(targetTenant);
|
||||
set({ isLoading: false });
|
||||
@@ -77,6 +82,111 @@ export const useTenantStore = create<TenantState>()(
|
||||
}
|
||||
},
|
||||
|
||||
switchToChildTenant: async (childTenant: TenantResponse): Promise<boolean> => {
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
const { currentTenant, availableTenants } = get();
|
||||
|
||||
console.log('[Tenant Store] Switching to child tenant:', {
|
||||
from: currentTenant?.id,
|
||||
fromName: currentTenant?.name,
|
||||
to: childTenant.id,
|
||||
toName: childTenant.name
|
||||
});
|
||||
|
||||
// Store current tenant as parent (for enterprise users navigating to child premises)
|
||||
if (currentTenant) {
|
||||
set({ parentTenant: currentTenant });
|
||||
console.log('[Tenant Store] Stored parent tenant:', currentTenant.id);
|
||||
}
|
||||
|
||||
// Add child to availableTenants if not already present
|
||||
if (availableTenants && !availableTenants.find(t => t.id === childTenant.id)) {
|
||||
set({ availableTenants: [...availableTenants, childTenant] });
|
||||
}
|
||||
|
||||
// CRITICAL: Directly update API client BEFORE updating state
|
||||
console.log('[Tenant Store] Directly updating API client with child tenant ID:', childTenant.id);
|
||||
const { apiClient } = await import('../api/client');
|
||||
apiClient.setTenantId(childTenant.id);
|
||||
|
||||
// Verify the API client was updated
|
||||
const verifiedTenantId = apiClient.getTenantId();
|
||||
console.log('[Tenant Store] Verified API client tenant ID:', verifiedTenantId);
|
||||
|
||||
if (verifiedTenantId !== childTenant.id) {
|
||||
console.error('[Tenant Store] API client tenant ID mismatch! Expected:', childTenant.id, 'Got:', verifiedTenantId);
|
||||
set({ isLoading: false, error: 'Failed to update API client tenant ID' });
|
||||
return false;
|
||||
}
|
||||
|
||||
// Now update the store state
|
||||
// IMPORTANT: We've already updated the API client above, so we just update the state
|
||||
// We DON'T call setCurrentTenant action because that would trigger tenantService.setCurrentTenant
|
||||
// which would call apiClient.setTenantId again
|
||||
set({ currentTenant: childTenant, currentTenantAccess: null });
|
||||
|
||||
console.log('[Tenant Store] Switch complete. Current tenant is now:', get().currentTenant?.id);
|
||||
console.log('[Tenant Store] Final API client tenant ID verification:', apiClient.getTenantId());
|
||||
|
||||
set({ isLoading: false });
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[Tenant Store] Failed to switch to child tenant:', error);
|
||||
set({
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to switch to child tenant',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
restoreParentTenant: async (): Promise<boolean> => {
|
||||
try {
|
||||
const { parentTenant } = get();
|
||||
|
||||
if (!parentTenant) {
|
||||
console.warn('[Tenant Store] No parent tenant to restore');
|
||||
return false;
|
||||
}
|
||||
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
console.log('[Tenant Store] Restoring parent tenant:', parentTenant.id, parentTenant.name);
|
||||
|
||||
// CRITICAL: Directly update API client BEFORE updating state
|
||||
const { apiClient } = await import('../api/client');
|
||||
apiClient.setTenantId(parentTenant.id);
|
||||
|
||||
// Verify the API client was updated
|
||||
const verifiedTenantId = apiClient.getTenantId();
|
||||
console.log('[Tenant Store] Verified API client tenant ID after restore:', verifiedTenantId);
|
||||
|
||||
if (verifiedTenantId !== parentTenant.id) {
|
||||
console.error('[Tenant Store] API client tenant ID mismatch! Expected:', parentTenant.id, 'Got:', verifiedTenantId);
|
||||
set({ isLoading: false, error: 'Failed to update API client tenant ID' });
|
||||
return false;
|
||||
}
|
||||
|
||||
// Now update the store state
|
||||
set({ currentTenant: parentTenant, currentTenantAccess: null });
|
||||
|
||||
// Clear parent tenant reference
|
||||
set({ parentTenant: null, isLoading: false });
|
||||
|
||||
console.log('[Tenant Store] Parent tenant restored successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[Tenant Store] Failed to restore parent tenant:', error);
|
||||
set({
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to restore parent tenant',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
loadUserTenants: async (): Promise<void> => {
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
@@ -129,6 +239,35 @@ export const useTenantStore = create<TenantState>()(
|
||||
}
|
||||
},
|
||||
|
||||
loadChildTenants: async (): Promise<void> => {
|
||||
try {
|
||||
const { currentTenant, availableTenants } = get();
|
||||
|
||||
if (!currentTenant) {
|
||||
console.warn('No current tenant to load children for');
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch child tenants
|
||||
const children = await tenantService.getChildTenants(currentTenant.id);
|
||||
|
||||
if (children && children.length > 0) {
|
||||
// Add child tenants to availableTenants if not already present
|
||||
const currentAvailable = availableTenants || [];
|
||||
const newTenants = children.filter(
|
||||
child => !currentAvailable.find(t => t.id === child.id)
|
||||
);
|
||||
|
||||
if (newTenants.length > 0) {
|
||||
set({ availableTenants: [...currentAvailable, ...newTenants] });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load child tenants:', error);
|
||||
// Don't set error state - this is optional enhancement
|
||||
}
|
||||
},
|
||||
|
||||
loadCurrentTenantAccess: async (): Promise<void> => {
|
||||
try {
|
||||
const { currentTenant } = get();
|
||||
@@ -146,6 +285,7 @@ export const useTenantStore = create<TenantState>()(
|
||||
clearTenants: () => {
|
||||
set({
|
||||
currentTenant: null,
|
||||
parentTenant: null,
|
||||
availableTenants: null,
|
||||
currentTenantAccess: null,
|
||||
error: null,
|
||||
@@ -213,6 +353,7 @@ export const useTenantStore = create<TenantState>()(
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
partialize: (state) => ({
|
||||
currentTenant: state.currentTenant,
|
||||
parentTenant: state.parentTenant,
|
||||
availableTenants: state.availableTenants,
|
||||
currentTenantAccess: state.currentTenantAccess,
|
||||
}),
|
||||
@@ -231,6 +372,7 @@ export const useTenantStore = create<TenantState>()(
|
||||
// Selectors for common use cases
|
||||
// Note: For getting tenant ID, prefer using useTenantId() from hooks/useTenantId.ts
|
||||
export const useCurrentTenant = () => useTenantStore((state) => state.currentTenant);
|
||||
export const useParentTenant = () => useTenantStore((state) => state.parentTenant);
|
||||
export const useAvailableTenants = () => useTenantStore((state) => state.availableTenants);
|
||||
export const useCurrentTenantAccess = () => useTenantStore((state) => state.currentTenantAccess);
|
||||
export const useTenantLoading = () => useTenantStore((state) => state.isLoading);
|
||||
@@ -241,7 +383,10 @@ export const useTenantActions = () => useTenantStore((state) => ({
|
||||
setCurrentTenant: state.setCurrentTenant,
|
||||
setAvailableTenants: state.setAvailableTenants,
|
||||
switchTenant: state.switchTenant,
|
||||
switchToChildTenant: state.switchToChildTenant,
|
||||
restoreParentTenant: state.restoreParentTenant,
|
||||
loadUserTenants: state.loadUserTenants,
|
||||
loadChildTenants: state.loadChildTenants,
|
||||
loadCurrentTenantAccess: state.loadCurrentTenantAccess,
|
||||
clearTenants: state.clearTenants,
|
||||
clearError: state.clearError,
|
||||
@@ -257,15 +402,17 @@ export const useTenantPermissions = () => useTenantStore((state) => ({
|
||||
// Combined hook for convenience
|
||||
export const useTenant = () => {
|
||||
const currentTenant = useCurrentTenant();
|
||||
const parentTenant = useParentTenant();
|
||||
const availableTenants = useAvailableTenants();
|
||||
const currentTenantAccess = useCurrentTenantAccess();
|
||||
const isLoading = useTenantLoading();
|
||||
const error = useTenantError();
|
||||
const actions = useTenantActions();
|
||||
const permissions = useTenantPermissions();
|
||||
|
||||
|
||||
return {
|
||||
currentTenant,
|
||||
parentTenant,
|
||||
availableTenants,
|
||||
currentTenantAccess,
|
||||
isLoading,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useIsAuthenticated } from './auth.store';
|
||||
import { useTenantActions, useAvailableTenants, useCurrentTenant } from './tenant.store';
|
||||
import { useTenantActions, useAvailableTenants, useCurrentTenant, useParentTenant } from './tenant.store';
|
||||
import { useIsDemoMode, useDemoSessionId, useDemoAccountType } from '../hooks/useAccessControl';
|
||||
import { useSubscription } from '../api/hooks/subscription';
|
||||
import { SUBSCRIPTION_TIERS, SubscriptionTier } from '../api/types/subscription';
|
||||
|
||||
/**
|
||||
@@ -54,7 +55,9 @@ export const useTenantInitializer = () => {
|
||||
const demoAccountType = useDemoAccountType();
|
||||
const availableTenants = useAvailableTenants();
|
||||
const currentTenant = useCurrentTenant();
|
||||
const { loadUserTenants, setCurrentTenant, setAvailableTenants } = useTenantActions();
|
||||
const parentTenant = useParentTenant(); // Track if we're viewing a child tenant
|
||||
const { loadUserTenants, loadChildTenants, setCurrentTenant, setAvailableTenants } = useTenantActions();
|
||||
const { subscriptionInfo } = useSubscription();
|
||||
|
||||
// Load tenants for authenticated users (but not demo users - they have special initialization below)
|
||||
useEffect(() => {
|
||||
@@ -63,6 +66,25 @@ export const useTenantInitializer = () => {
|
||||
}
|
||||
}, [isAuthenticated, availableTenants, loadUserTenants, isDemoMode]);
|
||||
|
||||
// Load child tenants for enterprise users
|
||||
useEffect(() => {
|
||||
if (
|
||||
isAuthenticated &&
|
||||
!isDemoMode &&
|
||||
currentTenant &&
|
||||
availableTenants &&
|
||||
subscriptionInfo?.plan === 'enterprise'
|
||||
) {
|
||||
// Only load if we haven't loaded child tenants yet
|
||||
// Check if availableTenants only contains the parent (length === 1)
|
||||
const hasOnlyParent = availableTenants.length === 1 && availableTenants[0].id === currentTenant.id;
|
||||
|
||||
if (hasOnlyParent) {
|
||||
loadChildTenants();
|
||||
}
|
||||
}
|
||||
}, [isAuthenticated, isDemoMode, currentTenant, availableTenants, subscriptionInfo, loadChildTenants]);
|
||||
|
||||
// Set up mock tenant for demo mode with appropriate subscription tier
|
||||
useEffect(() => {
|
||||
if (isDemoMode && demoSessionId) {
|
||||
@@ -106,8 +128,10 @@ export const useTenantInitializer = () => {
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Only set current tenant if not already valid
|
||||
if (!isValidDemoTenant) {
|
||||
// Only set current tenant if not already valid AND we're not viewing a child tenant
|
||||
// CRITICAL: If parentTenant exists, it means we're viewing a child tenant from the Premises page
|
||||
// and we should NOT overwrite the current tenant
|
||||
if (!isValidDemoTenant && !parentTenant) {
|
||||
// Set the demo tenant as current
|
||||
setCurrentTenant(mockTenant);
|
||||
|
||||
@@ -155,5 +179,5 @@ export const useTenantInitializer = () => {
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [isDemoMode, demoSessionId, demoAccountType, currentTenant, availableTenants, setCurrentTenant, setAvailableTenants]);
|
||||
}, [isDemoMode, demoSessionId, demoAccountType, currentTenant, availableTenants, parentTenant, setCurrentTenant, setAvailableTenants]);
|
||||
};
|
||||
Reference in New Issue
Block a user