From f5a8f3de45094c4e85758ba62a7e45aa0caa5a89 Mon Sep 17 00:00:00 2001 From: alma Date: Wed, 21 Jan 2026 00:39:05 +0100 Subject: [PATCH] agenda finition --- app/api/admin/clean-calendars/route.ts | 203 +++++++++++++++++++++++++ scripts/list-group-calendars.sql | 31 ++++ 2 files changed, 234 insertions(+) create mode 100644 app/api/admin/clean-calendars/route.ts create mode 100644 scripts/list-group-calendars.sql diff --git a/app/api/admin/clean-calendars/route.ts b/app/api/admin/clean-calendars/route.ts new file mode 100644 index 0000000..1e0f059 --- /dev/null +++ b/app/api/admin/clean-calendars/route.ts @@ -0,0 +1,203 @@ +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"; + +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) { + return null; + } + + return data.access_token; + } catch (error) { + console.error('Token Error:', 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('Error fetching groups:', error); + return new Set(); + } +} + +/** + * GET /api/admin/clean-calendars + * Lists orphan group calendars (calendars whose groups no longer exist in Keycloak) + * + * Query params: + * - action=list (default): List orphan calendars + * - action=delete: Delete orphan calendars + */ +export async function GET(req: NextRequest) { + const session = await getServerSession(authOptions); + + // Security: only authenticated users can access this + if (!session?.user?.id) { + return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); + } + + try { + const { searchParams } = new URL(req.url); + const action = searchParams.get('action') || 'list'; + + // Get Keycloak token + const token = await getAdminToken(); + if (!token) { + return NextResponse.json({ error: "Erreur d'authentification Keycloak" }, { status: 500 }); + } + + // Get all existing groups from Keycloak + const keycloakGroups = await getAllKeycloakGroups(token); + + // Get all group calendars from database + const groupCalendars = await prisma.calendar.findMany({ + where: { + name: { + startsWith: 'Groupe: ', + }, + }, + include: { + _count: { + select: { + events: true, + }, + }, + }, + }); + + // Identify orphan calendars + const orphanCalendars = groupCalendars.filter((calendar) => { + const groupName = calendar.name.replace('Groupe: ', ''); + return !keycloakGroups.has(groupName); + }); + + const validCalendars = groupCalendars.filter((calendar) => { + const groupName = calendar.name.replace('Groupe: ', ''); + return keycloakGroups.has(groupName); + }); + + if (action === 'list') { + // Just list the orphan calendars + return NextResponse.json({ + summary: { + totalGroupCalendars: groupCalendars.length, + validCalendars: validCalendars.length, + orphanCalendars: orphanCalendars.length, + keycloakGroups: keycloakGroups.size, + }, + orphans: orphanCalendars.map(cal => ({ + id: cal.id, + name: cal.name, + color: cal.color, + createdAt: cal.createdAt, + eventsCount: cal._count.events, + })), + validGroups: validCalendars.map(cal => ({ + id: cal.id, + name: cal.name, + color: cal.color, + eventsCount: cal._count.events, + })), + }); + } else if (action === 'delete') { + // Delete orphan calendars + if (orphanCalendars.length === 0) { + return NextResponse.json({ + message: "Aucun calendrier orphelin à supprimer", + deleted: 0, + }); + } + + const deletedIds: string[] = []; + const errors: Array<{ calendarId: string; calendarName: string; error: string }> = []; + + for (const calendar of orphanCalendars) { + try { + await prisma.calendar.delete({ + where: { id: calendar.id }, + }); + deletedIds.push(calendar.id); + logger.info('Orphan calendar deleted', { + calendarId: calendar.id, + calendarName: calendar.name + }); + } catch (error) { + logger.error('Failed to delete orphan calendar', { + calendarId: calendar.id, + error + }); + errors.push({ + calendarId: calendar.id, + calendarName: calendar.name, + error: error instanceof Error ? error.message : String(error), + }); + } + } + + return NextResponse.json({ + message: `${deletedIds.length} calendrier(s) orphelin(s) supprimé(s)`, + deleted: deletedIds.length, + deletedCalendars: orphanCalendars + .filter(cal => deletedIds.includes(cal.id)) + .map(cal => cal.name), + errors: errors.length > 0 ? errors : undefined, + }); + } else { + return NextResponse.json( + { error: "Action invalide. Utilisez 'list' ou 'delete'" }, + { status: 400 } + ); + } + } catch (error) { + logger.error('Error in clean-calendars API', { error }); + return NextResponse.json( + { + error: "Erreur serveur", + details: error instanceof Error ? error.message : String(error) + }, + { status: 500 } + ); + } +} diff --git a/scripts/list-group-calendars.sql b/scripts/list-group-calendars.sql new file mode 100644 index 0000000..7f4a806 --- /dev/null +++ b/scripts/list-group-calendars.sql @@ -0,0 +1,31 @@ +-- Script SQL pour lister et supprimer les calendriers de groupes orphelins +-- +-- Usage: +-- 1. Se connecter à PostgreSQL +-- 2. Copier-coller les requêtes une par une + +-- 1. LISTER tous les calendriers de groupes +SELECT + id, + name, + color, + "createdAt", + "userId" +FROM "Calendar" +WHERE name LIKE 'Groupe:%' +ORDER BY "createdAt" DESC; + +-- 2. COMPTER les calendriers de groupes +SELECT COUNT(*) as total_group_calendars +FROM "Calendar" +WHERE name LIKE 'Groupe:%'; + +-- 3. SUPPRIMER un calendrier spécifique (remplacez CALENDAR_ID) +-- DELETE FROM "Calendar" WHERE id = 'CALENDAR_ID'; + +-- 4. SUPPRIMER tous les calendriers d'un groupe spécifique (remplacez le nom) +-- DELETE FROM "Calendar" WHERE name = 'Groupe: NomDuGroupe'; + +-- 5. EXEMPLE: Supprimer tous les calendriers de groupes (ATTENTION!) +-- Décommentez cette ligne seulement si vous êtes SÛR de vouloir tout supprimer +-- DELETE FROM "Calendar" WHERE name LIKE 'Groupe:%';