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 } ); } }