377 lines
12 KiB
TypeScript
377 lines
12 KiB
TypeScript
import { getServerSession } from "next-auth/next";
|
|
import { authOptions } from "@/app/api/auth/options";
|
|
import { NextResponse } from "next/server";
|
|
import { logger } from '@/lib/logger';
|
|
import { prisma } from '@/lib/prisma';
|
|
|
|
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();
|
|
|
|
// Log the response for debugging (without exposing the full token!)
|
|
logger.debug('Token Response', {
|
|
status: tokenResponse.status,
|
|
ok: tokenResponse.ok,
|
|
hasToken: !!data.access_token,
|
|
expiresIn: data.expires_in
|
|
});
|
|
|
|
if (!tokenResponse.ok || !data.access_token) {
|
|
// Log the error details (without sensitive data)
|
|
logger.error('Token Error Details', {
|
|
status: tokenResponse.status,
|
|
error: data.error || 'Unknown error'
|
|
});
|
|
return null;
|
|
}
|
|
|
|
return data.access_token;
|
|
} catch (error) {
|
|
logger.error('Token Error', {
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function GET() {
|
|
try {
|
|
const session = await getServerSession(authOptions);
|
|
if (!session) {
|
|
return NextResponse.json({ message: "Non autorisé" }, { status: 401 });
|
|
}
|
|
|
|
const token = await getAdminToken();
|
|
if (!token) {
|
|
return NextResponse.json({ message: "Erreur d'authentification" }, { status: 401 });
|
|
}
|
|
|
|
const response = await fetch(
|
|
`${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/groups`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
return NextResponse.json({ message: "Échec de la récupération des groupes" }, { status: response.status });
|
|
}
|
|
|
|
const groups = await response.json();
|
|
|
|
// Return empty array if no groups
|
|
if (!Array.isArray(groups)) {
|
|
return NextResponse.json([]);
|
|
}
|
|
|
|
const groupsWithCounts = await Promise.all(
|
|
groups.map(async (group: any) => {
|
|
try {
|
|
// Récupérer tous les membres du groupe avec pagination pour gérer les grands groupes
|
|
// L'endpoint /members/count ne fonctionne pas correctement, on récupère donc la liste
|
|
let totalCount = 0;
|
|
let first = 0;
|
|
const max = 100; // Taille de page pour la pagination
|
|
let hasMore = true;
|
|
|
|
while (hasMore) {
|
|
const membersResponse = await fetch(
|
|
`${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/groups/${group.id}/members?briefRepresentation=true&first=${first}&max=${max}`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
}
|
|
);
|
|
|
|
if (membersResponse.ok) {
|
|
const members = await membersResponse.json();
|
|
const membersArray = Array.isArray(members) ? members : [];
|
|
totalCount += membersArray.length;
|
|
|
|
// Si on a récupéré moins que max, on a tout récupéré
|
|
if (membersArray.length < max) {
|
|
hasMore = false;
|
|
} else {
|
|
first += max;
|
|
}
|
|
} else {
|
|
// Si l'endpoint échoue, essayer l'endpoint count en fallback
|
|
try {
|
|
const countResponse = await fetch(
|
|
`${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/groups/${group.id}/members/count`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
}
|
|
);
|
|
if (countResponse.ok) {
|
|
totalCount = await countResponse.json();
|
|
}
|
|
} catch (countError) {
|
|
logger.debug('Failed to get member count for group', {
|
|
groupId: group.id,
|
|
groupName: group.name
|
|
});
|
|
}
|
|
hasMore = false;
|
|
}
|
|
}
|
|
|
|
return {
|
|
id: group.id,
|
|
name: group.name,
|
|
path: group.path,
|
|
membersCount: totalCount,
|
|
};
|
|
} catch (error) {
|
|
logger.error('Error getting group members count', {
|
|
groupId: group.id,
|
|
groupName: group.name,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
|
|
return {
|
|
id: group.id,
|
|
name: group.name,
|
|
path: group.path,
|
|
membersCount: 0,
|
|
};
|
|
}
|
|
})
|
|
);
|
|
|
|
return NextResponse.json(groupsWithCounts);
|
|
} catch (error) {
|
|
logger.error('Groups API Error', {
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
return NextResponse.json({ message: "Une erreur est survenue" }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
export async function POST(req: Request) {
|
|
try {
|
|
const session = await getServerSession(authOptions);
|
|
if (!session) {
|
|
return NextResponse.json({ message: "Non autorisé" }, { status: 401 });
|
|
}
|
|
|
|
// Safely parse request body
|
|
let name: string;
|
|
try {
|
|
const body = await req.json();
|
|
name = body?.name;
|
|
} catch (parseError) {
|
|
logger.error('Error parsing request body', {
|
|
error: parseError instanceof Error ? parseError.message : String(parseError)
|
|
});
|
|
return NextResponse.json(
|
|
{ message: "Corps de requête invalide" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
if (!name?.trim()) {
|
|
return NextResponse.json(
|
|
{ message: "Le nom du groupe est requis" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const token = await getAdminToken();
|
|
if (!token) {
|
|
return NextResponse.json(
|
|
{ message: "Erreur d'authentification avec Keycloak" },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
const response = await fetch(
|
|
`${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/groups`,
|
|
{
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ name }),
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
// Try to get error details from Keycloak
|
|
let errorMessage = 'Échec de la création du groupe';
|
|
const status = response.status;
|
|
const statusText = response.statusText;
|
|
|
|
try {
|
|
// Check if response has content before trying to parse
|
|
const contentType = response.headers.get('content-type') || '';
|
|
const text = await response.text();
|
|
|
|
logger.debug('Keycloak error response', {
|
|
status,
|
|
statusText,
|
|
contentType,
|
|
hasText: !!text,
|
|
textLength: text?.length || 0
|
|
});
|
|
|
|
if (text && text.trim().length > 0) {
|
|
if (contentType.includes('application/json')) {
|
|
try {
|
|
const errorData = JSON.parse(text);
|
|
// Keycloak error format: { errorMessage: "..." } or { error: "..." }
|
|
errorMessage = errorData.errorMessage || errorData.error || errorData.message || errorMessage;
|
|
} catch (parseError) {
|
|
// If JSON parsing fails, use the text as error message
|
|
logger.debug('Failed to parse error as JSON, using text', {
|
|
text: text.substring(0, 200) // Log first 200 chars
|
|
});
|
|
errorMessage = text || errorMessage;
|
|
}
|
|
} else {
|
|
// If there's text but not JSON, use it
|
|
errorMessage = text;
|
|
}
|
|
} else {
|
|
// If no content, use status text or a status-specific message
|
|
if (status === 409) {
|
|
errorMessage = 'Un groupe avec ce nom existe déjà';
|
|
} else if (status === 400) {
|
|
errorMessage = 'Nom de groupe invalide';
|
|
} else {
|
|
errorMessage = statusText || errorMessage;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// If anything fails, use status text or default message
|
|
logger.error('Error parsing Keycloak error response', {
|
|
status,
|
|
statusText,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
if (status === 409) {
|
|
errorMessage = 'Un groupe avec ce nom existe déjà';
|
|
} else {
|
|
errorMessage = statusText || errorMessage;
|
|
}
|
|
}
|
|
|
|
return NextResponse.json(
|
|
{ message: errorMessage },
|
|
{ status }
|
|
);
|
|
}
|
|
|
|
// Safely parse success response from Keycloak
|
|
let groupData: any;
|
|
try {
|
|
const text = await response.text();
|
|
if (!text || text.trim().length === 0) {
|
|
// If Keycloak returns empty response, create a group object with generated ID
|
|
logger.warn('Keycloak returned empty response for group creation', {
|
|
groupName: name
|
|
});
|
|
groupData = { id: `group-${Date.now()}` };
|
|
} else {
|
|
try {
|
|
groupData = JSON.parse(text);
|
|
} catch (parseError) {
|
|
logger.error('Failed to parse Keycloak success response as JSON', {
|
|
text: text.substring(0, 200),
|
|
error: parseError instanceof Error ? parseError.message : String(parseError)
|
|
});
|
|
// Fallback: create group object with generated ID
|
|
groupData = { id: `group-${Date.now()}` };
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logger.error('Error reading Keycloak success response', {
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
// Fallback: create group object with generated ID
|
|
groupData = { id: `group-${Date.now()}` };
|
|
}
|
|
|
|
const groupId = groupData.id || Date.now().toString();
|
|
|
|
// Create calendar for the group creator
|
|
// Color palette for calendars
|
|
const colorPalette = [
|
|
"#4f46e5", // Indigo
|
|
"#0891b2", // Cyan
|
|
"#0e7490", // Teal
|
|
"#16a34a", // Green
|
|
"#65a30d", // Lime
|
|
"#ca8a04", // Amber
|
|
"#d97706", // Orange
|
|
"#dc2626", // Red
|
|
"#e11d48", // Rose
|
|
"#9333ea", // Purple
|
|
"#7c3aed", // Violet
|
|
"#2563eb", // Blue
|
|
];
|
|
|
|
// Select a color based on group name hash for consistency
|
|
const colorIndex = name.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % colorPalette.length;
|
|
const calendarColor = colorPalette[colorIndex];
|
|
|
|
try {
|
|
await prisma.calendar.create({
|
|
data: {
|
|
name: `Groupe: ${name}`,
|
|
color: calendarColor,
|
|
description: `Calendrier pour le groupe "${name}"`,
|
|
userId: session.user.id,
|
|
},
|
|
});
|
|
logger.debug('Group calendar created successfully', {
|
|
groupId: groupId,
|
|
groupName: name,
|
|
userId: session.user.id
|
|
});
|
|
} catch (calendarError) {
|
|
logger.error('Error creating group calendar', {
|
|
error: calendarError instanceof Error ? calendarError.message : String(calendarError),
|
|
groupId: groupId,
|
|
groupName: name
|
|
});
|
|
// Don't fail group creation if calendar creation fails
|
|
}
|
|
|
|
return NextResponse.json({
|
|
id: groupId,
|
|
name,
|
|
path: `/${name}`,
|
|
membersCount: 0
|
|
});
|
|
} catch (error) {
|
|
logger.error('Create Group Error', {
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
return NextResponse.json(
|
|
{ message: error instanceof Error ? error.message : "Une erreur est survenue" },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|