diff --git a/DEPRECATED_FUNCTIONS.md b/DEPRECATED_FUNCTIONS.md index 588b01cb..ef244117 100644 --- a/DEPRECATED_FUNCTIONS.md +++ b/DEPRECATED_FUNCTIONS.md @@ -95,6 +95,12 @@ Use these functions for all email formatting needs. - **Status**: Removed - **Replacement**: Use `app/api/courrier/send/route.ts` instead. +### 4. `DELETE /api/users/[userId]` (DEPRECATED) +- **Status**: Deprecated but maintained for backward compatibility +- **Replacement**: Use `DELETE /api/users?id=[userId]&email=[userEmail]` instead +- **Reason**: The new endpoint format supports deletion across all integrated systems (Keycloak, Leantime, and Dolibarr) +- **Notes**: The deprecated endpoint now forwards requests to the new endpoint but developers should update their code to use the new format directly + ## Deprecated Components ### ComposeEmail (components/ComposeEmail.tsx) (REMOVED) diff --git a/README.md b/README.md index 8a549111..550c914d 100644 --- a/README.md +++ b/README.md @@ -79,4 +79,30 @@ This centralized approach prevents formatting inconsistencies and direction prob Several functions have been deprecated and removed in favor of centralized implementations: -- Check the `DEPRECATED_FUNCTIONS.md` file for a complete list of deprecated functions and their replacements. \ No newline at end of file +- Check the `DEPRECATED_FUNCTIONS.md` file for a complete list of deprecated functions and their replacements. + +## User Management API + +The application provides endpoints for managing users in multiple systems: + +- **Create User**: + - Endpoint: `POST /api/users` + - Creates users in Keycloak, Leantime, and Dolibarr (if they have "mediation" or "expression" roles) + +- **Update User**: + - Endpoint: `PUT /api/users/[userId]` + - Updates user details in Keycloak + +- **Delete User**: + - Endpoint: `DELETE /api/users?id=[userId]&email=[userEmail]` + - Deletes users from Keycloak, Leantime, and Dolibarr systems + - **Important**: Always include both `id` and `email` parameters for complete deletion across all systems + - The legacy endpoint `DELETE /api/users/[userId]` forwards to the above endpoint + +- **Manage Roles**: + - Endpoint: `PUT /api/users/[userId]/roles` + - Updates user roles in Keycloak + +- **Reset Password**: + - Endpoint: `PUT /api/users/[userId]/password` + - Resets user password in Keycloak \ No newline at end of file diff --git a/app/api/users/[userId]/route.ts b/app/api/users/[userId]/route.ts index febfabc2..38fc6da2 100644 --- a/app/api/users/[userId]/route.ts +++ b/app/api/users/[userId]/route.ts @@ -173,39 +173,35 @@ export async function DELETE( const userDetails = await userResponse.json(); console.log('Processing user deletion for ID:', params.userId); - - // Delete user from Keycloak - const deleteResponse = await fetch( - `${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users/${params.userId}`, - { - method: "DELETE", - headers: { - Authorization: `Bearer ${tokenData.access_token}`, - }, - } - ); - - if (!deleteResponse.ok) { - console.error("Keycloak delete error"); + + // 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', params.userId); + apiUrl.searchParams.append('email', userDetails.email); + + const forwardResponse = await fetch(apiUrl.toString(), { + method: "DELETE", + headers: { + "Cookie": req.headers.get('cookie') || '', + "Authorization": req.headers.get('authorization') || '', + }, + }); + + if (!forwardResponse.ok) { + const errorData = await forwardResponse.json(); + console.error("Error forwarding delete request:", errorData); return NextResponse.json( - { error: "Erreur lors de la suppression de l'utilisateur" }, - { status: deleteResponse.status } + { error: "Erreur lors de la suppression de l'utilisateur", details: errorData }, + { status: forwardResponse.status } ); } - - // Delete user from Leantime - const leantimeResult = await deleteLeantimeUser(userDetails.email, session.user.id); - - if (!leantimeResult.success) { - console.error("Leantime user deletion failed"); - // We don't return an error here since Keycloak user was deleted successfully - // We just log the error and continue - } - - return NextResponse.json({ success: true }); + + const responseData = await forwardResponse.json(); + return NextResponse.json(responseData); } catch (error) { - console.error("Error deleting user"); + console.error("Error deleting user:", error); return NextResponse.json( { error: "Erreur serveur" }, { status: 500 } diff --git a/scripts/test-user-deletion.js b/scripts/test-user-deletion.js new file mode 100644 index 00000000..5b805045 --- /dev/null +++ b/scripts/test-user-deletion.js @@ -0,0 +1,273 @@ +/** + * Test script for verifying user deletion across all integrated systems + * + * This script creates a test user with mediation role, verifies it exists in all systems, + * then deletes it and verifies deletion in all systems. + * + * Usage: node scripts/test-user-deletion.js + */ + +require('dotenv').config(); +const fetch = require('node-fetch'); + +// Test user configuration +const TEST_USER = { + username: `test-user-${Date.now()}`, + firstName: 'Test', + lastName: 'User', + email: `test-user-${Date.now()}@example.com`, + password: 'password123', + roles: ['mediation'] // Using mediation role which should be created in Dolibarr +}; + +// Configuration from environment variables +const config = { + keycloak: { + baseUrl: process.env.KEYCLOAK_BASE_URL, + realm: process.env.KEYCLOAK_REALM, + clientId: process.env.KEYCLOAK_CLIENT_ID, + clientSecret: process.env.KEYCLOAK_CLIENT_SECRET + }, + leantime: { + apiUrl: 'https://agilite.slm-lab.net/api/jsonrpc', + apiKey: process.env.LEANTIME_TOKEN + }, + dolibarr: { + apiUrl: process.env.DOLIBARR_API_URL, + apiKey: process.env.DOLIBARR_API_KEY + }, + nextAuthUrl: process.env.NEXTAUTH_URL || 'http://localhost:3000' +}; + +// Helper to get admin token for Keycloak operations +async function getKeycloakAdminToken() { + const response = await fetch( + `${config.keycloak.baseUrl}/realms/${config.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: config.keycloak.clientId, + client_secret: config.keycloak.clientSecret, + }), + } + ); + + const data = await response.json(); + if (!response.ok || !data.access_token) { + throw new Error('Failed to get Keycloak admin token'); + } + + return data.access_token; +} + +// Create a test user in all systems via the API +async function createTestUser() { + console.log(`Creating test user: ${TEST_USER.username} (${TEST_USER.email})`); + + const response = await fetch(`${config.nextAuthUrl}/api/users`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(TEST_USER), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(`Failed to create test user: ${JSON.stringify(error)}`); + } + + const data = await response.json(); + console.log('User created successfully:', data); + return data.user; +} + +// Check if user exists in Keycloak +async function checkKeycloakUser(userId) { + console.log(`Checking if user exists in Keycloak (ID: ${userId})`); + const token = await getKeycloakAdminToken(); + + const response = await fetch( + `${config.keycloak.baseUrl}/admin/realms/${config.keycloak.realm}/users/${userId}`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + + if (response.status === 404) { + console.log('User not found in Keycloak'); + return false; + } + + if (!response.ok) { + console.error('Error checking Keycloak user:', await response.text()); + return null; // Error state + } + + console.log('User exists in Keycloak'); + return true; +} + +// Check if user exists in Leantime +async function checkLeantimeUser(email) { + console.log(`Checking if user exists in Leantime (Email: ${email})`); + + try { + const response = await fetch(config.leantime.apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': config.leantime.apiKey, + }, + body: JSON.stringify({ + method: 'leantime.rpc.Users.Users.getUserIdByEmail', + jsonrpc: '2.0', + id: 1, + params: { + email: email + } + }) + }); + + const data = await response.json(); + if (!response.ok || !data.result) { + console.log('User not found in Leantime'); + return false; + } + + console.log('User exists in Leantime'); + return true; + } catch (error) { + console.error('Error checking Leantime user:', error); + return null; // Error state + } +} + +// Check if user exists in Dolibarr +async function checkDolibarrUser(email) { + console.log(`Checking if user exists in Dolibarr (Email: ${email})`); + + try { + const apiUrl = config.dolibarr.apiUrl.endsWith('/') + ? config.dolibarr.apiUrl + : `${config.dolibarr.apiUrl}/`; + + const response = await fetch( + `${apiUrl}users?sortfield=t.rowid&sortorder=ASC&limit=1&sqlfilters=(t.email:=:'${encodeURIComponent(email)}')`, + { + method: 'GET', + headers: { + 'DOLAPIKEY': config.dolibarr.apiKey, + }, + } + ); + + if (!response.ok) { + console.error('Error response from Dolibarr:', await response.text()); + return null; // Error state + } + + const data = await response.json(); + if (!Array.isArray(data) || data.length === 0) { + console.log('User not found in Dolibarr'); + return false; + } + + console.log('User exists in Dolibarr with ID:', data[0].id); + return { exists: true, id: data[0].id }; + } catch (error) { + console.error('Error checking Dolibarr user:', error); + return null; // Error state + } +} + +// Delete user from all systems via the API +async function deleteTestUser(userId, email) { + console.log(`Deleting test user: ID=${userId}, Email=${email}`); + + const response = await fetch( + `${config.nextAuthUrl}/api/users?id=${userId}&email=${encodeURIComponent(email)}`, + { + method: 'DELETE', + } + ); + + if (!response.ok) { + const error = await response.json(); + throw new Error(`Failed to delete test user: ${JSON.stringify(error)}`); + } + + console.log('User deletion request successful'); + return await response.json(); +} + +// Main test function +async function runTest() { + try { + console.log('=== STARTING USER DELETION TEST ==='); + + // Step 1: Create a test user + console.log('\n=== Step 1: Creating test user ==='); + const createdUser = await createTestUser(); + console.log(`Test user created with ID: ${createdUser.id}`); + + // Wait a moment for systems to process + console.log('Waiting for systems to process...'); + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Step 2: Verify user exists in all systems + console.log('\n=== Step 2: Verifying user exists in all systems ==='); + const keycloakExists = await checkKeycloakUser(createdUser.id); + const leantimeExists = await checkLeantimeUser(TEST_USER.email); + const dolibarrUser = await checkDolibarrUser(TEST_USER.email); + + if (keycloakExists === null || leantimeExists === null || dolibarrUser === null) { + throw new Error('Error checking user existence in integrated systems'); + } + + if (!keycloakExists || !leantimeExists || !dolibarrUser.exists) { + throw new Error('User not created in all systems properly'); + } + + console.log('User confirmed to exist in all integrated systems'); + + // Step 3: Delete the test user + console.log('\n=== Step 3: Deleting test user ==='); + await deleteTestUser(createdUser.id, TEST_USER.email); + + // Wait a moment for systems to process + console.log('Waiting for systems to process deletion...'); + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Step 4: Verify user has been deleted from all systems + console.log('\n=== Step 4: Verifying user deletion from all systems ==='); + const keycloakDeleted = !(await checkKeycloakUser(createdUser.id)); + const leantimeDeleted = !(await checkLeantimeUser(TEST_USER.email)); + const dolibarrDeleted = !(await checkDolibarrUser(TEST_USER.email)).exists; + + console.log('\n=== TEST RESULTS ==='); + console.log(`Keycloak user deleted: ${keycloakDeleted ? 'YES' : 'NO'}`); + console.log(`Leantime user deleted: ${leantimeDeleted ? 'YES' : 'NO'}`); + console.log(`Dolibarr user deleted: ${dolibarrDeleted ? 'YES' : 'NO'}`); + + if (keycloakDeleted && leantimeDeleted && dolibarrDeleted) { + console.log('\n✅ TEST PASSED: User successfully deleted from all systems'); + } else { + console.log('\n❌ TEST FAILED: User not deleted from all systems'); + } + + } catch (error) { + console.error('\n❌ TEST ERROR:', error); + } + + console.log('\n=== TEST COMPLETED ==='); +} + +// Run the test +runTest(); \ No newline at end of file