diff --git a/frontend/src/api/services/tenant.ts b/frontend/src/api/services/tenant.ts index 2be8708a..a7aefba6 100644 --- a/frontend/src/api/services/tenant.ts +++ b/frontend/src/api/services/tenant.ts @@ -54,6 +54,12 @@ export class TenantService { return apiClient.get(`${this.baseUrl}/${tenantId}/access/${userId}`); } + async getCurrentUserTenantAccess(tenantId: string): Promise { + // This will use the current user from the auth token + // The backend endpoint handles extracting user_id from the token + return apiClient.get(`${this.baseUrl}/${tenantId}/my-access`); + } + // Search & Discovery async searchTenants(params: TenantSearchParams): Promise { const queryParams = new URLSearchParams(); diff --git a/frontend/src/api/types/auth.ts b/frontend/src/api/types/auth.ts index 13cdac3f..4f4dfec9 100644 --- a/frontend/src/api/types/auth.ts +++ b/frontend/src/api/types/auth.ts @@ -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; } diff --git a/frontend/src/api/types/tenant.ts b/frontend/src/api/types/tenant.ts index 0f191b03..33c045bd 100644 --- a/frontend/src/api/types/tenant.ts +++ b/frontend/src/api/types/tenant.ts @@ -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; diff --git a/frontend/src/components/domain/auth/LoginForm.tsx b/frontend/src/components/domain/auth/LoginForm.tsx index b115a75e..bb1ca853 100644 --- a/frontend/src/components/domain/auth/LoginForm.tsx +++ b/frontend/src/components/domain/auth/LoginForm.tsx @@ -36,7 +36,7 @@ export const LoginForm: React.FC = ({ 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 = ({ 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' }); } }; diff --git a/frontend/src/components/layout/Sidebar/Sidebar.tsx b/frontend/src/components/layout/Sidebar/Sidebar.tsx index 6921180e..9cbfd1c3 100644 --- a/frontend/src/components/layout/Sidebar/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar/Sidebar.tsx @@ -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(({ const navigate = useNavigate(); const user = useAuthUser(); const isAuthenticated = useIsAuthenticated(); + const currentTenantAccess = useCurrentTenantAccess(); const [expandedItems, setExpandedItems] = useState>(new Set()); const [hoveredItem, setHoveredItem] = useState(null); @@ -160,8 +162,12 @@ export const Sidebar = forwardRef(({ ...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(({ requiredPermissions: item.requiredPermissions } as any, isAuthenticated, - userRoles, - userPermissions + allUserRoles, + tenantPermissions ); return hasAccess; @@ -180,7 +186,7 @@ export const Sidebar = forwardRef(({ }; return filterItemsByPermissions(navigationItems); - }, [navigationItems, isAuthenticated, user]); + }, [navigationItems, isAuthenticated, user, currentTenantAccess]); // Handle item click const handleItemClick = useCallback((item: NavigationItem) => { diff --git a/frontend/src/router/ProtectedRoute.tsx b/frontend/src/router/ProtectedRoute.tsx index 36b9fad3..a8f41273 100644 --- a/frontend/src/router/ProtectedRoute.tsx +++ b/frontend/src/router/ProtectedRoute.tsx @@ -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 = ({ 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 = ({ } // 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 ; @@ -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 = ({ }) => { 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 = ({ 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 = ({ 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 ( } > @@ -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 ( } + > + {children} + + ); +}; + +// Route guard for tenant owner-only routes +export const OwnerRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => { + return ( + } > diff --git a/frontend/src/router/routes.config.ts b/frontend/src/router/routes.config.ts index d17a7642..2db243fe 100644 --- a/frontend/src/router/routes.config.ts +++ b/frontend/src/router/routes.config.ts @@ -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, }, diff --git a/frontend/src/stores/auth.store.ts b/frontend/src/stores/auth.store.ts index e6fed9ca..b98cef58 100644 --- a/frontend/src/stores/auth.store.ts +++ b/frontend/src/stores/auth.store.ts @@ -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()( 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()( 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; } diff --git a/frontend/src/stores/tenant.store.ts b/frontend/src/stores/tenant.store.ts index ca7f832a..a78be834 100644 --- a/frontend/src/stores/tenant.store.ts +++ b/frontend/src/stores/tenant.store.ts @@ -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; loadUserTenants: () => Promise; + loadCurrentTenantAccess: () => Promise; clearTenants: () => void; clearError: () => void; setLoading: (loading: boolean) => void; @@ -29,14 +32,17 @@ export const useTenantStore = create()( // 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 => { @@ -116,10 +122,25 @@ export const useTenantStore = create()( } }, + loadCurrentTenantAccess: async (): Promise => { + 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()( // 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()( 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()( // 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, diff --git a/frontend/src/types/roles.ts b/frontend/src/types/roles.ts new file mode 100644 index 00000000..4681e60a --- /dev/null +++ b/frontend/src/types/roles.ts @@ -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; \ No newline at end of file diff --git a/services/auth/app/core/security.py b/services/auth/app/core/security.py index fd8c4d0c..83f1fd87 100644 --- a/services/auth/app/core/security.py +++ b/services/auth/app/core/security.py @@ -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())}") diff --git a/services/auth/app/schemas/auth.py b/services/auth/app/schemas/auth.py index 4c171d85..a3ac0276 100644 --- a/services/auth/app/schemas/auth.py +++ b/services/auth/app/schemas/auth.py @@ -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): diff --git a/services/auth/app/services/auth_service.py b/services/auth/app/services/auth_service.py index 8f95d34d..3d024b74 100644 --- a/services/auth/app/services/auth_service.py +++ b/services/auth/app/services/auth_service.py @@ -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 = { diff --git a/services/auth/app/services/user_service.py b/services/auth/app/services/user_service.py index 09c495d3..dd55a688 100644 --- a/services/auth/app/services/user_service.py +++ b/services/auth/app/services/user_service.py @@ -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}") diff --git a/services/tenant/app/api/tenants.py b/services/tenant/app/api/tenants.py index d74f6b07..66612080 100644 --- a/services/tenant/app/api/tenants.py +++ b/services/tenant/app/api/tenants.py @@ -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"),