import { getServerSession } from "next-auth/next"; import { authOptions } from "@/app/api/auth/[...nextauth]/route"; import { NextResponse } from "next/server"; 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: { firstName: string; lastName: string; email: string; password: string; roles: 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({ firstName: data.firstName, lastName: data.lastName, email: data.email, password: data.password, roles: validRoles, }); 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 } 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 } ); } }