Imporve the role based forntend protected roles

This commit is contained in:
Urtzi Alfaro
2025-09-09 07:32:59 +02:00
parent ddb75f8e55
commit 5269a083b6
15 changed files with 286 additions and 91 deletions

View File

@@ -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();

View File

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

View File

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

View File

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

View File

@@ -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) => {

View File

@@ -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 />}
>

View File

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

View File

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

View File

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

View 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;