import { getServerSession } from "next-auth/next"; import { authOptions } from "@/app/api/auth/options"; import { NextResponse } from "next/server"; import { createDolibarrUser, checkDolibarrUserExists, deleteDolibarrUser } from "@/lib/dolibarr-api"; export async function GET() { const session = await getServerSession(authOptions); if (!session) { return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); } console.log("Session:", { accessToken: session.accessToken?.substring(0, 20) + "...", user: session.user, }); try { // Get client credentials token const tokenResponse = await fetch( `${process.env.KEYCLOAK_BASE_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'client_credentials', client_id: process.env.KEYCLOAK_CLIENT_ID!, client_secret: process.env.KEYCLOAK_CLIENT_SECRET!, }), } ); const tokenData = await tokenResponse.json(); console.log("Token response:", { ok: tokenResponse.ok, status: tokenResponse.status, data: tokenData.access_token ? "Token received" : tokenData, }); if (!tokenResponse.ok) { console.error("Failed to get token:", tokenData); return NextResponse.json([getCurrentUser(session)]); } // Get users list with brief=false to get full user details const usersResponse = await fetch( `${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users?briefRepresentation=false`, { headers: { Authorization: `Bearer ${tokenData.access_token}`, 'Content-Type': 'application/json', }, } ); if (!usersResponse.ok) { console.error("Failed to fetch users:", await usersResponse.text()); return NextResponse.json([getCurrentUser(session)]); } const users = await usersResponse.json(); console.log("Raw users data:", users.map((u: any) => ({ id: u.id, username: u.username, realm: u.realm, serviceAccountClientId: u.serviceAccountClientId, }))); // Filter out service accounts and users from other realms const filteredUsers = users.filter((user: any) => !user.serviceAccountClientId && // Remove service accounts (!user.realm || user.realm === process.env.KEYCLOAK_REALM) // Only users from our realm ); console.log("Filtered users count:", filteredUsers.length); // Fetch roles for each user const usersWithRoles = await Promise.all(filteredUsers.map(async (user: any) => { try { const rolesResponse = await fetch( `${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users/${user.id}/role-mappings/realm`, { headers: { Authorization: `Bearer ${tokenData.access_token}`, 'Content-Type': 'application/json', }, } ); let roles = []; if (rolesResponse.ok) { const rolesData = await rolesResponse.json(); roles = rolesData .filter((role: any) => !role.name.startsWith('default-roles-') && !['offline_access', 'uma_authorization'].includes(role.name) ) .map((role: any) => role.name); console.log(`Roles for user ${user.username}:`, roles); } return { id: user.id, username: user.username, firstName: user.firstName || '', lastName: user.lastName || '', email: user.email, createdTimestamp: user.createdTimestamp, enabled: user.enabled, roles: roles, }; } catch (error) { console.error(`Error fetching roles for user ${user.id}:`, error); return { id: user.id, username: user.username, firstName: user.firstName || '', lastName: user.lastName || '', email: user.email, createdTimestamp: user.createdTimestamp, enabled: user.enabled, roles: [], }; } })); console.log("Final users data:", usersWithRoles.map(u => ({ username: u.username, roles: u.roles, }))); return NextResponse.json(usersWithRoles); } catch (error) { console.error("Error:", error); return NextResponse.json([getCurrentUser(session)]); } } // Helper function to get current user data function getCurrentUser(session: any) { return { id: session.user.id, username: session.user.username, firstName: session.user.first_name, lastName: session.user.last_name, email: session.user.email, createdTimestamp: Date.now(), roles: session.user.role || [], }; } async function getAdminToken() { try { const tokenResponse = await fetch( `${process.env.KEYCLOAK_BASE_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'client_credentials', client_id: process.env.KEYCLOAK_CLIENT_ID!, client_secret: process.env.KEYCLOAK_CLIENT_SECRET!, }), } ); const data = await tokenResponse.json(); if (!tokenResponse.ok || !data.access_token) { console.error('Token Error:', data); return null; } return data.access_token; } catch (error) { console.error('Token Error:', error); return null; } } // Validate username according to Keycloak requirements function validateUsername(username: string): { isValid: boolean; error?: string } { // Keycloak username requirements: // - Only alphanumeric characters, dots (.), hyphens (-), and underscores (_) // - Must start with a letter or number // - Must be between 3 and 255 characters const usernameRegex = /^[a-zA-Z0-9][a-zA-Z0-9._-]{2,254}$/; if (!usernameRegex.test(username)) { return { isValid: false, error: "Le nom d'utilisateur doit commencer par une lettre ou un chiffre, ne contenir que des lettres, chiffres, points, tirets et underscores, et faire entre 3 et 255 caractères" }; } return { isValid: true }; } // Helper function to create user in Leantime async function createLeantimeUser(userData: { username: string; firstName: string; lastName: string; email: string; password: string; }): Promise<{ success: boolean; error?: string }> { try { const response = await fetch('https://agilite.slm-lab.net/api/jsonrpc', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': process.env.LEANTIME_TOKEN || '', }, body: JSON.stringify({ method: 'leantime.rpc.Users.Users.addUser', jsonrpc: '2.0', id: 1, params: { values: { '0': 0, // This will be set by Leantime '1': userData.lastName, '2': userData.firstName, '3': '20', // Default role '4': '', // profileId '5': 'a', // status '6': userData.email, '7': 0, // twoFAEnabled '8': 0, // clientId '9': null, // clientName '10': '', // jobTitle '11': '', // jobLevel '12': '', // department '13': new Date().toISOString(), // modified lastname: userData.lastName, firstname: userData.firstName, role: '20', // Default role profileId: '', status: 'a', username: userData.email, password: userData.password, twoFAEnabled: 0, clientId: 0, clientName: null, jobTitle: '', jobLevel: '', department: '', modified: new Date().toISOString(), createdOn: new Date().toISOString(), source: 'keycloak', notifications: 1, settings: '{}' } } }) }); const data = await response.json(); console.log('Leantime response:', data); if (!response.ok || !data.result) { console.error('Leantime user creation failed:', data); return { success: false, error: data.error?.message || 'Failed to create user in Leantime' }; } return { success: true }; } catch (error) { console.error('Error creating Leantime user:', error); return { success: false, error: 'Error creating user in Leantime' }; } } export async function POST(req: Request) { const session = await getServerSession(authOptions); if (!session) { return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); } try { const data = await req.json(); console.log("Creating user:", data); // Validate username const usernameValidation = validateUsername(data.username); if (!usernameValidation.isValid) { return NextResponse.json( { error: usernameValidation.error }, { status: 400 } ); } const token = await getAdminToken(); if (!token) { return NextResponse.json({ error: "Erreur d'authentification" }, { status: 401 }); } // First, get all available roles from Keycloak const rolesResponse = await fetch( `${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/roles`, { headers: { Authorization: `Bearer ${token}`, }, } ); if (!rolesResponse.ok) { const errorData = await rolesResponse.json(); console.error("Failed to fetch roles:", errorData); return NextResponse.json({ error: "Erreur lors de la récupération des rôles" }, { status: rolesResponse.status }); } const availableRoles = await rolesResponse.json(); console.log("Available roles:", availableRoles); // Verify that the requested roles exist const requestedRoles = data.roles || []; const validRoles = requestedRoles.filter((roleName: string) => availableRoles.some((r: any) => r.name === roleName) ); if (validRoles.length === 0) { return NextResponse.json( { error: "Aucun rôle valide n'a été spécifié" }, { status: 400 } ); } // Create the user in Keycloak const createResponse = await fetch( `${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ username: data.username, enabled: true, emailVerified: true, firstName: data.firstName, lastName: data.lastName, email: data.email, credentials: [ { type: "password", value: data.password, temporary: false, }, ], }), } ); console.log("Keycloak create response:", { status: createResponse.status, ok: createResponse.ok }); if (!createResponse.ok) { const errorData = await createResponse.json(); console.error("Keycloak error:", errorData); if (errorData.errorMessage?.includes("User exists with same username")) { return NextResponse.json( { error: "Un utilisateur existe déjà avec ce nom d'utilisateur" }, { status: 400 } ); } else if (errorData.errorMessage?.includes("User exists with same email")) { return NextResponse.json( { error: "Un utilisateur existe déjà avec cet email" }, { status: 400 } ); } return NextResponse.json( { error: "Erreur création utilisateur", details: errorData }, { status: 400 } ); } // Get the created user const userResponse = await fetch( `${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users?username=${data.username}`, { headers: { Authorization: `Bearer ${token}`, }, } ); const users = await userResponse.json(); const user = users[0]; if (!user) { return NextResponse.json( { error: "Utilisateur créé mais impossible de le récupérer" }, { status: 500 } ); } // Add roles to the user const roleObjects = validRoles.map((roleName: string) => availableRoles.find((r: any) => r.name === roleName) ); const roleResponse = await fetch( `${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users/${user.id}/role-mappings/realm`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify(roleObjects), } ); if (!roleResponse.ok) { const errorData = await roleResponse.json(); console.error("Failed to add roles:", errorData); return NextResponse.json( { error: "Erreur lors de l'ajout des rôles", details: errorData }, { status: 500 } ); } // Create user in Leantime const leantimeResult = await createLeantimeUser({ username: data.username, firstName: data.firstName, lastName: data.lastName, email: data.email, password: data.password, }); if (!leantimeResult.success) { console.error("Leantime user creation failed:", leantimeResult.error); // We don't return an error here since Keycloak user was created successfully // We just log the error and continue } // Add detailed diagnostic logging console.log('=== DOLIBARR INTEGRATION DIAGNOSTICS ==='); console.log('Role check values:', { allRoles: validRoles, exactCase: { hasMediationExact: validRoles.includes('Mediation'), hasExpressionExact: validRoles.includes('Expression') }, lowerCase: { hasMediationLower: validRoles.includes('mediation'), hasExpressionLower: validRoles.includes('expression') } }); console.log('Environment variables:', { dolibarrUrlExists: !!process.env.DOLIBARR_API_URL, dolibarrUrl: process.env.DOLIBARR_API_URL ? `${process.env.DOLIBARR_API_URL.substring(0, 10)}...` : 'undefined', dolibarrKeyExists: !!process.env.DOLIBARR_API_KEY, dolibarrKeyFirstChars: process.env.DOLIBARR_API_KEY ? `${process.env.DOLIBARR_API_KEY.substring(0, 5)}...` : 'undefined' }); // Check if the user has mediation or expression role and create in Dolibarr if needed const hasMediationRole = validRoles.includes('Mediation'); const hasExpressionRole = validRoles.includes('Expression'); console.log('Role check results:', { hasMediationRole, hasExpressionRole, shouldCreateInDolibarr: hasMediationRole || hasExpressionRole }); let dolibarrUserId = null; if (hasMediationRole || hasExpressionRole) { console.log(`User has special role (mediation: ${hasMediationRole}, expression: ${hasExpressionRole}), creating in Dolibarr`); try { // First check if the user already exists in Dolibarr console.log('Checking if user already exists in Dolibarr with email:', data.email); const existingUser = await checkDolibarrUserExists(data.email); if (existingUser.exists) { console.log(`User already exists in Dolibarr with ID: ${existingUser.id}`); dolibarrUserId = existingUser.id; } else { // Create user account in Dolibarr console.log('Creating new user account in Dolibarr with data:', { username: data.username, email: data.email, name: `${data.firstName} ${data.lastName}` }); const dolibarrResult = await createDolibarrUser({ username: data.username, firstName: data.firstName, lastName: data.lastName, email: data.email, password: data.password, }); if (dolibarrResult.success) { console.log(`User account created in Dolibarr with ID: ${dolibarrResult.id}`); dolibarrUserId = dolibarrResult.id; } else { console.error("Dolibarr user account creation failed:", dolibarrResult.error); // We don't return an error here since Keycloak user was created successfully // We just log the error and continue } } } catch (dolibarrError) { console.error('Unexpected error during Dolibarr integration:', dolibarrError); } } else { console.log('User does not have mediation or expression role, skipping Dolibarr creation'); } console.log('=== END DOLIBARR INTEGRATION DIAGNOSTICS ==='); return NextResponse.json({ success: true, user: { ...user, roles: validRoles, }, }); } catch (error) { console.error("Error creating user:", error); return NextResponse.json( { error: "Erreur serveur", details: error }, { status: 500 } ); } } // Helper function to get Leantime user ID by email async function getLeantimeUserId(email: string): Promise { try { // Validate email format const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { console.error('Invalid email format'); return null; } // Get user by email using the proper method const userResponse = await fetch('https://agilite.slm-lab.net/api/jsonrpc', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': process.env.LEANTIME_TOKEN || '', }, body: JSON.stringify({ method: 'leantime.rpc.Users.Users.getUserByEmail', jsonrpc: '2.0', id: 1, params: { email: email } }) }); const userData = await userResponse.json(); console.log('Leantime user lookup response status:', userResponse.status); if (!userResponse.ok || !userData.result) { console.error('Failed to get Leantime user'); return null; } // The result should be the user object or false if (userData.result === false) { return null; } return userData.result.id; } catch (error) { console.error('Error getting Leantime user'); return null; } } // Helper function to delete user from Leantime async function deleteLeantimeUser(email: string): Promise<{ success: boolean; error?: string }> { try { // First get the Leantime user ID const leantimeUserId = await getLeantimeUserId(email); if (!leantimeUserId) { return { success: false, error: 'User not found in Leantime' }; } console.log(`Found Leantime user with ID: ${leantimeUserId}, proceeding with deletion`); const response = await fetch('https://agilite.slm-lab.net/api/jsonrpc', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': process.env.LEANTIME_TOKEN || '', }, body: JSON.stringify({ method: 'leantime.rpc.Users.Users.deleteUser', jsonrpc: '2.0', id: 1, params: { id: leantimeUserId } }) }); const data = await response.json(); console.log('Leantime delete response:', data); if (!response.ok || !data.result) { console.error('Leantime user deletion failed:', data); return { success: false, error: data.error?.message || 'Failed to delete user in Leantime' }; } return { success: true }; } catch (error) { console.error('Error deleting Leantime user:', error); return { success: false, error: 'Error deleting user in Leantime' }; } } export async function DELETE(req: Request) { const session = await getServerSession(authOptions); if (!session) { return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); } try { const { searchParams } = new URL(req.url); const userId = searchParams.get('id'); const email = searchParams.get('email'); if (!userId || !email) { return NextResponse.json( { error: "ID utilisateur et email requis" }, { status: 400 } ); } console.log(`Deleting user: ID=${userId}, email=${email}`); const token = await getAdminToken(); if (!token) { return NextResponse.json({ error: "Erreur d'authentification" }, { status: 401 }); } // Delete user from Keycloak const deleteResponse = await fetch( `${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users/${userId}`, { method: "DELETE", headers: { Authorization: `Bearer ${token}`, }, } ); if (!deleteResponse.ok) { const errorData = await deleteResponse.json(); console.error("Keycloak delete error:", errorData); return NextResponse.json( { error: "Erreur lors de la suppression de l'utilisateur", details: errorData }, { status: deleteResponse.status } ); } console.log("Successfully deleted user from Keycloak"); // Delete user from Leantime const leantimeResult = await deleteLeantimeUser(email); if (!leantimeResult.success) { console.error("Leantime user deletion failed:", leantimeResult.error); // We don't return an error here since Keycloak user was deleted successfully // We just log the error and continue } else { console.log("Successfully deleted user from Leantime"); } // Check if user exists in Dolibarr and delete if found console.log(`Checking if user exists in Dolibarr with email: ${email}`); try { const dolibarrUser = await checkDolibarrUserExists(email); if (dolibarrUser.exists && dolibarrUser.id) { console.log(`User found in Dolibarr with ID: ${dolibarrUser.id}. Proceeding with deletion.`); const dolibarrResult = await deleteDolibarrUser(dolibarrUser.id); if (!dolibarrResult.success) { console.error("Dolibarr user deletion failed:", dolibarrResult.error); // We don't return an error here since Keycloak user was deleted successfully // We just log the error and continue } else { console.log(`Successfully deleted user from Dolibarr with ID: ${dolibarrUser.id}`); } } else { console.log("User not found in Dolibarr, skipping Dolibarr deletion"); } } catch (dolibarrError) { console.error("Error during Dolibarr user deletion:", dolibarrError); // Continue despite Dolibarr errors } return NextResponse.json({ success: true, message: "User deleted from all systems" }); } catch (error) { console.error("Error deleting user:", error); return NextResponse.json( { error: "Erreur serveur", details: error }, { status: 500 } ); } }