diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index 14ee41bc..4d8f54e6 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -60,15 +60,58 @@ export const authOptions: NextAuthOptions = { clientSecret: process.env.KEYCLOAK_CLIENT_SECRET || "", issuer: process.env.KEYCLOAK_ISSUER || "", profile(profile: any) { - // Extract roles from the profile - const roles = profile.realm_access?.roles || []; + // Debug the raw profile from Keycloak to understand its structure + console.log('Raw Keycloak profile:', JSON.stringify(profile, null, 2)); + + // Extract roles from realm_access.roles and resource_access + let roles: string[] = []; + + // Get roles from realm_access + if (profile.realm_access && Array.isArray(profile.realm_access.roles)) { + roles = roles.concat(profile.realm_access.roles); + } + + // Get roles from resource_access for the client + if (profile.resource_access) { + const clientId = process.env.KEYCLOAK_CLIENT_ID; + if (clientId && profile.resource_access[clientId] && Array.isArray(profile.resource_access[clientId].roles)) { + roles = roles.concat(profile.resource_access[clientId].roles); + } + + // Also check resource_access roles under 'account' + if (profile.resource_access.account && Array.isArray(profile.resource_access.account.roles)) { + roles = roles.concat(profile.resource_access.account.roles); + } + } + + // Extract groups if available + if (profile.groups && Array.isArray(profile.groups)) { + // Remove any path prefixes (like "/") and add as roles + const groupRoles = profile.groups.map((group: string) => + group.replace(/^\//, '').toLowerCase() + ); + roles = roles.concat(groupRoles); + } + + // Clean up roles and convert to lowercase + const cleanedRoles = roles + .filter(Boolean) // Remove empty roles + .map((role: string) => + role.replace(/^ROLE_/, '').toLowerCase() + ); + + // Add some common application-specific role mappings + const applicationRoles = mapToApplicationRoles(cleanedRoles); + const allRoles = [...new Set([...cleanedRoles, ...applicationRoles, 'user'])]; + + console.log('Extracted roles:', allRoles); return { id: profile.sub, name: profile.name || profile.preferred_username, email: profile.email, image: null, - role: roles.map((role: string) => role.replace(/^ROLE_/, '').toLowerCase()), + role: allRoles, first_name: profile.given_name || '', last_name: profile.family_name || '', username: profile.preferred_username || profile.email?.split('@')[0] || '', @@ -87,23 +130,24 @@ export const authOptions: NextAuthOptions = { token.accessToken = account.access_token; token.refreshToken = account.refresh_token; - // Extract roles correctly from the raw Keycloak profile - if (profile.realm_access && profile.realm_access.roles) { - // Directly extract roles from the Keycloak profile structure - token.role = profile.realm_access.roles.map( - (role: string) => role.replace(/^ROLE_/, '').toLowerCase() - ); - } else if (profile.role) { - // Fallback to using the role property if already processed + // Use the roles from the profile function + if (profile.role && Array.isArray(profile.role)) { token.role = profile.role; + console.log('JWT callback - roles from profile:', profile.role); + } else { + // Fallback for missing roles + token.role = ['user']; + console.log('JWT callback - no roles in profile, using fallback'); } // Store user information - token.username = profile.preferred_username || profile.username || ''; - token.first_name = profile.given_name || profile.first_name || ''; - token.last_name = profile.family_name || profile.last_name || ''; + token.username = profile.username || ''; + token.first_name = profile.first_name || ''; + token.last_name = profile.last_name || ''; } + // Log the token roles + console.log('JWT token roles:', token.role); return token; }, async session({ session, token }: any) { @@ -118,15 +162,17 @@ export const authOptions: NextAuthOptions = { session.user.username = token.username || ''; session.user.first_name = token.first_name || ''; session.user.last_name = token.last_name || ''; + console.log('Session callback - using token roles:', token.role); } else { // Fallback roles session.user.role = ["user"]; session.user.username = ''; session.user.first_name = ''; session.user.last_name = ''; + console.log('Session callback - no token roles, using fallback'); } - // Add debug log to see what roles are being passed + // Log the session user roles console.log('Session user roles:', session.user.role); } return session; @@ -147,9 +193,64 @@ export const authOptions: NextAuthOptions = { }, }, }, - debug: true, // Enable debug logs temporarily to see role information + debug: true, // Enable debug logs to help with troubleshooting }; +/** + * Maps Keycloak roles to application-specific roles + */ +function mapToApplicationRoles(keycloakRoles: string[]): string[] { + const mappings: Record = { + // Map Keycloak roles to your application's role names + 'admin': ['admin', 'dataintelligence', 'coding', 'expression', 'mediation'], + 'owner': ['admin', 'dataintelligence', 'coding', 'expression', 'mediation'], + 'cercle-admin': ['admin', 'dataintelligence', 'coding', 'expression', 'mediation'], + 'manager': ['dataintelligence', 'coding', 'expression', 'mediation'], + 'developer': ['coding', 'dataintelligence'], + 'data-scientist': ['dataintelligence'], + 'designer': ['expression'], + 'writer': ['expression'], + 'mediator': ['mediation'], + // Default access roles from Keycloak + 'default-roles-cercle': ['user'], + 'uma_authorization': ['user'], + 'offline_access': ['user'], + // Add more mappings as needed + }; + + // Convert all keycloak roles to lowercase for case-insensitive matching + const lowerKeycloakRoles = keycloakRoles.map(role => role.toLowerCase()); + + // Map roles based on the defined mappings + let applicationRoles: string[] = []; + + // Check all Keycloak roles for matches in our mapping + for (const role of lowerKeycloakRoles) { + if (mappings[role]) { + applicationRoles = applicationRoles.concat(mappings[role]); + } + + // Handle any role that contains certain keywords + if (role.includes('admin')) { + applicationRoles.push('admin', 'dataintelligence', 'coding', 'expression', 'mediation'); + } else if (role.includes('developer') || role.includes('dev')) { + applicationRoles.push('coding', 'dataintelligence'); + } else if (role.includes('design')) { + applicationRoles.push('expression'); + } else if (role.includes('data')) { + applicationRoles.push('dataintelligence'); + } else if (role.includes('mediat')) { + applicationRoles.push('mediation'); + } + } + + // Ensure user always has basic access + applicationRoles.push('user'); + + // Return unique application roles + return [...new Set(applicationRoles)]; +} + const handler = NextAuth(authOptions); export { handler as GET, handler as POST }; diff --git a/components/sidebar.tsx b/components/sidebar.tsx index 8f2dd26d..0c4b41ac 100644 --- a/components/sidebar.tsx +++ b/components/sidebar.tsx @@ -171,28 +171,28 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) { icon: Palette, href: "/design", iframe: process.env.NEXT_PUBLIC_IFRAME_ARTLAB_URL, - requiredRole: "Expression", + requiredRole: "expression", }, { title: "Gite", icon: GitFork, href: "/gite", iframe: process.env.NEXT_PUBLIC_IFRAME_GITE_URL, - requiredRole: ["Coding", "DataIntelligence"], + requiredRole: ["coding", "dataintelligence"], }, { title: "Calcul", icon: Calculator, href: "/calcul", iframe: process.env.NEXT_PUBLIC_IFRAME_CALCULATION_URL, - requiredRole: "DataIntelligence", + requiredRole: "dataintelligence", }, { title: "Médiation", icon: Building2, href: "/mediation", iframe: process.env.NEXT_PUBLIC_IFRAME_MEDIATIONS_URL, - requiredRole: ["Mediation", "Expression"], + requiredRole: ["mediation", "expression"], }, ];