From d6c8376bb48733070b720fd8dd08d75483b8bbce Mon Sep 17 00:00:00 2001 From: alma Date: Wed, 21 Jan 2026 00:34:52 +0100 Subject: [PATCH] agenda finition --- app/api/calendars/[calendarId]/route.ts | 185 ++++++++++++++++++++++++ app/vision/page.tsx | 5 + scripts/clean-orphan-calendars.ts | 149 +++++++++++++++++++ 3 files changed, 339 insertions(+) create mode 100644 app/api/calendars/[calendarId]/route.ts create mode 100644 scripts/clean-orphan-calendars.ts diff --git a/app/api/calendars/[calendarId]/route.ts b/app/api/calendars/[calendarId]/route.ts new file mode 100644 index 0000000..ad186dc --- /dev/null +++ b/app/api/calendars/[calendarId]/route.ts @@ -0,0 +1,185 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/app/api/auth/options"; +import { prisma } from "@/lib/prisma"; +import logger from "@/utils/logger"; + +/** + * GET /api/calendars/[calendarId] + * Retrieves a specific calendar + */ +export async function GET( + req: NextRequest, + { params }: { params: Promise<{ calendarId: string }> } +) { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); + } + + try { + const { calendarId } = await params; + + const calendar = await prisma.calendar.findUnique({ + where: { id: calendarId }, + include: { + events: { + orderBy: { + start: 'asc' + } + }, + mission: true, + }, + }); + + if (!calendar) { + return NextResponse.json({ error: "Calendrier non trouvé" }, { status: 404 }); + } + + // Check if user has access to this calendar + const hasAccess = + calendar.userId === session.user.id || // User owns the calendar + calendar.isPublic || // Calendar is public + (calendar.mission && calendar.mission.creatorId === session.user.id); // User created the mission + + if (!hasAccess) { + return NextResponse.json({ error: "Non autorisé" }, { status: 403 }); + } + + return NextResponse.json(calendar); + } catch (error) { + logger.error('Error fetching calendar', { error }); + return NextResponse.json({ error: "Erreur serveur" }, { status: 500 }); + } +} + +/** + * DELETE /api/calendars/[calendarId] + * Deletes a calendar and all its events + */ +export async function DELETE( + req: NextRequest, + { params }: { params: Promise<{ calendarId: string }> } +) { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); + } + + try { + const { calendarId } = await params; + + const calendar = await prisma.calendar.findUnique({ + where: { id: calendarId }, + include: { + mission: true, + }, + }); + + if (!calendar) { + return NextResponse.json({ error: "Calendrier non trouvé" }, { status: 404 }); + } + + // Check permissions + // Only allow deletion if: + // 1. User owns the calendar + // 2. OR it's a group calendar (starts with "Groupe:") + // 3. OR user created the associated mission + const isOwner = calendar.userId === session.user.id; + const isGroupCalendar = calendar.name.startsWith("Groupe:"); + const isMissionCreator = calendar.mission && calendar.mission.creatorId === session.user.id; + + if (!isOwner && !isGroupCalendar && !isMissionCreator) { + return NextResponse.json( + { error: "Vous n'avez pas la permission de supprimer ce calendrier" }, + { status: 403 } + ); + } + + // For group calendars, verify the group no longer exists + if (isGroupCalendar) { + logger.info('Deleting group calendar', { + calendarId, + calendarName: calendar.name, + userId: session.user.id + }); + } + + // Delete the calendar (cascade will delete events) + await prisma.calendar.delete({ + where: { id: calendarId }, + }); + + logger.info('Calendar deleted successfully', { + calendarId, + calendarName: calendar.name + }); + + return NextResponse.json({ success: true, message: "Calendrier supprimé avec succès" }); + } catch (error) { + logger.error('Error deleting calendar', { error }); + return NextResponse.json({ error: "Erreur lors de la suppression du calendrier" }, { status: 500 }); + } +} + +/** + * PATCH /api/calendars/[calendarId] + * Updates calendar properties (name, color, description, etc.) + */ +export async function PATCH( + req: NextRequest, + { params }: { params: Promise<{ calendarId: string }> } +) { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); + } + + try { + const { calendarId } = await params; + const body = await req.json(); + + const calendar = await prisma.calendar.findUnique({ + where: { id: calendarId }, + include: { + mission: true, + }, + }); + + if (!calendar) { + return NextResponse.json({ error: "Calendrier non trouvé" }, { status: 404 }); + } + + // Check permissions + const isOwner = calendar.userId === session.user.id; + const isMissionCreator = calendar.mission && calendar.mission.creatorId === session.user.id; + + if (!isOwner && !isMissionCreator) { + return NextResponse.json( + { error: "Vous n'avez pas la permission de modifier ce calendrier" }, + { status: 403 } + ); + } + + // Update calendar + const updatedCalendar = await prisma.calendar.update({ + where: { id: calendarId }, + data: { + ...(body.name && { name: body.name }), + ...(body.color && { color: body.color }), + ...(body.description !== undefined && { description: body.description }), + ...(body.isPublic !== undefined && { isPublic: body.isPublic }), + }, + }); + + logger.info('Calendar updated successfully', { + calendarId, + updatedFields: Object.keys(body) + }); + + return NextResponse.json(updatedCalendar); + } catch (error) { + logger.error('Error updating calendar', { error }); + return NextResponse.json({ error: "Erreur lors de la mise à jour du calendrier" }, { status: 500 }); + } +} diff --git a/app/vision/page.tsx b/app/vision/page.tsx index b14727a..f4ef15d 100644 --- a/app/vision/page.tsx +++ b/app/vision/page.tsx @@ -277,6 +277,11 @@ export default function VisionPage() { const groupsRes = await fetch(`/api/users/${userId}/groups`); if (groupsRes.ok) { const groupsData = await groupsRes.json(); + console.log('[Vision] Groups loaded:', groupsData.map((g: any) => ({ + id: g.id, + name: g.name, + calendarColor: g.calendarColor + }))); setGroups(Array.isArray(groupsData) ? groupsData : []); } else { console.error("Failed to fetch groups"); diff --git a/scripts/clean-orphan-calendars.ts b/scripts/clean-orphan-calendars.ts new file mode 100644 index 0000000..d7498a9 --- /dev/null +++ b/scripts/clean-orphan-calendars.ts @@ -0,0 +1,149 @@ +/** + * Script de nettoyage des calendriers de groupes orphelins + * + * Ce script supprime les calendriers de groupes dont le groupe n'existe plus dans Keycloak. + * + * Usage: + * npx tsx scripts/clean-orphan-calendars.ts + */ + +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(); + if (!tokenResponse.ok || !data.access_token) { + console.error('❌ Erreur token:', data); + return null; + } + + return data.access_token; + } catch (error) { + console.error('❌ Erreur token:', error); + return null; + } +} + +async function getAllKeycloakGroups(token: string): Promise> { + try { + const response = await fetch( + `${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/groups`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + + if (!response.ok) { + throw new Error('Failed to fetch groups from Keycloak'); + } + + const groups = await response.json(); + const groupNames = new Set(); + + groups.forEach((group: any) => { + groupNames.add(group.name); + }); + + return groupNames; + } catch (error) { + console.error('❌ Erreur lors de la récupération des groupes:', error); + return new Set(); + } +} + +async function cleanOrphanCalendars() { + console.log('🧹 Début du nettoyage des calendriers orphelins...\n'); + + // 1. Récupérer le token Keycloak + const token = await getAdminToken(); + if (!token) { + console.error('❌ Impossible d\'obtenir le token Keycloak'); + process.exit(1); + } + + // 2. Récupérer tous les groupes existants dans Keycloak + console.log('📋 Récupération des groupes depuis Keycloak...'); + const keycloakGroups = await getAllKeycloakGroups(token); + console.log(`✅ ${keycloakGroups.size} groupes trouvés dans Keycloak\n`); + + // 3. Récupérer tous les calendriers de groupes + console.log('📋 Récupération des calendriers de groupes depuis la base...'); + const groupCalendars = await prisma.calendar.findMany({ + where: { + name: { + startsWith: 'Groupe: ', + }, + }, + }); + console.log(`✅ ${groupCalendars.length} calendriers de groupes trouvés\n`); + + // 4. Identifier les calendriers orphelins + const orphanCalendars = groupCalendars.filter((calendar) => { + const groupName = calendar.name.replace('Groupe: ', ''); + return !keycloakGroups.has(groupName); + }); + + if (orphanCalendars.length === 0) { + console.log('✅ Aucun calendrier orphelin trouvé. Base de données propre!\n'); + return; + } + + console.log(`⚠️ ${orphanCalendars.length} calendrier(s) orphelin(s) trouvé(s):\n`); + orphanCalendars.forEach((cal) => { + console.log(` - ${cal.name} (ID: ${cal.id})`); + }); + + // 5. Demander confirmation (commentez cette section si vous voulez une suppression automatique) + console.log('\n⚠️ ATTENTION: Les calendriers ci-dessus vont être supprimés avec tous leurs événements!'); + console.log('Pour confirmer, lancez le script avec: npx tsx scripts/clean-orphan-calendars.ts --confirm\n'); + + const isConfirmed = process.argv.includes('--confirm'); + + if (!isConfirmed) { + console.log('❌ Suppression annulée. Utilisez --confirm pour supprimer.'); + return; + } + + // 6. Supprimer les calendriers orphelins + console.log('\n🗑️ Suppression des calendriers orphelins...\n'); + + for (const calendar of orphanCalendars) { + try { + await prisma.calendar.delete({ + where: { id: calendar.id }, + }); + console.log(` ✅ Supprimé: ${calendar.name}`); + } catch (error) { + console.error(` ❌ Erreur lors de la suppression de ${calendar.name}:`, error); + } + } + + console.log(`\n✅ Nettoyage terminé! ${orphanCalendars.length} calendrier(s) supprimé(s).\n`); +} + +// Exécuter le script +cleanOrphanCalendars() + .then(() => { + process.exit(0); + }) + .catch((error) => { + console.error('❌ Erreur fatale:', error); + process.exit(1); + });