Start integrating the onboarding flow with backend 1
This commit is contained in:
@@ -4,19 +4,16 @@ import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: 'admin' | 'manager' | 'baker' | 'staff';
|
||||
permissions: string[];
|
||||
tenantId: string;
|
||||
tenantName: string;
|
||||
avatar?: string;
|
||||
lastLogin?: string;
|
||||
preferences?: {
|
||||
language: string;
|
||||
timezone: string;
|
||||
theme: 'light' | 'dark';
|
||||
notifications: boolean;
|
||||
};
|
||||
full_name: string; // Updated to match backend
|
||||
is_active: boolean;
|
||||
is_verified: boolean;
|
||||
created_at: string;
|
||||
last_login?: string;
|
||||
phone?: string;
|
||||
language?: string;
|
||||
timezone?: string;
|
||||
tenant_id?: string;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
export interface AuthState {
|
||||
@@ -30,6 +27,7 @@ export interface AuthState {
|
||||
|
||||
// Actions
|
||||
login: (email: string, password: string) => Promise<void>;
|
||||
register: (userData: { email: string; password: string; full_name: string; tenant_name?: string }) => Promise<void>;
|
||||
logout: () => void;
|
||||
refreshAuth: () => Promise<void>;
|
||||
updateUser: (updates: Partial<User>) => void;
|
||||
@@ -42,50 +40,7 @@ export interface AuthState {
|
||||
canAccess: (resource: string, action: string) => boolean;
|
||||
}
|
||||
|
||||
// Mock API functions (replace with actual API calls)
|
||||
const mockLogin = async (email: string, password: string): Promise<{ user: User; token: string; refreshToken: string }> => {
|
||||
// Simulate API delay
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
if (email === 'admin@bakery.com' && password === 'admin12345') {
|
||||
return {
|
||||
user: {
|
||||
id: '1',
|
||||
email: 'admin@bakery.com',
|
||||
name: 'Admin User',
|
||||
role: 'admin',
|
||||
permissions: ['*'],
|
||||
tenantId: 'tenant-1',
|
||||
tenantName: 'Panadería San Miguel',
|
||||
avatar: undefined,
|
||||
lastLogin: new Date().toISOString(),
|
||||
preferences: {
|
||||
language: 'es',
|
||||
timezone: 'Europe/Madrid',
|
||||
theme: 'light',
|
||||
notifications: true,
|
||||
},
|
||||
},
|
||||
token: 'mock-jwt-token',
|
||||
refreshToken: 'mock-refresh-token',
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('Credenciales inválidas');
|
||||
};
|
||||
|
||||
const mockRefreshToken = async (refreshToken: string): Promise<{ token: string; refreshToken: string }> => {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
if (refreshToken === 'mock-refresh-token') {
|
||||
return {
|
||||
token: 'new-mock-jwt-token',
|
||||
refreshToken: 'new-mock-refresh-token',
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('Invalid refresh token');
|
||||
};
|
||||
import { authService } from '../services/api/auth.service';
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
@@ -103,16 +58,20 @@ export const useAuthStore = create<AuthState>()(
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
const response = await mockLogin(email, password);
|
||||
const response = await authService.login({ email, password });
|
||||
|
||||
set({
|
||||
user: response.user,
|
||||
token: response.token,
|
||||
refreshToken: response.refreshToken,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
if (response.success && response.data) {
|
||||
set({
|
||||
user: response.data.user || null,
|
||||
token: response.data.access_token,
|
||||
refreshToken: response.data.refresh_token || null,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
} else {
|
||||
throw new Error('Login failed');
|
||||
}
|
||||
} catch (error) {
|
||||
set({
|
||||
user: null,
|
||||
@@ -126,6 +85,37 @@ export const useAuthStore = create<AuthState>()(
|
||||
}
|
||||
},
|
||||
|
||||
register: async (userData: { email: string; password: string; full_name: string; tenant_name?: string }) => {
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
const response = await authService.register(userData);
|
||||
|
||||
if (response.success && response.data) {
|
||||
set({
|
||||
user: response.data.user || null,
|
||||
token: response.data.access_token,
|
||||
refreshToken: response.data.refresh_token || null,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
} else {
|
||||
throw new Error('Registration failed');
|
||||
}
|
||||
} catch (error) {
|
||||
set({
|
||||
user: null,
|
||||
token: null,
|
||||
refreshToken: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Error de registro',
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
set({
|
||||
user: null,
|
||||
@@ -146,14 +136,18 @@ export const useAuthStore = create<AuthState>()(
|
||||
|
||||
set({ isLoading: true });
|
||||
|
||||
const response = await mockRefreshToken(refreshToken);
|
||||
const response = await authService.refreshToken(refreshToken);
|
||||
|
||||
set({
|
||||
token: response.token,
|
||||
refreshToken: response.refreshToken,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
if (response.success && response.data) {
|
||||
set({
|
||||
token: response.data.access_token,
|
||||
refreshToken: response.data.refresh_token || refreshToken,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
} else {
|
||||
throw new Error('Token refresh failed');
|
||||
}
|
||||
} catch (error) {
|
||||
set({
|
||||
user: null,
|
||||
@@ -184,15 +178,16 @@ export const useAuthStore = create<AuthState>()(
|
||||
set({ isLoading: loading });
|
||||
},
|
||||
|
||||
// Permission helpers
|
||||
hasPermission: (permission: string): boolean => {
|
||||
// Permission helpers - Simplified for backend compatibility
|
||||
hasPermission: (_permission: string): boolean => {
|
||||
const { user } = get();
|
||||
if (!user) return false;
|
||||
if (!user || !user.is_active) return false;
|
||||
|
||||
// Admin has all permissions
|
||||
if (user.permissions.includes('*')) return true;
|
||||
if (user.role === 'admin') return true;
|
||||
|
||||
return user.permissions.includes(permission);
|
||||
// Basic role-based permissions
|
||||
return false;
|
||||
},
|
||||
|
||||
hasRole: (role: string): boolean => {
|
||||
@@ -201,28 +196,17 @@ export const useAuthStore = create<AuthState>()(
|
||||
},
|
||||
|
||||
canAccess: (resource: string, action: string): boolean => {
|
||||
const { user, hasPermission } = get();
|
||||
if (!user) return false;
|
||||
const { user } = get();
|
||||
if (!user || !user.is_active) return false;
|
||||
|
||||
// Check specific permission
|
||||
if (hasPermission(`${resource}:${action}`)) return true;
|
||||
|
||||
// Check wildcard permissions
|
||||
if (hasPermission(`${resource}:*`)) return true;
|
||||
if (hasPermission('*')) return true;
|
||||
|
||||
// Role-based access fallback
|
||||
// Role-based access control
|
||||
switch (user.role) {
|
||||
case 'admin':
|
||||
return true;
|
||||
case 'manager':
|
||||
return ['inventory', 'production', 'sales', 'reports'].includes(resource);
|
||||
case 'baker':
|
||||
return ['production', 'inventory'].includes(resource) &&
|
||||
['read', 'update'].includes(action);
|
||||
case 'staff':
|
||||
return ['inventory', 'sales'].includes(resource) &&
|
||||
action === 'read';
|
||||
case 'user':
|
||||
return ['inventory', 'sales'].includes(resource) && action === 'read';
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -237,6 +221,18 @@ export const useAuthStore = create<AuthState>()(
|
||||
refreshToken: state.refreshToken,
|
||||
isAuthenticated: state.isAuthenticated,
|
||||
}),
|
||||
onRehydrateStorage: () => (state) => {
|
||||
// Initialize API client with stored token when store rehydrates
|
||||
if (state?.token) {
|
||||
import('../services/api/client').then(({ apiClient }) => {
|
||||
apiClient.setAuthToken(state.token!);
|
||||
|
||||
if (state.user?.tenant_id) {
|
||||
apiClient.setTenantId(state.user.tenant_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -255,6 +251,7 @@ export const usePermissions = () => useAuthStore((state) => ({
|
||||
// Hook for auth actions
|
||||
export const useAuthActions = () => useAuthStore((state) => ({
|
||||
login: state.login,
|
||||
register: state.register,
|
||||
logout: state.logout,
|
||||
refreshAuth: state.refreshAuth,
|
||||
updateUser: state.updateUser,
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
|
||||
export type BakeryType = 'artisan' | 'dependent';
|
||||
export type BusinessModel = 'production' | 'retail' | 'hybrid';
|
||||
|
||||
interface BakeryState {
|
||||
// State
|
||||
currentTenant: Tenant | null;
|
||||
bakeryType: BakeryType;
|
||||
businessModel: BusinessModel;
|
||||
operatingHours: {
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
features: {
|
||||
inventory: boolean;
|
||||
production: boolean;
|
||||
forecasting: boolean;
|
||||
analytics: boolean;
|
||||
pos: boolean;
|
||||
};
|
||||
|
||||
// Actions
|
||||
setTenant: (tenant: Tenant) => void;
|
||||
setBakeryType: (type: BakeryType) => void;
|
||||
setBusinessModel: (model: BusinessModel) => void;
|
||||
updateFeatures: (features: Partial<BakeryState['features']>) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
interface Tenant {
|
||||
id: string;
|
||||
name: string;
|
||||
subscription_plan: string;
|
||||
created_at: string;
|
||||
members_count: number;
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
currentTenant: null,
|
||||
bakeryType: 'artisan' as BakeryType,
|
||||
businessModel: 'production' as BusinessModel,
|
||||
operatingHours: {
|
||||
start: '04:00',
|
||||
end: '20:00',
|
||||
},
|
||||
features: {
|
||||
inventory: true,
|
||||
production: true,
|
||||
forecasting: true,
|
||||
analytics: true,
|
||||
pos: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const useBakeryStore = create<BakeryState>()(
|
||||
devtools(
|
||||
persist(
|
||||
immer((set) => ({
|
||||
...initialState,
|
||||
|
||||
setTenant: (tenant) =>
|
||||
set((state) => {
|
||||
state.currentTenant = tenant;
|
||||
}),
|
||||
|
||||
setBakeryType: (type) =>
|
||||
set((state) => {
|
||||
state.bakeryType = type;
|
||||
// Adjust features based on bakery type
|
||||
if (type === 'artisan') {
|
||||
state.features.pos = false;
|
||||
state.businessModel = 'production';
|
||||
} else if (type === 'dependent') {
|
||||
state.features.pos = true;
|
||||
state.businessModel = 'retail';
|
||||
}
|
||||
}),
|
||||
|
||||
setBusinessModel: (model) =>
|
||||
set((state) => {
|
||||
state.businessModel = model;
|
||||
}),
|
||||
|
||||
updateFeatures: (features) =>
|
||||
set((state) => {
|
||||
state.features = { ...state.features, ...features };
|
||||
}),
|
||||
|
||||
reset: () => set(() => initialState),
|
||||
})),
|
||||
{
|
||||
name: 'bakery-storage',
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -5,8 +5,9 @@ export type { User, AuthState } from './auth.store';
|
||||
export { useUIStore, useLanguage, useSidebar, useCompactMode, useViewMode, useLoading, useToasts, useModals, useBreadcrumbs, usePreferences, useUIActions } from './ui.store';
|
||||
export type { Theme, Language, ViewMode, SidebarState, Toast, Modal, UIState } from './ui.store';
|
||||
|
||||
export { useBakeryStore } from './bakery.store';
|
||||
export type { BakeryType, BusinessModel } from './bakery.store';
|
||||
|
||||
export { useTenantStore, useCurrentTenant, useAvailableTenants, useTenantLoading, useTenantError, useTenantActions, useTenantPermissions, useTenant } from './tenant.store';
|
||||
export type { TenantState } from './tenant.store';
|
||||
|
||||
export { useAlertsStore, useAlerts, useAlertRules, useAlertFilters, useAlertSettings, useUnreadAlertsCount, useCriticalAlertsCount } from './alerts.store';
|
||||
export type { Alert, AlertRule, AlertCondition, AlertAction, AlertsState, AlertType, AlertCategory, AlertPriority, AlertStatus } from './alerts.store';
|
||||
230
frontend/src/stores/tenant.store.ts
Normal file
230
frontend/src/stores/tenant.store.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
import { tenantService, TenantResponse } from '../services/api/tenant.service';
|
||||
import { useAuthUser } from './auth.store';
|
||||
|
||||
export interface TenantState {
|
||||
// State
|
||||
currentTenant: TenantResponse | null;
|
||||
availableTenants: TenantResponse[] | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Actions
|
||||
setCurrentTenant: (tenant: TenantResponse) => void;
|
||||
switchTenant: (tenantId: string) => Promise<boolean>;
|
||||
loadUserTenants: () => 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,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
// Actions
|
||||
setCurrentTenant: (tenant: TenantResponse) => {
|
||||
set({ currentTenant: tenant });
|
||||
// Update API client with new tenant ID
|
||||
tenantService.setCurrentTenant(tenant);
|
||||
},
|
||||
|
||||
switchTenant: async (tenantId: string): Promise<boolean> => {
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
const { availableTenants } = get();
|
||||
|
||||
// Find tenant in available tenants first
|
||||
const targetTenant = availableTenants?.find(t => t.id === tenantId);
|
||||
if (!targetTenant) {
|
||||
throw new Error('Tenant not found in available tenants');
|
||||
}
|
||||
|
||||
// Switch tenant using service
|
||||
const response = await tenantService.switchTenant(tenantId);
|
||||
|
||||
if (response.success && response.data?.tenant) {
|
||||
get().setCurrentTenant(response.data.tenant);
|
||||
set({ isLoading: false });
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(response.error || 'Failed to switch tenant');
|
||||
}
|
||||
} 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 user = useAuthUser.getState?.() || JSON.parse(localStorage.getItem('auth-storage') || '{}')?.state?.user;
|
||||
|
||||
if (!user?.id) {
|
||||
throw new Error('User not authenticated');
|
||||
}
|
||||
|
||||
const response = await tenantService.getUserTenants(user.id);
|
||||
|
||||
if (response.success && response.data) {
|
||||
const tenants = Array.isArray(response.data) ? response.data : [response.data];
|
||||
|
||||
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 {
|
||||
throw new Error(response.error || 'Failed to load user tenants');
|
||||
}
|
||||
} catch (error) {
|
||||
set({
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to load tenants',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
clearTenants: () => {
|
||||
set({
|
||||
currentTenant: null,
|
||||
availableTenants: 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 } = get();
|
||||
if (!currentTenant) return false;
|
||||
|
||||
// Get user to determine role within this tenant
|
||||
const user = useAuthUser.getState?.() || JSON.parse(localStorage.getItem('auth-storage') || '{}')?.state?.user;
|
||||
|
||||
// Admin role has all permissions
|
||||
if (user?.role === 'admin') return true;
|
||||
|
||||
// TODO: Implement proper tenant-based permissions
|
||||
// For now, use basic role-based permissions
|
||||
switch (user?.role) {
|
||||
case 'admin':
|
||||
return true;
|
||||
case 'manager':
|
||||
return ['inventory', 'production', 'sales', 'reports'].some(resource =>
|
||||
permission.startsWith(resource)
|
||||
);
|
||||
case 'baker':
|
||||
return ['production', 'inventory'].some(resource =>
|
||||
permission.startsWith(resource)
|
||||
) && !permission.includes(':delete');
|
||||
case 'staff':
|
||||
return ['inventory', 'sales'].some(resource =>
|
||||
permission.startsWith(resource)
|
||||
) && permission.includes(':read');
|
||||
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,
|
||||
}),
|
||||
onRehydrateStorage: () => (state) => {
|
||||
// Initialize API client with stored tenant when store rehydrates
|
||||
if (state?.currentTenant) {
|
||||
import('../services/api/client').then(({ apiClient }) => {
|
||||
apiClient.setTenantId(state.currentTenant!.id);
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Selectors for common use cases
|
||||
export const useCurrentTenant = () => useTenantStore((state) => state.currentTenant);
|
||||
export const useAvailableTenants = () => useTenantStore((state) => state.availableTenants);
|
||||
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,
|
||||
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 isLoading = useTenantLoading();
|
||||
const error = useTenantError();
|
||||
const actions = useTenantActions();
|
||||
const permissions = useTenantPermissions();
|
||||
|
||||
return {
|
||||
currentTenant,
|
||||
availableTenants,
|
||||
isLoading,
|
||||
error,
|
||||
...actions,
|
||||
...permissions,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user