Improve enterprise tier child tenants access

This commit is contained in:
Urtzi Alfaro
2026-01-07 16:01:19 +01:00
parent 2c1fc756a1
commit 560c7ba86f
19 changed files with 854 additions and 15 deletions

View File

@@ -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,

View File

@@ -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]);
};