573 lines
17 KiB
TypeScript
573 lines
17 KiB
TypeScript
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: {
|
|
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
|
|
}
|
|
|
|
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 delete user from Leantime
|
|
async function deleteLeantimeUser(email: 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.deleteUser',
|
|
jsonrpc: '2.0',
|
|
id: 1,
|
|
params: {
|
|
email: email
|
|
}
|
|
})
|
|
});
|
|
|
|
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 }
|
|
);
|
|
}
|
|
|
|
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 }
|
|
);
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
return NextResponse.json({ success: true });
|
|
|
|
} catch (error) {
|
|
console.error("Error deleting user:", error);
|
|
return NextResponse.json(
|
|
{ error: "Erreur serveur", details: error },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|