Imporve the role based forntend protected roles
This commit is contained in:
@@ -54,6 +54,12 @@ export class TenantService {
|
||||
return apiClient.get<TenantAccessResponse>(`${this.baseUrl}/${tenantId}/access/${userId}`);
|
||||
}
|
||||
|
||||
async getCurrentUserTenantAccess(tenantId: string): Promise<TenantAccessResponse> {
|
||||
// This will use the current user from the auth token
|
||||
// The backend endpoint handles extracting user_id from the token
|
||||
return apiClient.get<TenantAccessResponse>(`${this.baseUrl}/${tenantId}/my-access`);
|
||||
}
|
||||
|
||||
// Search & Discovery
|
||||
async searchTenants(params: TenantSearchParams): Promise<TenantResponse[]> {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* Auth API Types - Mirror backend schemas
|
||||
*/
|
||||
|
||||
import type { GlobalUserRole } from '../../types/roles';
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
@@ -14,7 +16,7 @@ export interface User {
|
||||
language?: string;
|
||||
timezone?: string;
|
||||
tenant_id?: string;
|
||||
role?: string;
|
||||
role?: GlobalUserRole;
|
||||
}
|
||||
|
||||
export interface UserRegistration {
|
||||
@@ -65,7 +67,7 @@ export interface UserResponse {
|
||||
language?: string;
|
||||
timezone?: string;
|
||||
tenant_id?: string;
|
||||
role?: string;
|
||||
role?: GlobalUserRole;
|
||||
}
|
||||
|
||||
export interface UserUpdate {
|
||||
@@ -79,7 +81,7 @@ export interface TokenVerificationResponse {
|
||||
valid: boolean;
|
||||
user_id?: string;
|
||||
email?: string;
|
||||
role?: string;
|
||||
role?: GlobalUserRole;
|
||||
exp?: number;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* Tenant API Types - Mirror backend schemas
|
||||
*/
|
||||
|
||||
import type { TenantRole } from '../../types/roles';
|
||||
|
||||
export interface BakeryRegistration {
|
||||
name: string;
|
||||
address: string;
|
||||
@@ -38,8 +40,10 @@ export interface TenantResponse {
|
||||
|
||||
export interface TenantAccessResponse {
|
||||
has_access: boolean;
|
||||
role?: string;
|
||||
role?: TenantRole;
|
||||
permissions?: string[];
|
||||
membership_id?: string;
|
||||
joined_at?: string;
|
||||
}
|
||||
|
||||
export interface TenantUpdate {
|
||||
@@ -62,7 +66,7 @@ export interface TenantMemberResponse {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
user_id: string;
|
||||
role: string;
|
||||
role: TenantRole;
|
||||
is_active: boolean;
|
||||
joined_at: string;
|
||||
user_email?: string;
|
||||
|
||||
@@ -36,7 +36,7 @@ export const LoginForm: React.FC<LoginFormProps> = ({
|
||||
const { login } = useAuthActions();
|
||||
const isLoading = useAuthLoading();
|
||||
const error = useAuthError();
|
||||
const { showToast } = useToast();
|
||||
const { success, error: showError } = useToast();
|
||||
|
||||
// Auto-focus on email field when component mounts
|
||||
useEffect(() => {
|
||||
@@ -76,17 +76,13 @@ export const LoginForm: React.FC<LoginFormProps> = ({
|
||||
|
||||
try {
|
||||
await login(credentials.email, credentials.password);
|
||||
showToast({
|
||||
type: 'success',
|
||||
title: 'Sesión iniciada correctamente',
|
||||
message: '¡Bienvenido de vuelta a tu panadería!'
|
||||
success('¡Bienvenido de vuelta a tu panadería!', {
|
||||
title: 'Sesión iniciada correctamente'
|
||||
});
|
||||
onSuccess?.();
|
||||
} catch (err) {
|
||||
showToast({
|
||||
type: 'error',
|
||||
title: 'Error al iniciar sesión',
|
||||
message: error || 'Email o contraseña incorrectos. Verifica tus credenciales.'
|
||||
showError(error || 'Email o contraseña incorrectos. Verifica tus credenciales.', {
|
||||
title: 'Error al iniciar sesión'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useCallback, forwardRef, useMemo } from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useAuthUser, useIsAuthenticated } from '../../../stores';
|
||||
import { useCurrentTenantAccess } from '../../../stores/tenant.store';
|
||||
import { getNavigationRoutes, canAccessRoute, ROUTES } from '../../../router/routes.config';
|
||||
import { Button } from '../../ui';
|
||||
import { Badge } from '../../ui';
|
||||
@@ -127,6 +128,7 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
|
||||
const navigate = useNavigate();
|
||||
const user = useAuthUser();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const currentTenantAccess = useCurrentTenantAccess();
|
||||
|
||||
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
|
||||
const [hoveredItem, setHoveredItem] = useState<string | null>(null);
|
||||
@@ -160,8 +162,12 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
|
||||
...item, // Create a shallow copy to avoid mutation
|
||||
children: item.children ? filterItemsByPermissions(item.children) : item.children
|
||||
})).filter(item => {
|
||||
const userRoles = user.role ? [user.role] : [];
|
||||
const userPermissions: string[] = user?.permissions || [];
|
||||
// Combine global and tenant roles for comprehensive access control
|
||||
const globalUserRoles = user.role ? [user.role as string] : [];
|
||||
const tenantRole = currentTenantAccess?.role;
|
||||
const tenantRoles = tenantRole ? [tenantRole as string] : [];
|
||||
const allUserRoles = [...globalUserRoles, ...tenantRoles];
|
||||
const tenantPermissions = currentTenantAccess?.permissions || [];
|
||||
|
||||
const hasAccess = !item.requiredPermissions && !item.requiredRoles ||
|
||||
canAccessRoute(
|
||||
@@ -171,8 +177,8 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
|
||||
requiredPermissions: item.requiredPermissions
|
||||
} as any,
|
||||
isAuthenticated,
|
||||
userRoles,
|
||||
userPermissions
|
||||
allUserRoles,
|
||||
tenantPermissions
|
||||
);
|
||||
|
||||
return hasAccess;
|
||||
@@ -180,7 +186,7 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
|
||||
};
|
||||
|
||||
return filterItemsByPermissions(navigationItems);
|
||||
}, [navigationItems, isAuthenticated, user]);
|
||||
}, [navigationItems, isAuthenticated, user, currentTenantAccess]);
|
||||
|
||||
// Handle item click
|
||||
const handleItemClick = useCallback((item: NavigationItem) => {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import React from 'react';
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
import { useAuthUser, useIsAuthenticated, useAuthLoading } from '../stores';
|
||||
import { useCurrentTenantAccess, useTenantPermissions } from '../stores/tenant.store';
|
||||
import { RouteConfig, canAccessRoute, ROUTES } from './routes.config';
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
@@ -126,6 +127,8 @@ export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
||||
const user = useAuthUser();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const isLoading = useAuthLoading();
|
||||
const currentTenantAccess = useCurrentTenantAccess();
|
||||
const { hasPermission } = useTenantPermissions();
|
||||
const location = useLocation();
|
||||
|
||||
// Note: Onboarding routes are now properly protected and require authentication
|
||||
@@ -160,16 +163,21 @@ export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
||||
}
|
||||
|
||||
// Get user roles and permissions
|
||||
const userRoles = user?.role ? [user.role] : [];
|
||||
const userPermissions: string[] = [];
|
||||
const globalUserRoles = user?.role ? [user.role] : [];
|
||||
const tenantRole = currentTenantAccess?.role;
|
||||
const tenantRoles = tenantRole ? [tenantRole] : [];
|
||||
|
||||
// Combine global and tenant roles for comprehensive access control
|
||||
const allUserRoles = [...globalUserRoles, ...tenantRoles];
|
||||
const tenantPermissions = currentTenantAccess?.permissions || [];
|
||||
|
||||
// Check if user can access this route
|
||||
const canAccess = canAccessRoute(route, isAuthenticated, userRoles, userPermissions);
|
||||
const canAccess = canAccessRoute(route, isAuthenticated, allUserRoles, tenantPermissions);
|
||||
|
||||
if (!canAccess) {
|
||||
// Check if it's a permission issue or role issue
|
||||
const hasRequiredRoles = !route.requiredRoles ||
|
||||
route.requiredRoles.some(role => userRoles.includes(role));
|
||||
route.requiredRoles.some(role => allUserRoles.includes(role as string));
|
||||
|
||||
if (!hasRequiredRoles) {
|
||||
return <UnauthorizedPage />;
|
||||
@@ -211,22 +219,29 @@ export const useRouteAccess = (route: RouteConfig) => {
|
||||
const user = useAuthUser();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const isLoading = useAuthLoading();
|
||||
const currentTenantAccess = useCurrentTenantAccess();
|
||||
|
||||
const userRoles = user?.role ? [user.role] : [];
|
||||
const userPermissions: string[] = [];
|
||||
const globalUserRoles = user?.role ? [user.role as string] : [];
|
||||
const tenantRole = currentTenantAccess?.role;
|
||||
const tenantRoles = tenantRole ? [tenantRole as string] : [];
|
||||
const allUserRoles = [...globalUserRoles, ...tenantRoles];
|
||||
const tenantPermissions = currentTenantAccess?.permissions || [];
|
||||
|
||||
const canAccess = canAccessRoute(route, isAuthenticated, userRoles, userPermissions);
|
||||
const canAccess = canAccessRoute(route, isAuthenticated, allUserRoles, tenantPermissions);
|
||||
|
||||
return {
|
||||
canAccess,
|
||||
isLoading,
|
||||
isAuthenticated,
|
||||
userRoles,
|
||||
userPermissions,
|
||||
globalUserRoles,
|
||||
tenantRoles,
|
||||
allUserRoles,
|
||||
tenantPermissions,
|
||||
currentTenantAccess,
|
||||
hasRequiredRoles: !route.requiredRoles ||
|
||||
route.requiredRoles.some(role => userRoles.includes(role)),
|
||||
route.requiredRoles.some(role => allUserRoles.includes(role as string)),
|
||||
hasRequiredPermissions: !route.requiredPermissions ||
|
||||
route.requiredPermissions.every(permission => userPermissions.includes(permission)),
|
||||
route.requiredPermissions.every(permission => tenantPermissions.includes(permission)),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -248,21 +263,25 @@ export const ConditionalRender: React.FC<ConditionalRenderProps> = ({
|
||||
}) => {
|
||||
const user = useAuthUser();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const currentTenantAccess = useCurrentTenantAccess();
|
||||
|
||||
if (!isAuthenticated || !user) {
|
||||
return <>{fallback}</>;
|
||||
}
|
||||
|
||||
const userRoles = user.role ? [user.role] : [];
|
||||
const userPermissions: string[] = [];
|
||||
const globalUserRoles = user.role ? [user.role as string] : [];
|
||||
const tenantRole = currentTenantAccess?.role;
|
||||
const tenantRoles = tenantRole ? [tenantRole as string] : [];
|
||||
const allUserRoles = [...globalUserRoles, ...tenantRoles];
|
||||
const tenantPermissions = currentTenantAccess?.permissions || [];
|
||||
|
||||
// Check roles
|
||||
let hasRoles = true;
|
||||
if (requiredRoles.length > 0) {
|
||||
if (requireAll) {
|
||||
hasRoles = requiredRoles.every(role => userRoles.includes(role));
|
||||
hasRoles = requiredRoles.every(role => allUserRoles.includes(role as string));
|
||||
} else {
|
||||
hasRoles = requiredRoles.some(role => userRoles.includes(role));
|
||||
hasRoles = requiredRoles.some(role => allUserRoles.includes(role as string));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,9 +289,9 @@ export const ConditionalRender: React.FC<ConditionalRenderProps> = ({
|
||||
let hasPermissions = true;
|
||||
if (requiredPermissions.length > 0) {
|
||||
if (requireAll) {
|
||||
hasPermissions = requiredPermissions.every(permission => userPermissions.includes(permission));
|
||||
hasPermissions = requiredPermissions.every(permission => tenantPermissions.includes(permission));
|
||||
} else {
|
||||
hasPermissions = requiredPermissions.some(permission => userPermissions.includes(permission));
|
||||
hasPermissions = requiredPermissions.some(permission => tenantPermissions.includes(permission));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,11 +302,11 @@ export const ConditionalRender: React.FC<ConditionalRenderProps> = ({
|
||||
return <>{fallback}</>;
|
||||
};
|
||||
|
||||
// Route guard for admin-only routes
|
||||
// Route guard for admin-only routes (global admin or tenant owner/admin)
|
||||
export const AdminRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
return (
|
||||
<ConditionalRender
|
||||
requiredRoles={['admin', 'super_admin']}
|
||||
requiredRoles={['admin', 'super_admin', 'owner']}
|
||||
requireAll={false}
|
||||
fallback={<UnauthorizedPage />}
|
||||
>
|
||||
@@ -296,11 +315,24 @@ export const AdminRoute: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
);
|
||||
};
|
||||
|
||||
// Route guard for manager-level routes
|
||||
// Route guard for manager-level routes (global admin/manager or tenant admin/owner)
|
||||
export const ManagerRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
return (
|
||||
<ConditionalRender
|
||||
requiredRoles={['admin', 'super_admin', 'manager']}
|
||||
requiredRoles={['admin', 'super_admin', 'manager', 'owner']}
|
||||
requireAll={false}
|
||||
fallback={<UnauthorizedPage />}
|
||||
>
|
||||
{children}
|
||||
</ConditionalRender>
|
||||
);
|
||||
};
|
||||
|
||||
// Route guard for tenant owner-only routes
|
||||
export const OwnerRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
return (
|
||||
<ConditionalRender
|
||||
requiredRoles={['super_admin', 'owner']}
|
||||
requireAll={false}
|
||||
fallback={<UnauthorizedPage />}
|
||||
>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* Route configuration for the bakery management application
|
||||
*/
|
||||
|
||||
import { ROLE_COMBINATIONS } from '../types/roles';
|
||||
|
||||
export interface RouteConfig {
|
||||
path: string;
|
||||
name: string;
|
||||
@@ -286,7 +288,7 @@ export const routesConfig: RouteConfig[] = [
|
||||
title: 'Analytics',
|
||||
icon: 'sales',
|
||||
requiresAuth: true,
|
||||
requiredRoles: ['admin', 'manager'],
|
||||
requiredRoles: ROLE_COMBINATIONS.MANAGEMENT_ACCESS,
|
||||
showInNavigation: true,
|
||||
children: [
|
||||
{
|
||||
@@ -296,7 +298,7 @@ export const routesConfig: RouteConfig[] = [
|
||||
title: 'Pronósticos',
|
||||
icon: 'forecasting',
|
||||
requiresAuth: true,
|
||||
requiredRoles: ['admin', 'manager'],
|
||||
requiredRoles: ROLE_COMBINATIONS.MANAGEMENT_ACCESS,
|
||||
showInNavigation: true,
|
||||
showInBreadcrumbs: true,
|
||||
},
|
||||
@@ -307,7 +309,7 @@ export const routesConfig: RouteConfig[] = [
|
||||
title: 'Análisis de Ventas',
|
||||
icon: 'sales',
|
||||
requiresAuth: true,
|
||||
requiredRoles: ['admin', 'manager'],
|
||||
requiredRoles: ROLE_COMBINATIONS.MANAGEMENT_ACCESS,
|
||||
showInNavigation: true,
|
||||
showInBreadcrumbs: true,
|
||||
},
|
||||
@@ -318,7 +320,7 @@ export const routesConfig: RouteConfig[] = [
|
||||
title: 'Análisis de Rendimiento',
|
||||
icon: 'sales',
|
||||
requiresAuth: true,
|
||||
requiredRoles: ['admin', 'manager'],
|
||||
requiredRoles: ROLE_COMBINATIONS.MANAGEMENT_ACCESS,
|
||||
showInNavigation: true,
|
||||
showInBreadcrumbs: true,
|
||||
},
|
||||
@@ -329,7 +331,7 @@ export const routesConfig: RouteConfig[] = [
|
||||
title: 'Insights de IA',
|
||||
icon: 'forecasting',
|
||||
requiresAuth: true,
|
||||
requiredRoles: ['admin', 'manager'],
|
||||
requiredRoles: ROLE_COMBINATIONS.MANAGEMENT_ACCESS,
|
||||
showInNavigation: true,
|
||||
showInBreadcrumbs: true,
|
||||
},
|
||||
@@ -363,7 +365,7 @@ export const routesConfig: RouteConfig[] = [
|
||||
title: 'Configuración de Panadería',
|
||||
icon: 'settings',
|
||||
requiresAuth: true,
|
||||
requiredRoles: ['admin'],
|
||||
requiredRoles: ROLE_COMBINATIONS.ADMIN_ACCESS,
|
||||
showInNavigation: true,
|
||||
showInBreadcrumbs: true,
|
||||
},
|
||||
@@ -374,7 +376,7 @@ export const routesConfig: RouteConfig[] = [
|
||||
title: 'Gestión de Equipo',
|
||||
icon: 'settings',
|
||||
requiresAuth: true,
|
||||
requiredRoles: ['admin', 'manager'],
|
||||
requiredRoles: ROLE_COMBINATIONS.ADMIN_ACCESS,
|
||||
showInNavigation: true,
|
||||
showInBreadcrumbs: true,
|
||||
},
|
||||
@@ -385,7 +387,7 @@ export const routesConfig: RouteConfig[] = [
|
||||
title: 'Suscripción y Facturación',
|
||||
icon: 'credit-card',
|
||||
requiresAuth: true,
|
||||
requiredRoles: ['admin', 'owner'],
|
||||
requiredRoles: ROLE_COMBINATIONS.ADMIN_ACCESS,
|
||||
showInNavigation: true,
|
||||
showInBreadcrumbs: true,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
import { GLOBAL_USER_ROLES, type GlobalUserRole } from '../types/roles';
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
@@ -13,7 +14,7 @@ export interface User {
|
||||
language?: string;
|
||||
timezone?: string;
|
||||
tenant_id?: string;
|
||||
role?: string;
|
||||
role?: GlobalUserRole;
|
||||
}
|
||||
|
||||
export interface AuthState {
|
||||
@@ -191,15 +192,22 @@ export const useAuthStore = create<AuthState>()(
|
||||
set({ isLoading: loading });
|
||||
},
|
||||
|
||||
// Permission helpers - Simplified for backend compatibility
|
||||
// Permission helpers - Global user permissions only
|
||||
hasPermission: (_permission: string): boolean => {
|
||||
const { user } = get();
|
||||
if (!user || !user.is_active) return false;
|
||||
|
||||
// Admin has all permissions
|
||||
if (user.role === 'admin') return true;
|
||||
// Super admin and admin have all global permissions
|
||||
if (user.role === GLOBAL_USER_ROLES.SUPER_ADMIN || user.role === GLOBAL_USER_ROLES.ADMIN) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Basic role-based permissions
|
||||
// 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;
|
||||
},
|
||||
|
||||
@@ -212,14 +220,15 @@ export const useAuthStore = create<AuthState>()(
|
||||
const { user } = get();
|
||||
if (!user || !user.is_active) return false;
|
||||
|
||||
// Role-based access control
|
||||
// Global role-based access control (system-wide)
|
||||
switch (user.role) {
|
||||
case 'admin':
|
||||
case GLOBAL_USER_ROLES.SUPER_ADMIN:
|
||||
case GLOBAL_USER_ROLES.ADMIN:
|
||||
return true;
|
||||
case 'manager':
|
||||
return ['inventory', 'production', 'sales', 'reports'].includes(resource);
|
||||
case 'user':
|
||||
return ['inventory', 'sales'].includes(resource) && action === 'read';
|
||||
case GLOBAL_USER_ROLES.MANAGER:
|
||||
return ['users', 'system'].includes(resource);
|
||||
case GLOBAL_USER_ROLES.USER:
|
||||
return action === 'read';
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
import { tenantService, type TenantResponse } from '../api';
|
||||
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;
|
||||
|
||||
@@ -14,6 +16,7 @@ export interface TenantState {
|
||||
setCurrentTenant: (tenant: TenantResponse) => void;
|
||||
switchTenant: (tenantId: string) => Promise<boolean>;
|
||||
loadUserTenants: () => Promise<void>;
|
||||
loadCurrentTenantAccess: () => Promise<void>;
|
||||
clearTenants: () => void;
|
||||
clearError: () => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
@@ -29,14 +32,17 @@ export const useTenantStore = create<TenantState>()(
|
||||
// Initial state
|
||||
currentTenant: null,
|
||||
availableTenants: null,
|
||||
currentTenantAccess: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
// Actions
|
||||
setCurrentTenant: (tenant: TenantResponse) => {
|
||||
set({ currentTenant: tenant });
|
||||
set({ currentTenant: tenant, currentTenantAccess: null });
|
||||
// Update API client with new tenant ID
|
||||
tenantService.setCurrentTenant(tenant);
|
||||
// Load tenant access info
|
||||
get().loadCurrentTenantAccess();
|
||||
},
|
||||
|
||||
switchTenant: async (tenantId: string): Promise<boolean> => {
|
||||
@@ -116,10 +122,25 @@ export const useTenantStore = create<TenantState>()(
|
||||
}
|
||||
},
|
||||
|
||||
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();
|
||||
@@ -135,33 +156,34 @@ export const useTenantStore = create<TenantState>()(
|
||||
|
||||
// Permission helpers (migrated from BakeryContext)
|
||||
hasPermission: (permission: string): boolean => {
|
||||
const { currentTenant } = get();
|
||||
if (!currentTenant) return false;
|
||||
const { currentTenant, currentTenantAccess } = get();
|
||||
if (!currentTenant || !currentTenantAccess || !currentTenantAccess.has_access) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get user to determine role within this tenant
|
||||
const authState = JSON.parse(localStorage.getItem('auth-storage') || '{}')?.state;
|
||||
const user = authState?.user;
|
||||
// Check if user has specific permission in their tenant permissions array
|
||||
if (currentTenantAccess.permissions?.includes(permission)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Admin role has all permissions
|
||||
if (user?.role === 'admin') return true;
|
||||
// Check if user has broader permissions that include this one
|
||||
if (currentTenantAccess.permissions?.includes('*') ||
|
||||
currentTenantAccess.permissions?.includes('admin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: Implement proper tenant-based permissions
|
||||
// For now, use basic role-based permissions
|
||||
switch (user?.role) {
|
||||
case 'admin':
|
||||
// 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 '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');
|
||||
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;
|
||||
}
|
||||
@@ -185,6 +207,7 @@ export const useTenantStore = create<TenantState>()(
|
||||
partialize: (state) => ({
|
||||
currentTenant: state.currentTenant,
|
||||
availableTenants: state.availableTenants,
|
||||
currentTenantAccess: state.currentTenantAccess,
|
||||
}),
|
||||
onRehydrateStorage: () => (state) => {
|
||||
// Initialize API client with stored tenant when store rehydrates
|
||||
@@ -201,6 +224,7 @@ export const useTenantStore = create<TenantState>()(
|
||||
// Selectors for common use cases
|
||||
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);
|
||||
|
||||
@@ -209,6 +233,7 @@ 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,
|
||||
@@ -224,6 +249,7 @@ export const useTenantPermissions = () => useTenantStore((state) => ({
|
||||
export const useTenant = () => {
|
||||
const currentTenant = useCurrentTenant();
|
||||
const availableTenants = useAvailableTenants();
|
||||
const currentTenantAccess = useCurrentTenantAccess();
|
||||
const isLoading = useTenantLoading();
|
||||
const error = useTenantError();
|
||||
const actions = useTenantActions();
|
||||
@@ -232,6 +258,7 @@ export const useTenant = () => {
|
||||
return {
|
||||
currentTenant,
|
||||
availableTenants,
|
||||
currentTenantAccess,
|
||||
isLoading,
|
||||
error,
|
||||
...actions,
|
||||
|
||||
83
frontend/src/types/roles.ts
Normal file
83
frontend/src/types/roles.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Role Types - Must match backend role definitions exactly
|
||||
*/
|
||||
|
||||
// Global User Roles (Auth Service)
|
||||
export const GLOBAL_USER_ROLES = {
|
||||
USER: 'user',
|
||||
ADMIN: 'admin',
|
||||
MANAGER: 'manager',
|
||||
SUPER_ADMIN: 'super_admin',
|
||||
} as const;
|
||||
|
||||
// Tenant-Specific Roles (Tenant Service)
|
||||
export const TENANT_ROLES = {
|
||||
OWNER: 'owner',
|
||||
ADMIN: 'admin',
|
||||
MEMBER: 'member',
|
||||
VIEWER: 'viewer',
|
||||
} as const;
|
||||
|
||||
// Combined role types
|
||||
export type GlobalUserRole = typeof GLOBAL_USER_ROLES[keyof typeof GLOBAL_USER_ROLES];
|
||||
export type TenantRole = typeof TENANT_ROLES[keyof typeof TENANT_ROLES];
|
||||
export type Role = GlobalUserRole | TenantRole;
|
||||
|
||||
// Role hierarchy for permission checking
|
||||
export const ROLE_HIERARCHY = {
|
||||
// Global roles (highest to lowest)
|
||||
global: [
|
||||
GLOBAL_USER_ROLES.SUPER_ADMIN,
|
||||
GLOBAL_USER_ROLES.ADMIN,
|
||||
GLOBAL_USER_ROLES.MANAGER,
|
||||
GLOBAL_USER_ROLES.USER,
|
||||
],
|
||||
// Tenant roles (highest to lowest)
|
||||
tenant: [
|
||||
TENANT_ROLES.OWNER,
|
||||
TENANT_ROLES.ADMIN,
|
||||
TENANT_ROLES.MEMBER,
|
||||
TENANT_ROLES.VIEWER,
|
||||
],
|
||||
} as const;
|
||||
|
||||
// Permission helper functions
|
||||
export const hasGlobalRole = (userRole: string, requiredRole: GlobalUserRole): boolean => {
|
||||
const userIndex = ROLE_HIERARCHY.global.indexOf(userRole as GlobalUserRole);
|
||||
const requiredIndex = ROLE_HIERARCHY.global.indexOf(requiredRole);
|
||||
return userIndex !== -1 && requiredIndex !== -1 && userIndex <= requiredIndex;
|
||||
};
|
||||
|
||||
export const hasTenantRole = (userRole: string, requiredRole: TenantRole): boolean => {
|
||||
const userIndex = ROLE_HIERARCHY.tenant.indexOf(userRole as TenantRole);
|
||||
const requiredIndex = ROLE_HIERARCHY.tenant.indexOf(requiredRole);
|
||||
return userIndex !== -1 && requiredIndex !== -1 && userIndex <= requiredIndex;
|
||||
};
|
||||
|
||||
export const hasAnyRole = (userRoles: string[], requiredRoles: Role[]): boolean => {
|
||||
return requiredRoles.some(requiredRole => userRoles.includes(requiredRole));
|
||||
};
|
||||
|
||||
// Common role combinations for easy reuse
|
||||
export const ROLE_COMBINATIONS = {
|
||||
// Administrative access (global admin or tenant owner)
|
||||
ADMIN_ACCESS: [GLOBAL_USER_ROLES.ADMIN, GLOBAL_USER_ROLES.SUPER_ADMIN, TENANT_ROLES.OWNER],
|
||||
|
||||
// Management access (admin + manager + tenant admin)
|
||||
MANAGEMENT_ACCESS: [
|
||||
GLOBAL_USER_ROLES.ADMIN,
|
||||
GLOBAL_USER_ROLES.SUPER_ADMIN,
|
||||
GLOBAL_USER_ROLES.MANAGER,
|
||||
TENANT_ROLES.OWNER,
|
||||
TENANT_ROLES.ADMIN,
|
||||
],
|
||||
|
||||
// Owner-only access (super admin or tenant owner)
|
||||
OWNER_ACCESS: [GLOBAL_USER_ROLES.SUPER_ADMIN, TENANT_ROLES.OWNER],
|
||||
|
||||
// Basic access (any authenticated user with any role)
|
||||
BASIC_ACCESS: [
|
||||
...Object.values(GLOBAL_USER_ROLES),
|
||||
...Object.values(TENANT_ROLES),
|
||||
],
|
||||
} as const;
|
||||
@@ -132,7 +132,7 @@ class SecurityManager:
|
||||
if "role" in user_data:
|
||||
payload["role"] = user_data["role"]
|
||||
else:
|
||||
payload["role"] = "user" # Default role if not specified
|
||||
payload["role"] = "admin" # Default role if not specified
|
||||
|
||||
logger.debug(f"Creating access token with payload keys: {list(payload.keys())}")
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ class UserRegistration(BaseModel):
|
||||
password: str = Field(..., min_length=8, max_length=128)
|
||||
full_name: str = Field(..., min_length=1, max_length=255)
|
||||
tenant_name: Optional[str] = Field(None, max_length=255)
|
||||
role: Optional[str] = Field("user", pattern=r'^(user|admin|manager)$')
|
||||
role: Optional[str] = Field("admin", pattern=r'^(user|admin|manager|super_admin)$')
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
"""User login request"""
|
||||
@@ -56,7 +56,7 @@ class UserData(BaseModel):
|
||||
is_verified: bool
|
||||
created_at: str # ISO format datetime string
|
||||
tenant_id: Optional[str] = None
|
||||
role: Optional[str] = "user"
|
||||
role: Optional[str] = "admin"
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
"""
|
||||
@@ -101,7 +101,7 @@ class UserResponse(BaseModel):
|
||||
language: Optional[str] = None # ✅ Added missing field
|
||||
timezone: Optional[str] = None # ✅ Added missing field
|
||||
tenant_id: Optional[str] = None
|
||||
role: Optional[str] = "user"
|
||||
role: Optional[str] = "admin"
|
||||
|
||||
class Config:
|
||||
from_attributes = True # ✅ Enable ORM mode for SQLAlchemy objects
|
||||
@@ -189,7 +189,7 @@ class UserContext(BaseModel):
|
||||
user_id: str
|
||||
email: str
|
||||
tenant_id: Optional[str] = None
|
||||
roles: list[str] = ["user"]
|
||||
roles: list[str] = ["admin"]
|
||||
is_verified: bool = False
|
||||
|
||||
class TokenClaims(BaseModel):
|
||||
|
||||
@@ -55,7 +55,9 @@ class EnhancedAuthService:
|
||||
raise ValueError("Password does not meet security requirements")
|
||||
|
||||
# Create user data
|
||||
user_role = user_data.role if user_data.role else "user"
|
||||
# Default to admin role for first-time registrations during onboarding flow
|
||||
# Users creating their own bakery should have admin privileges
|
||||
user_role = user_data.role if user_data.role else "admin"
|
||||
hashed_password = SecurityManager.hash_password(user_data.password)
|
||||
|
||||
create_data = {
|
||||
|
||||
@@ -413,7 +413,7 @@ class EnhancedUserService:
|
||||
user_repo = UserRepository(User, session)
|
||||
|
||||
# Validate role
|
||||
valid_roles = ["user", "admin", "super_admin"]
|
||||
valid_roles = ["user", "admin", "manager", "super_admin"]
|
||||
if new_role not in valid_roles:
|
||||
raise ValidationError(f"Invalid role. Must be one of: {valid_roles}")
|
||||
|
||||
|
||||
@@ -67,6 +67,32 @@ async def register_bakery_enhanced(
|
||||
detail="Bakery registration failed"
|
||||
)
|
||||
|
||||
@router.get("/tenants/{tenant_id}/my-access", response_model=TenantAccessResponse)
|
||||
async def get_current_user_tenant_access(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep)
|
||||
):
|
||||
"""Get current user's access to tenant with role and permissions"""
|
||||
|
||||
try:
|
||||
# Create tenant service directly
|
||||
from app.core.config import settings
|
||||
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
|
||||
tenant_service = EnhancedTenantService(database_manager)
|
||||
|
||||
access_info = await tenant_service.verify_user_access(current_user["user_id"], str(tenant_id))
|
||||
return access_info
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Current user access verification failed",
|
||||
user_id=current_user["user_id"],
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Access verification failed"
|
||||
)
|
||||
|
||||
@router.get("/tenants/{tenant_id}/access/{user_id}", response_model=TenantAccessResponse)
|
||||
async def verify_tenant_access_enhanced(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
|
||||
Reference in New Issue
Block a user