440 lines
13 KiB
TypeScript
440 lines
13 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;
|
|
}): Promise<{ success: boolean; error?: string }> {
|
|
try {
|
|
const response = await fetch('https://agilite.slm-lab.net/api/jsonrpc', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${process.env.LEANTIME_TOKEN}`,
|
|
},
|
|
body: JSON.stringify({
|
|
method: 'leantime.rpc.Users.Users.addUser',
|
|
jsonrpc: '2.0',
|
|
id: 1,
|
|
params: {
|
|
values: {
|
|
firstname: userData.firstName,
|
|
lastname: userData.lastName,
|
|
username: userData.username,
|
|
email: userData.email,
|
|
status: 'active',
|
|
role: 'user', // Default role in Leantime
|
|
}
|
|
}
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok || !data.result) {
|
|
console.error('Leantime user creation failed:', data);
|
|
return {
|
|
success: false,
|
|
error: '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,
|
|
});
|
|
|
|
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 }
|
|
);
|
|
}
|
|
}
|