import { getServerSession } from "next-auth/next"; import { authOptions } from "@/app/api/auth/options"; import { NextResponse } from "next/server"; import { logger } from '@/lib/logger'; import { fetchWithTimeout, fetchJsonWithTimeout } from '@/lib/utils/fetch-with-timeout'; // 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)) { logger.error('[LEANTIME] Invalid email format', { email: email.substring(0, 5) + '***' }); return null; } // Get user by email using the proper method const userData = await fetchJsonWithTimeout('https://agilite.slm-lab.net/api/jsonrpc', { method: 'POST', timeout: 10000, // 10 seconds 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 } }) }); // Only log minimal information for debugging logger.debug('[LEANTIME] User lookup completed', { hasResult: !!userData.result }); if (!userData.result) { logger.warn('[LEANTIME] User not found', { email: email.substring(0, 5) + '***' }); return null; } // The result should be the user object or false if (userData.result === false) { return null; } return userData.result.id; } catch (error) { logger.error('[LEANTIME] Error getting user', { error: error instanceof Error ? error.message : String(error) }); return null; } } // Helper function to delete user from Leantime async function deleteLeantimeUser(email: string, requestingUserId: 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' }; } // Check if the requesting user has permission to delete this user // This is a placeholder - implement proper permission check based on your requirements if (!(await hasDeletePermission(requestingUserId, leantimeUserId))) { return { success: false, error: 'Unauthorized to delete this user' }; } const data = await fetchJsonWithTimeout('https://agilite.slm-lab.net/api/jsonrpc', { method: 'POST', timeout: 10000, // 10 seconds 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 } }) }); // Only log minimal information logger.debug('[LEANTIME] Delete user completed', { success: !!data.result }); if (!data.result) { return { success: false, error: 'Failed to delete user in Leantime' }; } return { success: true }; } catch (error) { logger.error('[LEANTIME] Error deleting user', { error: error instanceof Error ? error.message : String(error) }); return { success: false, error: 'Error deleting user in Leantime' }; } } // Placeholder for permission check - implement based on your requirements async function hasDeletePermission(requestingUserId: string, targetUserId: number): Promise { // Implement proper permission check logic here // For example: // 1. Check if requesting user is admin // 2. Check if requesting user is trying to delete themselves // 3. Check if requesting user has the right role/permissions return true; } export async function DELETE(req: Request, props: { params: Promise<{ userId: string }> }) { const params = await props.params; const session = await getServerSession(authOptions); const userId = params.userId; // Store userId from params to avoid sync access issues if (!session) { return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); } try { // Get admin token const tokenData = await fetchJsonWithTimeout( `${process.env.KEYCLOAK_BASE_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`, { method: 'POST', timeout: 10000, // 10 seconds 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!, }), } ); if (!tokenData.access_token) { logger.error('[KEYCLOAK] Failed to get admin token'); return NextResponse.json({ error: "Erreur d'authentification" }, { status: 401 }); } // Get user details before deletion const userDetails = await fetchJsonWithTimeout( `${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users/${userId}`, { timeout: 10000, // 10 seconds headers: { Authorization: `Bearer ${tokenData.access_token}`, }, } ); logger.debug('[USER_DELETE] Processing user deletion', { userId }); // Forward the request to the new endpoint format with the email parameter // This ensures Dolibarr deletion is also handled const apiUrl = new URL(`${req.headers.get('origin') || process.env.NEXTAUTH_URL}/api/users`); apiUrl.searchParams.append('id', userId); apiUrl.searchParams.append('email', userDetails.email); const forwardResponse = await fetchWithTimeout(apiUrl.toString(), { method: "DELETE", timeout: 30000, // 30 seconds headers: { "Cookie": req.headers.get('cookie') || '', "Authorization": req.headers.get('authorization') || '', }, }); if (!forwardResponse.ok) { const errorData = await forwardResponse.json(); logger.error('[USER_DELETE] Error forwarding delete request', { errorData }); return NextResponse.json( { error: "Erreur lors de la suppression de l'utilisateur", details: errorData }, { status: forwardResponse.status } ); } const responseData = await forwardResponse.json(); return NextResponse.json(responseData); } catch (error) { logger.error('[USER_DELETE] Error deleting user', { error: error instanceof Error ? error.message : String(error), userId }); return NextResponse.json( { error: "Erreur serveur" }, { status: 500 } ); } } export async function PUT(req: Request, props: { params: Promise<{ userId: string }> }) { const params = await props.params; const session = await getServerSession(authOptions); if (!session) { return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); } try { const body = await req.json(); // Get client credentials token const tokenData = await fetchJsonWithTimeout( `${process.env.KEYCLOAK_BASE_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`, { method: 'POST', timeout: 10000, // 10 seconds 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!, }), } ); if (!tokenData.access_token) { logger.error('[KEYCLOAK] Failed to get token', { tokenData }); return NextResponse.json({ error: "Failed to get token" }, { status: 500 }); } // Update user const updateResponse = await fetchWithTimeout( `${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users/${params.userId}`, { method: 'PUT', timeout: 10000, // 10 seconds headers: { 'Authorization': `Bearer ${tokenData.access_token}`, 'Content-Type': 'application/json', }, body: JSON.stringify(body), } ); if (!updateResponse.ok) { const errorData = await updateResponse.json(); logger.error('[KEYCLOAK] Failed to update user', { errorData, userId: params.userId }); return NextResponse.json({ error: "Failed to update user" }, { status: updateResponse.status }); } return NextResponse.json({ success: true }); } catch (error) { logger.error('[USER_UPDATE] Error updating user', { error: error instanceof Error ? error.message : String(error), userId: params.userId }); return NextResponse.json({ error: "Internal server error" }, { status: 500 }); } }