270 lines
8.9 KiB
TypeScript
270 lines
8.9 KiB
TypeScript
import { create } from 'zustand';
|
|
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
import { tenantService, type TenantResponse, type TenantAccessResponse } from '../api';
|
|
import { useAuthUser } from './auth.store';
|
|
import { TENANT_ROLES, GLOBAL_USER_ROLES } from '../types/roles';
|
|
|
|
export interface TenantState {
|
|
// State
|
|
currentTenant: TenantResponse | null;
|
|
availableTenants: TenantResponse[] | null;
|
|
currentTenantAccess: TenantAccessResponse | null;
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
|
|
// Actions
|
|
setCurrentTenant: (tenant: TenantResponse) => void;
|
|
switchTenant: (tenantId: string) => Promise<boolean>;
|
|
loadUserTenants: () => Promise<void>;
|
|
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;
|
|
}
|
|
|
|
export const useTenantStore = create<TenantState>()(
|
|
persist(
|
|
(set, get) => ({
|
|
// Initial state
|
|
currentTenant: null,
|
|
availableTenants: null,
|
|
currentTenantAccess: null,
|
|
isLoading: false,
|
|
error: null,
|
|
|
|
// Actions
|
|
setCurrentTenant: (tenant: TenantResponse) => {
|
|
set({ currentTenant: tenant, currentTenantAccess: null });
|
|
// Update API client with new tenant ID
|
|
if (tenant) {
|
|
tenantService.setCurrentTenant(tenant);
|
|
// Load tenant access info
|
|
get().loadCurrentTenantAccess();
|
|
}
|
|
},
|
|
|
|
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 });
|
|
return true;
|
|
} catch (error) {
|
|
set({
|
|
isLoading: false,
|
|
error: error instanceof Error ? error.message : 'Failed to switch tenant',
|
|
});
|
|
return false;
|
|
}
|
|
},
|
|
|
|
loadUserTenants: async (): Promise<void> => {
|
|
try {
|
|
set({ isLoading: true, error: null });
|
|
|
|
// Get current user to determine user ID
|
|
const authState = JSON.parse(localStorage.getItem('auth-storage') || '{}')?.state;
|
|
const user = authState?.user;
|
|
|
|
if (!user?.id) {
|
|
throw new Error('User not authenticated');
|
|
}
|
|
|
|
const response = await tenantService.getUserTenants(user.id);
|
|
|
|
if (response) {
|
|
const tenants = Array.isArray(response) ? response : [response];
|
|
|
|
set({
|
|
availableTenants: tenants,
|
|
isLoading: false
|
|
});
|
|
|
|
// If no current tenant is set, set the first one as current
|
|
const { currentTenant } = get();
|
|
if (!currentTenant && tenants.length > 0) {
|
|
get().setCurrentTenant(tenants[0]);
|
|
}
|
|
} else {
|
|
// No tenants found - this is fine for users who haven't completed onboarding
|
|
set({
|
|
availableTenants: [],
|
|
isLoading: false,
|
|
error: null
|
|
});
|
|
}
|
|
} catch (error) {
|
|
// Handle 404 gracefully - user might not have created any tenants yet
|
|
if (error && typeof error === 'object' && 'status' in error && error.status === 404) {
|
|
set({
|
|
availableTenants: [],
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
} else {
|
|
set({
|
|
isLoading: false,
|
|
error: error instanceof Error ? error.message : 'Failed to load tenants',
|
|
});
|
|
}
|
|
}
|
|
},
|
|
|
|
loadCurrentTenantAccess: async (): Promise<void> => {
|
|
try {
|
|
const { currentTenant } = get();
|
|
if (!currentTenant) return;
|
|
|
|
const accessInfo = await tenantService.getCurrentUserTenantAccess(currentTenant.id);
|
|
set({ currentTenantAccess: accessInfo });
|
|
} catch (error) {
|
|
// Don't set error state for access loading failures - just log
|
|
console.warn('Failed to load tenant access:', error);
|
|
set({ currentTenantAccess: null });
|
|
}
|
|
},
|
|
|
|
clearTenants: () => {
|
|
set({
|
|
currentTenant: null,
|
|
availableTenants: null,
|
|
currentTenantAccess: null,
|
|
error: null,
|
|
});
|
|
tenantService.clearCurrentTenant();
|
|
},
|
|
|
|
clearError: () => {
|
|
set({ error: null });
|
|
},
|
|
|
|
setLoading: (loading: boolean) => {
|
|
set({ isLoading: loading });
|
|
},
|
|
|
|
// Permission helpers (migrated from BakeryContext)
|
|
hasPermission: (permission: string): boolean => {
|
|
const { currentTenant, currentTenantAccess } = get();
|
|
if (!currentTenant || !currentTenantAccess || !currentTenantAccess.has_access) {
|
|
return false;
|
|
}
|
|
|
|
// Check if user has specific permission in their tenant permissions array
|
|
if (currentTenantAccess.permissions?.includes(permission)) {
|
|
return true;
|
|
}
|
|
|
|
// Check if user has broader permissions that include this one
|
|
if (currentTenantAccess.permissions?.includes('*') ||
|
|
currentTenantAccess.permissions?.includes('admin')) {
|
|
return true;
|
|
}
|
|
|
|
// Role-based fallback for common permissions based on tenant role
|
|
const tenantRole = currentTenantAccess.role;
|
|
switch (tenantRole) {
|
|
case TENANT_ROLES.OWNER:
|
|
case TENANT_ROLES.ADMIN:
|
|
return true;
|
|
case TENANT_ROLES.MEMBER:
|
|
// Members can read and write but not delete or manage users
|
|
return !permission.includes('delete') && !permission.includes('admin');
|
|
case TENANT_ROLES.VIEWER:
|
|
// Viewers can only read
|
|
return permission.includes('read') || permission.includes('view');
|
|
default:
|
|
return false;
|
|
}
|
|
},
|
|
|
|
canAccess: (resource: string, action: string): boolean => {
|
|
const { hasPermission } = get();
|
|
|
|
// Check specific permission
|
|
if (hasPermission(`${resource}:${action}`)) return true;
|
|
|
|
// Check wildcard permissions
|
|
if (hasPermission(`${resource}:*`)) return true;
|
|
|
|
return false;
|
|
},
|
|
}),
|
|
{
|
|
name: 'tenant-storage',
|
|
storage: createJSONStorage(() => localStorage),
|
|
partialize: (state) => ({
|
|
currentTenant: state.currentTenant,
|
|
availableTenants: state.availableTenants,
|
|
currentTenantAccess: state.currentTenantAccess,
|
|
}),
|
|
onRehydrateStorage: () => (state) => {
|
|
// Initialize API client with stored tenant when store rehydrates
|
|
if (state?.currentTenant) {
|
|
import('../api').then(({ apiClient }) => {
|
|
apiClient.setTenantId(state.currentTenant!.id);
|
|
});
|
|
}
|
|
},
|
|
}
|
|
)
|
|
);
|
|
|
|
// 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 useAvailableTenants = () => useTenantStore((state) => state.availableTenants);
|
|
export const useCurrentTenantAccess = () => useTenantStore((state) => state.currentTenantAccess);
|
|
export const useTenantLoading = () => useTenantStore((state) => state.isLoading);
|
|
export const useTenantError = () => useTenantStore((state) => state.error);
|
|
|
|
// Hook for tenant actions
|
|
export const useTenantActions = () => useTenantStore((state) => ({
|
|
setCurrentTenant: state.setCurrentTenant,
|
|
switchTenant: state.switchTenant,
|
|
loadUserTenants: state.loadUserTenants,
|
|
loadCurrentTenantAccess: state.loadCurrentTenantAccess,
|
|
clearTenants: state.clearTenants,
|
|
clearError: state.clearError,
|
|
setLoading: state.setLoading,
|
|
}));
|
|
|
|
// Hook for tenant permissions (replaces useBakeryPermissions)
|
|
export const useTenantPermissions = () => useTenantStore((state) => ({
|
|
hasPermission: state.hasPermission,
|
|
canAccess: state.canAccess,
|
|
}));
|
|
|
|
// Combined hook for convenience
|
|
export const useTenant = () => {
|
|
const currentTenant = useCurrentTenant();
|
|
const availableTenants = useAvailableTenants();
|
|
const currentTenantAccess = useCurrentTenantAccess();
|
|
const isLoading = useTenantLoading();
|
|
const error = useTenantError();
|
|
const actions = useTenantActions();
|
|
const permissions = useTenantPermissions();
|
|
|
|
return {
|
|
currentTenant,
|
|
availableTenants,
|
|
currentTenantAccess,
|
|
isLoading,
|
|
error,
|
|
...actions,
|
|
...permissions,
|
|
};
|
|
}; |