diff --git a/app/api/users/[userId]/roles/route.ts b/app/api/users/[userId]/roles/route.ts index ad021cb8..16ca7552 100644 --- a/app/api/users/[userId]/roles/route.ts +++ b/app/api/users/[userId]/roles/route.ts @@ -2,18 +2,14 @@ import { NextResponse } from "next/server"; import { getServerSession } from "next-auth"; import { authOptions } from "@/app/api/auth/[...nextauth]/route"; import { getKeycloakAdminClient } from "@/lib/keycloak"; +import { RoleRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation"; -// Fix for Next.js "params should be awaited" error - temporarily disabled -// export const dynamic = 'force-dynamic'; - -// @ts-ignore - disable dynamic route params warning -export const config = { - runtime: 'nodejs' -}; +// Fix for Next.js "params should be awaited" error +export const dynamic = 'force-dynamic'; export async function GET( request: Request, - { params }: { params: { userId: string } } + { params }: { params: { userId?: string } } ) { try { const session = await getServerSession(authOptions); @@ -21,21 +17,58 @@ export async function GET( return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const { userId } = params; - const kcAdminClient = await getKeycloakAdminClient(); - - // Get all available roles - const availableRoles = await kcAdminClient.roles.find(); + // Safely extract userId from params + const rawUserId = params?.userId; + const userId = typeof rawUserId === 'string' ? rawUserId : ''; - // Get user's current roles - const userRoles = await kcAdminClient.users.listRoleMappings({ - id: userId, - }); + if (!userId) { + return NextResponse.json({ error: "User ID is required" }, { status: 400 }); + } - return NextResponse.json({ - availableRoles, - userRoles, - }); + try { + // Check for required environment variables before attempting to connect + const missingVars = []; + if (!process.env.KEYCLOAK_BASE_URL && !process.env.KEYCLOAK_ISSUER && !process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER) { + missingVars.push('KEYCLOAK_BASE_URL or KEYCLOAK_ISSUER'); + } + if (!process.env.KEYCLOAK_ADMIN_CLIENT_ID) missingVars.push('KEYCLOAK_ADMIN_CLIENT_ID'); + if (!process.env.KEYCLOAK_ADMIN_USERNAME) missingVars.push('KEYCLOAK_ADMIN_USERNAME'); + if (!process.env.KEYCLOAK_ADMIN_PASSWORD) missingVars.push('KEYCLOAK_ADMIN_PASSWORD'); + if (!process.env.KEYCLOAK_REALM) missingVars.push('KEYCLOAK_REALM'); + + if (missingVars.length > 0) { + console.error(`Missing Keycloak environment variables: ${missingVars.join(', ')}`); + return NextResponse.json( + { + error: "Keycloak configuration incomplete", + message: "Role management is currently unavailable due to missing configuration.", + details: `Missing: ${missingVars.join(', ')}` + }, + { status: 503 } + ); + } + + const kcAdminClient = await getKeycloakAdminClient(); + + // Get all available roles + const availableRoles = await kcAdminClient.roles.find(); + + // Get user's current roles + const userRoles = await kcAdminClient.users.listRoleMappings({ + id: userId, + }); + + return NextResponse.json({ + availableRoles, + userRoles, + }); + } catch (keycloakError) { + console.error("Error connecting to Keycloak:", keycloakError); + return NextResponse.json( + { error: "Failed to connect to Keycloak service", details: String(keycloakError) }, + { status: 503 } + ); + } } catch (error) { console.error("Error fetching roles:", error); return NextResponse.json( @@ -47,7 +80,7 @@ export async function GET( export async function PUT( request: Request, - { params }: { params: { userId: string } } + { params }: { params: { userId?: string } } ) { try { const session = await getServerSession(authOptions); @@ -55,46 +88,83 @@ export async function PUT( return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const { userId } = params; - const { roles } = await request.json(); - const kcAdminClient = await getKeycloakAdminClient(); - - // Get all available roles - const availableRoles = await kcAdminClient.roles.find(); + // Safely extract userId from params + const rawUserId = params?.userId; + const userId = typeof rawUserId === 'string' ? rawUserId : ''; - // Get current user roles - const currentRoles = await kcAdminClient.users.listRoleMappings({ - id: userId, - }); + if (!userId) { + return NextResponse.json({ error: "User ID is required" }, { status: 400 }); + } + + try { + // Check for required environment variables before attempting to connect + const missingVars = []; + if (!process.env.KEYCLOAK_BASE_URL && !process.env.KEYCLOAK_ISSUER && !process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER) { + missingVars.push('KEYCLOAK_BASE_URL or KEYCLOAK_ISSUER'); + } + if (!process.env.KEYCLOAK_ADMIN_CLIENT_ID) missingVars.push('KEYCLOAK_ADMIN_CLIENT_ID'); + if (!process.env.KEYCLOAK_ADMIN_USERNAME) missingVars.push('KEYCLOAK_ADMIN_USERNAME'); + if (!process.env.KEYCLOAK_ADMIN_PASSWORD) missingVars.push('KEYCLOAK_ADMIN_PASSWORD'); + if (!process.env.KEYCLOAK_REALM) missingVars.push('KEYCLOAK_REALM'); + + if (missingVars.length > 0) { + console.error(`Missing Keycloak environment variables: ${missingVars.join(', ')}`); + return NextResponse.json( + { + error: "Keycloak configuration incomplete", + message: "Role management is currently unavailable due to missing configuration.", + details: `Missing: ${missingVars.join(', ')}` + }, + { status: 503 } + ); + } + + const { roles } = await request.json(); + const kcAdminClient = await getKeycloakAdminClient(); - // Find roles to add and remove - const rolesToAdd = roles.filter( - (role: string) => !currentRoles.realmMappings?.some((r: any) => r.name === role) - ); - const rolesToRemove = currentRoles.realmMappings?.filter( - (role: any) => !roles.includes(role.name) - ); + // Get all available roles + const availableRoles = await kcAdminClient.roles.find(); + + // Get current user roles + const currentRoles = await kcAdminClient.users.listRoleMappings({ + id: userId, + }); - // Add new roles - for (const roleName of rolesToAdd) { - const role = availableRoles.find((r: any) => r.name === roleName); - if (role) { - await kcAdminClient.users.addRealmRoleMappings({ + // Find roles to add and remove + const rolesToAdd = roles.filter( + (role: string) => !currentRoles.realmMappings?.some((r: any) => r.name === role) + ); + const rolesToRemove = currentRoles.realmMappings?.filter( + (role: any) => !roles.includes(role.name) + ); + + // Add new roles + for (const roleName of rolesToAdd) { + const role = availableRoles.find((r: any) => r.name === roleName); + if (role) { + await kcAdminClient.users.addRealmRoleMappings({ + id: userId, + roles: [role as any], + }); + } + } + + // Remove old roles + if (rolesToRemove && rolesToRemove.length > 0) { + await kcAdminClient.users.delRealmRoleMappings({ id: userId, - roles: [role as any], + roles: rolesToRemove as any, }); } - } - // Remove old roles - if (rolesToRemove && rolesToRemove.length > 0) { - await kcAdminClient.users.delRealmRoleMappings({ - id: userId, - roles: rolesToRemove as any, - }); + return NextResponse.json({ success: true }); + } catch (keycloakError) { + console.error("Error connecting to Keycloak:", keycloakError); + return NextResponse.json( + { error: "Failed to connect to Keycloak service", details: String(keycloakError) }, + { status: 503 } + ); } - - return NextResponse.json({ success: true }); } catch (error) { console.error("Error updating roles:", error); return NextResponse.json( diff --git a/components/sidebar.tsx b/components/sidebar.tsx index 8f2dd26d..81265f88 100644 --- a/components/sidebar.tsx +++ b/components/sidebar.tsx @@ -71,46 +71,66 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) { // Function to check if user has a specific role const hasRole = (requiredRole: string | string[] | undefined) => { - if (!requiredRole || !session?.user?.role) { - console.log('No required role or user roles found', { - requiredRole, - userRoles: session?.user?.role - }); + // If no role is required, allow access + if (!requiredRole) { + return true; + } + + // If no session or user roles, deny access + if (!session?.user?.role) { + console.log('No user roles found in session'); return false; } + // Get user roles and normalize them properly const userRoles = Array.isArray(session.user.role) ? session.user.role : [session.user.role]; - const cleanUserRoles = userRoles.map(role => role.toLowerCase()); - console.log('Debug roles:', { - rawUserRoles: session.user.role, - processedUserRoles: cleanUserRoles, - requiredRole, - pathname - }); + // Filter out technical/system roles that shouldn't count for permissions + const ignoredRoles = ['offline_access', 'uma_authorization', 'default-roles-cercle']; - // If requiredRole is an array, check if user has any of the roles - if (Array.isArray(requiredRole)) { - const cleanRequiredRoles = requiredRole.map(role => role.toLowerCase()); - console.log('Checking multiple roles:', { - requiredRoles: requiredRole, - cleanRequiredRoles, - userRoles: cleanUserRoles, - hasAnyRole: cleanRequiredRoles.some(role => cleanUserRoles.includes(role)), - matchingRoles: cleanRequiredRoles.filter(role => cleanUserRoles.includes(role)) + const cleanUserRoles = userRoles + .filter(Boolean) // Remove any null/undefined values + .filter(role => !ignoredRoles.includes(String(role))) // Filter out system roles + .map(role => { + if (typeof role !== 'string') return ''; + return role + .replace(/^\//, '') // Remove leading slash + .replace(/^ROLE_/i, '') // Remove ROLE_ prefix, case insensitive + .replace(/^default-roles-[^/]*\//i, '') // Remove realm prefix like default-roles-cercle/ + .toLowerCase(); + }) + .filter(role => role !== ''); // Remove empty strings + + // For debugging only + if (process.env.NODE_ENV === 'development') { + console.log(`Role check for: ${JSON.stringify(requiredRole)}`, { + userRoles, + ignoredRoles, + cleanUserRoles, }); - return cleanRequiredRoles.some(role => cleanUserRoles.includes(role)); } - // For single role requirement - const cleanRequiredRole = requiredRole.toLowerCase(); - console.log('Checking single role:', { - requiredRole, - cleanRequiredRole, - userRoles: cleanUserRoles, - hasRole: cleanUserRoles.includes(cleanRequiredRole) - }); - return cleanUserRoles.includes(cleanRequiredRole); + // Check against array of required roles + if (Array.isArray(requiredRole)) { + const cleanRequiredRoles = requiredRole + .filter(Boolean) + .map(role => typeof role === 'string' ? role.toLowerCase() : '') + .filter(role => role !== ''); + + const hasRequiredRole = cleanRequiredRoles.some(role => cleanUserRoles.includes(role)); + console.log(`Array role check: Required ${JSON.stringify(cleanRequiredRoles)}, Has any: ${hasRequiredRole}`); + return hasRequiredRole; + } + + // Check against single required role + if (typeof requiredRole === 'string') { + const cleanRequiredRole = requiredRole.toLowerCase(); + const hasRequiredRole = cleanUserRoles.includes(cleanRequiredRole); + console.log(`Single role check: Required "${cleanRequiredRole}", Has: ${hasRequiredRole}`); + return hasRequiredRole; + } + + return false; }; // Base menu items (available for everyone) @@ -171,35 +191,39 @@ 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"], }, ]; // Combine base items with role-specific items based on user roles const visibleMenuItems = [ ...baseMenuItems, - ...roleSpecificItems.filter(item => hasRole(item.requiredRole)) + ...roleSpecificItems.filter(item => { + const isVisible = hasRole(item.requiredRole); + console.log(`Item ${item.title} with requiredRole ${JSON.stringify(item.requiredRole)} is ${isVisible ? 'visible' : 'hidden'}`); + return isVisible; + }) ]; const handleNavigation = (href: string, external?: boolean) => { @@ -264,6 +288,57 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) { {item.title} ))} + + {/* Debug display only in development */} + {process.env.NODE_ENV === 'development' && ( +
+

Debug Info:

+
+

User: {session?.user?.name}

+

Email: {session?.user?.email}

+
+ User Roles +
+                      {JSON.stringify(session?.user?.role, null, 2)}
+                    
+
+
+ Normalized Roles +
+                      {JSON.stringify(
+                        Array.isArray(session?.user?.role) 
+                          ? session.user.role
+                              .filter(role => typeof role === 'string')
+                              .filter(role => !['offline_access', 'uma_authorization', 'default-roles-cercle'].includes(role))
+                              .map(role => 
+                                role
+                                  .replace(/^\//, '')
+                                  .replace(/^ROLE_/i, '')
+                                  .replace(/^default-roles-[^/]*\//i, '')
+                                  .toLowerCase()
+                              )
+                          : []
+                        , null, 2)}
+                    
+
+
+ Role-Based Items +
+ {roleSpecificItems.map(item => { + const isVisible = hasRole(item.requiredRole); + return ( +
+

Item: {item.title}

+

Required Role: {JSON.stringify(item.requiredRole)}

+

Visible: {isVisible ? '✅' : '❌'}

+
+ ); + })} +
+
+
+
+ )} diff --git a/lib/keycloak.ts b/lib/keycloak.ts index 3a19ebf4..9ddb155e 100644 --- a/lib/keycloak.ts +++ b/lib/keycloak.ts @@ -28,6 +28,23 @@ export async function getKeycloakAdminClient(): Promise { const adminPassword = process.env.KEYCLOAK_ADMIN_PASSWORD; const realmName = process.env.KEYCLOAK_REALM; + // Validate required environment variables + if (!keycloakUrl) { + console.error('Missing Keycloak URL. Please add one of these to your .env file: KEYCLOAK_BASE_URL, KEYCLOAK_ISSUER, or NEXT_PUBLIC_KEYCLOAK_ISSUER'); + throw new Error('Missing Keycloak URL configuration'); + } + + if (!adminClientId || !adminUsername || !adminPassword || !realmName) { + const missing = []; + if (!adminClientId) missing.push('KEYCLOAK_ADMIN_CLIENT_ID'); + if (!adminUsername) missing.push('KEYCLOAK_ADMIN_USERNAME'); + if (!adminPassword) missing.push('KEYCLOAK_ADMIN_PASSWORD'); + if (!realmName) missing.push('KEYCLOAK_REALM'); + + console.error(`Missing Keycloak admin credentials in .env: ${missing.join(', ')}`); + throw new Error('Missing Keycloak admin credentials'); + } + console.log(`Connecting to Keycloak at ${keycloakUrl}, realm: ${realmName}`); try {