NeahStable/app/api/groups/route.ts
2026-01-14 12:47:48 +01:00

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