312 lines
11 KiB
TypeScript
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 });
|
|
}
|
|
}
|