Fixed multiple TypeScript type errors that were preventing the build from working properly: 1. Fixed infinite query type issue in forecasting.ts by excluding 'select' from options 2. Fixed Card variant type errors by changing contentPadding="default" to contentPadding="md" 3. Fixed router export issues by removing non-existent exports (ROUTE_CONFIGS, getRoutesForRole, etc.) 4. Fixed router readonly array type issues by updating RouteConfig interface 5. Fixed ProtectedRoute requiredRoles prop issue by removing invalid prop usage 6. Fixed auth store User type compatibility by allowing null for tenant_id 7. Fixed missing useToasts export from ui.store by removing from exports 8. Fixed permissions utility boolean type issues by wrapping expressions in Boolean() The frontend build now completes successfully.
319 lines
9.3 KiB
TypeScript
319 lines
9.3 KiB
TypeScript
import { create } from 'zustand';
|
|
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
import { GLOBAL_USER_ROLES, type GlobalUserRole } from '../types/roles';
|
|
|
|
export interface User {
|
|
id: string;
|
|
email: string;
|
|
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;
|
|
avatar?: string; // User avatar image URL
|
|
tenant_id?: string | null;
|
|
role?: GlobalUserRole;
|
|
}
|
|
|
|
export interface AuthState {
|
|
// State
|
|
user: User | null;
|
|
token: string | null;
|
|
refreshToken: string | null;
|
|
isAuthenticated: boolean;
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
|
|
// Actions
|
|
login: (email: string, password: string) => Promise<void>;
|
|
register: (userData: {
|
|
email: string;
|
|
password: string;
|
|
full_name: string;
|
|
tenant_name?: string;
|
|
subscription_plan?: string;
|
|
use_trial?: boolean;
|
|
payment_method_id?: string;
|
|
}) => Promise<void>;
|
|
logout: () => void;
|
|
refreshAuth: () => Promise<void>;
|
|
updateUser: (updates: Partial<User>) => void;
|
|
clearError: () => void;
|
|
setLoading: (loading: boolean) => void;
|
|
|
|
// Permission helpers
|
|
hasPermission: (permission: string) => boolean;
|
|
hasRole: (role: string) => boolean;
|
|
canAccess: (resource: string, action: string) => boolean;
|
|
}
|
|
|
|
import { authService, apiClient } from '../api';
|
|
|
|
export const useAuthStore = create<AuthState>()(
|
|
persist(
|
|
(set, get) => ({
|
|
// Initial state
|
|
user: null,
|
|
token: null,
|
|
refreshToken: null,
|
|
isAuthenticated: false,
|
|
isLoading: false,
|
|
error: null,
|
|
|
|
// Actions
|
|
login: async (email: string, password: string) => {
|
|
try {
|
|
set({ isLoading: true, error: null });
|
|
|
|
const response = await authService.login({ email, password });
|
|
|
|
if (response && response.access_token) {
|
|
// Set the auth tokens on the API client immediately
|
|
apiClient.setAuthToken(response.access_token);
|
|
if (response.refresh_token) {
|
|
apiClient.setRefreshToken(response.refresh_token);
|
|
}
|
|
|
|
set({
|
|
user: response.user || null,
|
|
token: response.access_token,
|
|
refreshToken: response.refresh_token || null,
|
|
isAuthenticated: true,
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
} else {
|
|
throw new Error('Login failed');
|
|
}
|
|
} catch (error) {
|
|
set({
|
|
user: null,
|
|
token: null,
|
|
refreshToken: null,
|
|
isAuthenticated: false,
|
|
isLoading: false,
|
|
error: error instanceof Error ? error.message : 'Error de autenticación',
|
|
});
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
register: async (userData: {
|
|
email: string;
|
|
password: string;
|
|
full_name: string;
|
|
tenant_name?: string;
|
|
subscription_plan?: string;
|
|
use_trial?: boolean;
|
|
payment_method_id?: string;
|
|
}) => {
|
|
try {
|
|
set({ isLoading: true, error: null });
|
|
|
|
const response = await authService.register(userData);
|
|
|
|
if (response && response.access_token) {
|
|
// Set the auth tokens on the API client immediately
|
|
apiClient.setAuthToken(response.access_token);
|
|
if (response.refresh_token) {
|
|
apiClient.setRefreshToken(response.refresh_token);
|
|
}
|
|
|
|
set({
|
|
user: response.user || null,
|
|
token: response.access_token,
|
|
refreshToken: response.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: () => {
|
|
// Clear the auth tokens from API client
|
|
apiClient.setAuthToken(null);
|
|
apiClient.setRefreshToken(null);
|
|
apiClient.setTenantId(null);
|
|
|
|
// Clear tenant store to remove cached tenant data
|
|
// Import dynamically to avoid circular dependencies
|
|
import('./tenant.store').then(({ useTenantStore }) => {
|
|
useTenantStore.getState().clearTenants();
|
|
}).catch(err => {
|
|
console.warn('Failed to clear tenant store on logout:', err);
|
|
});
|
|
|
|
set({
|
|
user: null,
|
|
token: null,
|
|
refreshToken: null,
|
|
isAuthenticated: false,
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
},
|
|
|
|
refreshAuth: async () => {
|
|
try {
|
|
const { refreshToken } = get();
|
|
if (!refreshToken) {
|
|
throw new Error('No refresh token available');
|
|
}
|
|
|
|
set({ isLoading: true });
|
|
|
|
const response = await authService.refreshToken(refreshToken);
|
|
|
|
if (response && response.access_token) {
|
|
// Set the auth tokens on the API client immediately
|
|
apiClient.setAuthToken(response.access_token);
|
|
if (response.refresh_token) {
|
|
apiClient.setRefreshToken(response.refresh_token);
|
|
}
|
|
|
|
set({
|
|
token: response.access_token,
|
|
refreshToken: response.refresh_token || refreshToken,
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
} else {
|
|
throw new Error('Token refresh failed');
|
|
}
|
|
} catch (error) {
|
|
set({
|
|
user: null,
|
|
token: null,
|
|
refreshToken: null,
|
|
isAuthenticated: false,
|
|
isLoading: false,
|
|
error: error instanceof Error ? error.message : 'Error al renovar sesión',
|
|
});
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
updateUser: (updates: Partial<User>) => {
|
|
const { user } = get();
|
|
if (user) {
|
|
set({
|
|
user: { ...user, ...updates },
|
|
});
|
|
}
|
|
},
|
|
|
|
clearError: () => {
|
|
set({ error: null });
|
|
},
|
|
|
|
setLoading: (loading: boolean) => {
|
|
set({ isLoading: loading });
|
|
},
|
|
|
|
// Permission helpers - Global user permissions only
|
|
hasPermission: (_permission: string): boolean => {
|
|
const { user } = get();
|
|
if (!user || !user.is_active) return false;
|
|
|
|
// Super admin and admin have all global permissions
|
|
if (user.role === GLOBAL_USER_ROLES.SUPER_ADMIN || user.role === GLOBAL_USER_ROLES.ADMIN) {
|
|
return true;
|
|
}
|
|
|
|
// Manager has limited permissions
|
|
if (user.role === GLOBAL_USER_ROLES.MANAGER) {
|
|
return ['user_management', 'system_settings'].includes(_permission);
|
|
}
|
|
|
|
// Regular users have basic permissions
|
|
return false;
|
|
},
|
|
|
|
hasRole: (role: string): boolean => {
|
|
const { user } = get();
|
|
return user?.role === role;
|
|
},
|
|
|
|
canAccess: (resource: string, action: string): boolean => {
|
|
const { user } = get();
|
|
if (!user || !user.is_active) return false;
|
|
|
|
// Global role-based access control (system-wide)
|
|
switch (user.role) {
|
|
case GLOBAL_USER_ROLES.SUPER_ADMIN:
|
|
case GLOBAL_USER_ROLES.ADMIN:
|
|
return true;
|
|
case GLOBAL_USER_ROLES.MANAGER:
|
|
return ['users', 'system'].includes(resource);
|
|
case GLOBAL_USER_ROLES.USER:
|
|
return action === 'read';
|
|
default:
|
|
return false;
|
|
}
|
|
},
|
|
}),
|
|
{
|
|
name: 'auth-storage',
|
|
storage: createJSONStorage(() => localStorage),
|
|
partialize: (state) => ({
|
|
user: state.user,
|
|
token: state.token,
|
|
refreshToken: state.refreshToken,
|
|
isAuthenticated: state.isAuthenticated,
|
|
}),
|
|
onRehydrateStorage: () => (state) => {
|
|
// Initialize API client with stored tokens when store rehydrates
|
|
if (state?.token) {
|
|
// Use direct import to avoid timing issues
|
|
apiClient.setAuthToken(state.token);
|
|
if (state.refreshToken) {
|
|
apiClient.setRefreshToken(state.refreshToken);
|
|
}
|
|
|
|
if (state.user?.tenant_id) {
|
|
apiClient.setTenantId(state.user.tenant_id);
|
|
}
|
|
}
|
|
},
|
|
}
|
|
)
|
|
);
|
|
|
|
// Selectors for common use cases
|
|
export const useAuthUser = () => useAuthStore((state) => state.user);
|
|
export const useIsAuthenticated = () => useAuthStore((state) => state.isAuthenticated);
|
|
export const useAuthLoading = () => useAuthStore((state) => state.isLoading);
|
|
export const useAuthError = () => useAuthStore((state) => state.error);
|
|
export const usePermissions = () => useAuthStore((state) => ({
|
|
hasPermission: state.hasPermission,
|
|
hasRole: state.hasRole,
|
|
canAccess: state.canAccess,
|
|
}));
|
|
|
|
// Hook for auth actions
|
|
export const useAuthActions = () => useAuthStore((state) => ({
|
|
login: state.login,
|
|
register: state.register,
|
|
logout: state.logout,
|
|
refreshAuth: state.refreshAuth,
|
|
updateUser: state.updateUser,
|
|
clearError: state.clearError,
|
|
setLoading: state.setLoading,
|
|
})); |