NeahStable/app/api/calendars/route.ts
2026-01-17 02:57:30 +01:00

312 lines
11 KiB
TypeScript

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 { getCachedCalendarData, cacheCalendarData } from "@/lib/redis";
import { logger } from "@/lib/logger";
import { Prisma } from "@prisma/client";
// Type helper for calendar with all includes
type CalendarWithIncludes = Prisma.CalendarGetPayload<{
include: {
events: true;
mission: {
include: {
missionUsers: true;
};
};
syncConfig: {
include: {
mailCredential: {
select: {
id: true;
email: true;
display_name: true;
};
};
};
};
};
}>;
/**
* Handles the GET request to retrieve calendars for the authenticated user.
*
* @param {NextRequest} req - The incoming request object.
* @returns {Promise<NextResponse>} - A promise that resolves to a JSON response containing the calendars or an error message.
*
* The function performs the following steps:
* 1. Retrieves the server session using `getServerSession`.
* 2. Checks if the user is authenticated by verifying the presence of `session.user.id`.
* - If not authenticated, returns a 401 response with an error message.
* 3. Attempts to fetch the calendars associated with the authenticated user from the database.
* - If successful, returns the calendars in a JSON response.
* - If an error occurs during the database query, logs the error and returns a 500 response with an error message.
*/
export async function GET(req: NextRequest) {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
}
try {
// Check for force refresh parameter
const url = new URL(req.url);
const forceRefresh = url.searchParams.get('refresh') === 'true';
// Try to get data from cache if not forcing refresh
if (!forceRefresh) {
const cachedData = await getCachedCalendarData(session.user.id);
if (cachedData) {
logger.debug('[CALENDAR] Using cached calendar data', {
userIdHash: Buffer.from(session.user.id).toString('base64').slice(0, 12),
calendarCount: cachedData.length,
});
return NextResponse.json(cachedData);
}
}
// If no cache or forcing refresh, fetch from database
logger.debug('[CALENDAR] Fetching calendar data from database', {
userIdHash: Buffer.from(session.user.id).toString('base64').slice(0, 12),
});
// Ensure user has a default private calendar (created automatically if missing)
const defaultPrivateCalendarName = "Mon Calendrier";
const existingDefaultCalendar = await prisma.calendar.findFirst({
where: {
userId: session.user.id,
name: defaultPrivateCalendarName,
missionId: null,
syncConfig: null,
}
});
if (!existingDefaultCalendar) {
await prisma.calendar.create({
data: {
name: defaultPrivateCalendarName,
color: "#4f46e5",
description: "Votre calendrier personnel",
userId: session.user.id,
missionId: null,
}
});
logger.debug('[CALENDAR] Created default private calendar', {
userIdHash: Buffer.from(session.user.id).toString('base64').slice(0, 12),
});
}
// Get user's personal calendars
// Include syncConfig to filter "Privée"/"Default" calendars that don't have active sync
const personalCalendars = await prisma.calendar.findMany({
where: {
userId: session.user.id,
},
include: {
events: {
orderBy: {
start: 'asc'
}
},
mission: {
include: {
missionUsers: true
}
},
syncConfig: {
include: {
mailCredential: {
select: {
id: true,
email: true,
display_name: true,
}
}
}
}
},
orderBy: {
createdAt: "desc",
},
});
// Filter out "Privée"/"Default" calendars that don't have active sync
// This matches the logic in app/agenda/page.tsx
const filteredPersonalCalendars = personalCalendars.filter(cal => {
const isPrivateOrDefault = cal.name === "Privée" || cal.name === "Default";
const hasActiveSync = cal.syncConfig?.syncEnabled === true && cal.syncConfig?.mailCredential;
// Exclude "Privée"/"Default" calendars that are not actively synced
if (isPrivateOrDefault && !hasActiveSync) {
logger.debug('[CALENDAR] Filtering out calendar without active sync', {
calendarIdHash: Buffer.from(cal.id).toString('base64').slice(0, 12),
calendarName: cal.name,
syncEnabled: cal.syncConfig?.syncEnabled,
hasMailCredential: !!cal.syncConfig?.mailCredential,
});
return false;
}
return true;
});
// Get mission calendars where user is associated via MissionUser
const missionUserRelations = await prisma.missionUser.findMany({
where: {
userId: session.user.id,
},
include: {
mission: {
include: {
calendars: {
include: {
events: {
orderBy: {
start: 'asc'
}
},
mission: {
include: {
missionUsers: true
}
},
syncConfig: {
include: {
mailCredential: {
select: {
id: true,
email: true,
display_name: true,
}
}
}
}
}
}
}
}
}
});
// Extract mission calendars (excluding those already in personalCalendars)
// Use a Set to avoid duplicate calendars by ID
const personalCalendarIds = new Set(filteredPersonalCalendars.map(cal => cal.id));
const missionCalendars: CalendarWithIncludes[] = missionUserRelations
.flatMap(mu => mu.mission?.calendars || [])
.filter(cal => !personalCalendarIds.has(cal.id)) as CalendarWithIncludes[]; // Exclude calendars already in personalCalendars
// Combine personal and mission calendars
const calendars: CalendarWithIncludes[] = [...filteredPersonalCalendars, ...missionCalendars];
// Remove duplicate calendars by ID (in case same calendar appears multiple times)
const uniqueCalendars: CalendarWithIncludes[] = Array.from(
new Map(calendars.map(cal => [cal.id, cal])).values()
);
// Sort calendars: "Mon Calendrier" first, then synced, then groups, then missions
const sortedCalendars = uniqueCalendars.sort((a, b) => {
const aIsMonCalendrier = a.name === "Mon Calendrier";
const bIsMonCalendrier = b.name === "Mon Calendrier";
const aIsSynced = a.syncConfig?.syncEnabled && a.syncConfig?.mailCredential;
const bIsSynced = b.syncConfig?.syncEnabled && b.syncConfig?.mailCredential;
const aIsGroup = a.name?.startsWith("Groupe:");
const bIsGroup = b.name?.startsWith("Groupe:");
const aIsMission = a.name?.startsWith("Mission:");
const bIsMission = b.name?.startsWith("Mission:");
// "Mon Calendrier" always first
if (aIsMonCalendrier && !bIsMonCalendrier) return -1;
if (!aIsMonCalendrier && bIsMonCalendrier) return 1;
// Synced calendars second
if (aIsSynced && !bIsSynced) return -1;
if (!aIsSynced && bIsSynced) return 1;
// Groups third
if (aIsGroup && !bIsGroup && !bIsSynced) return -1;
if (!aIsGroup && bIsGroup && !aIsSynced) return 1;
// Missions fourth
if (aIsMission && !bIsMission && !bIsGroup && !bIsSynced) return -1;
if (!aIsMission && bIsMission && !aIsGroup && !aIsSynced) return 1;
// Same type, sort by name
return (a.name || '').localeCompare(b.name || '');
});
logger.debug('[CALENDAR] Fetched calendars with events', {
userIdHash: Buffer.from(session.user.id).toString('base64').slice(0, 12),
personalCount: filteredPersonalCalendars.length,
missionCount: missionCalendars.length,
totalCount: sortedCalendars.length,
filteredOut: personalCalendars.length - filteredPersonalCalendars.length,
eventsPerCalendar: sortedCalendars.map(cal => ({
name: cal.name,
eventCount: cal.events?.length || 0
}))
});
// Cache the results
await cacheCalendarData(session.user.id, sortedCalendars);
return NextResponse.json(sortedCalendars);
} catch (error) {
logger.error('[CALENDAR] Erreur lors de la récupération des calendriers', {
error: error instanceof Error ? error.message : String(error),
});
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
}
}
/**
* Handles the POST request to create a new calendar.
*
* @param {NextRequest} req - The incoming request object.
* @returns {Promise<NextResponse>} The response object containing the created calendar or an error message.
*
* @throws {Error} If there is an issue with the request or server.
*
* The function performs the following steps:
* 1. Retrieves the server session using `getServerSession`.
* 2. Checks if the user is authenticated by verifying the presence of `session.user.id`.
* 3. Parses the request body to extract `name`, `color`, and `description`.
* 4. Validates that the `name` field is provided.
* 5. Creates a new calendar entry in the database using Prisma.
* 6. Returns the created calendar with a 201 status code.
* 7. Catches and logs any errors, returning a 500 status code with an error message.
*/
export async function POST(req: NextRequest) {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
}
try {
const { name, color, description } = await req.json();
// Validation
if (!name) {
return NextResponse.json(
{ error: "Le nom du calendrier est requis" },
{ status: 400 }
);
}
const calendar = await prisma.calendar.create({
data: {
name,
color: color || "#0082c9",
description,
userId: session.user.id,
},
});
return NextResponse.json(calendar, { status: 201 });
} catch (error) {
console.error("Erreur lors de la création du calendrier:", error);
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
}
}