diff --git a/app/agenda/page.tsx b/app/agenda/page.tsx index 43da248..fcb3faf 100644 --- a/app/agenda/page.tsx +++ b/app/agenda/page.tsx @@ -235,7 +235,44 @@ export default async function CalendarPage() { // If sync exists but is disabled, check if it's due to invalid credentials // Don't re-enable if the last error was 401 (invalid credentials) if (existingSync) { - console.log(`[AGENDA] Found existing sync for Infomaniak account ${account.email}: syncId=${existingSync.id}, calendarId=${existingSync.calendarId}, syncEnabled=${existingSync.syncEnabled}, hasCalendar=${!!existingSync.calendar}`); + console.log(`[AGENDA] Found existing sync for Infomaniak account ${account.email}: syncId=${existingSync.id}, calendarId=${existingSync.calendarId}, syncEnabled=${existingSync.syncEnabled}, hasCalendar=${!!existingSync.calendar}, externalCalendarUrl=${existingSync.externalCalendarUrl}`); + + // Fix invalid calendar URLs (like /principals) + if (existingSync.externalCalendarUrl === '/principals' || !existingSync.externalCalendarUrl || existingSync.externalCalendarUrl === '/') { + console.log(`[AGENDA] Invalid calendar URL detected (${existingSync.externalCalendarUrl}), attempting to rediscover...`); + try { + const { discoverInfomaniakCalendars } = await import('@/lib/services/caldav-sync'); + const externalCalendars = await discoverInfomaniakCalendars( + account.email, + account.password! + ); + + if (externalCalendars.length > 0) { + const mainCalendar = externalCalendars[0]; + console.log(`[AGENDA] Updating sync with correct calendar URL: ${mainCalendar.url}`); + await prisma.calendarSync.update({ + where: { id: existingSync.id }, + data: { + externalCalendarId: mainCalendar.id, + externalCalendarUrl: mainCalendar.url, + lastSyncError: null, // Clear error since we're fixing the URL + } + }); + // Reload the sync config + const updatedSync = await prisma.calendarSync.findUnique({ + where: { id: existingSync.id }, + include: { calendar: true } + }); + if (updatedSync) { + existingSync = updatedSync; + } + } else { + console.log(`[AGENDA] No calendars found during rediscovery for ${account.email}`); + } + } catch (rediscoverError) { + console.error(`[AGENDA] Error rediscovering calendars:`, rediscoverError); + } + } // Check if calendar still exists if (!existingSync.calendar) { diff --git a/lib/services/caldav-sync.ts b/lib/services/caldav-sync.ts index bae5362..98a8aca 100644 --- a/lib/services/caldav-sync.ts +++ b/lib/services/caldav-sync.ts @@ -53,46 +53,55 @@ export async function discoverInfomaniakCalendars( const calendars: CalDAVCalendar[] = []; for (const item of items) { - if (item.type === 'directory' && item.filename !== '/') { - // Get calendar properties - try { - const props = await client.customRequest(item.filename, { - method: 'PROPFIND', - headers: { - Depth: '0', - 'Content-Type': 'application/xml', - }, - data: ` + // Skip non-directories, root, and special directories like /principals + if (item.type !== 'directory' || item.filename === '/' || item.filename === '/principals') { + continue; + } + + // Get calendar properties to verify it's actually a calendar + try { + const props = await client.customRequest(item.filename, { + method: 'PROPFIND', + headers: { + Depth: '0', + 'Content-Type': 'application/xml', + }, + data: ` + `, - }); + }); - // Parse XML response to extract calendar name and color - const displayName = extractDisplayName(props.data); - const color = extractCalendarColor(props.data); - - calendars.push({ - id: item.filename.replace(/^\//, '').replace(/\/$/, ''), - name: displayName || item.basename || 'Calendrier', - url: item.filename, - color: color, - }); - } catch (error) { - logger.error('Error fetching calendar properties', { - calendar: item.filename, - error: error instanceof Error ? error.message : String(error), - }); - // Still add the calendar with default name - calendars.push({ - id: item.filename.replace(/^\//, '').replace(/\/$/, ''), - name: item.basename || 'Calendrier', - url: item.filename, + // Check if this is actually a calendar (has in resourcetype) + const isCalendar = props.data && props.data.includes(' { try { + // Validate calendar URL - must not be /principals or other non-calendar paths + if (!calendarUrl || calendarUrl === '/principals' || calendarUrl === '/') { + throw new Error(`Invalid calendar URL: ${calendarUrl}. This is not a calendar directory.`); + } + const client = await getInfomaniakCalDAVClient(email, password); // Build calendar query URL @@ -188,6 +202,11 @@ export async function fetchCalDAVEvents( data: queryXml, }); + // Validate response data exists + if (!response.data || typeof response.data !== 'string') { + throw new Error(`Invalid response from CalDAV server: expected string data, got ${typeof response.data}`); + } + // Parse iCalendar data from response const events = parseICalendarEvents(response.data); @@ -399,7 +418,8 @@ export async function syncInfomaniakCalendar( }); // Create a map of existing events by externalEventId (UID) for fast lookup - const existingEventsByExternalId = new Map(); + type EventType = typeof existingEvents[number]; + const existingEventsByExternalId = new Map(); for (const event of existingEvents) { if (event.externalEventId) { existingEventsByExternalId.set(event.externalEventId, event); @@ -413,14 +433,14 @@ export async function syncInfomaniakCalendar( // Sync events: create or update for (const caldavEvent of caldavEvents) { // Priority 1: Match by externalEventId (UID) - most reliable - let existingEvent = caldavEvent.uid + let existingEvent: EventType | undefined = caldavEvent.uid ? existingEventsByExternalId.get(caldavEvent.uid) : undefined; // Priority 2: Fallback to title + date matching for events without externalEventId (backward compatibility) if (!existingEvent) { existingEvent = existingEvents.find( - (e) => { + (e: EventType) => { if (!e.externalEventId && // Only match events that don't have externalEventId yet e.title === caldavEvent.summary) { const timeDiff = Math.abs(new Date(e.start).getTime() - caldavEvent.start.getTime()); @@ -431,29 +451,33 @@ export async function syncInfomaniakCalendar( ); } - const eventData = { + // For updates, we cannot modify calendarId and userId (they are relations) + // For creates, we need them + const baseEventData = { title: caldavEvent.summary, description: caldavEvent.description || null, start: caldavEvent.start, end: caldavEvent.end, location: caldavEvent.location || null, isAllDay: caldavEvent.allDay, - calendarId: syncConfig.calendarId, - userId: syncConfig.calendar.userId, externalEventId: caldavEvent.uid, // Store UID for reliable matching }; if (existingEvent) { - // Update existing event + // Update existing event (without calendarId and userId - they are relations) await prisma.event.update({ where: { id: existingEvent.id }, - data: eventData, + data: baseEventData, }); updated++; } else { - // Create new event + // Create new event (with calendarId and userId) await prisma.event.create({ - data: eventData, + data: { + ...baseEventData, + calendarId: syncConfig.calendarId, + userId: syncConfig.calendar.userId, + }, }); created++; } diff --git a/lib/services/microsoft-calendar-sync.ts b/lib/services/microsoft-calendar-sync.ts index 736e3d5..70c04fb 100644 --- a/lib/services/microsoft-calendar-sync.ts +++ b/lib/services/microsoft-calendar-sync.ts @@ -497,23 +497,23 @@ export async function syncMicrosoftCalendar( // Clean description (remove [MS_ID:xxx] prefix if present from previous syncs) const cleanedDescription = cleanDescription(caldavEvent.description); - const eventData = { + // For updates, we cannot modify calendarId and userId (they are relations) + // For creates, we need them + const baseEventData = { title: caldavEvent.summary, description: cleanedDescription, // Clean description without [MS_ID:xxx] prefix start: caldavEvent.start, end: caldavEvent.end, location: caldavEvent.location || null, isAllDay: caldavEvent.allDay, - calendarId: syncConfig.calendarId, - userId: syncConfig.calendar.userId, externalEventId: microsoftId, // Store Microsoft ID for reliable matching }; if (existingEvent) { - // Update existing event + // Update existing event (without calendarId and userId - they are relations) await prisma.event.update({ where: { id: existingEvent.id }, - data: eventData, + data: baseEventData, }); updated++; logger.debug('Updated event', { @@ -522,9 +522,13 @@ export async function syncMicrosoftCalendar( microsoftId, }); } else { - // Create new event + // Create new event (with calendarId and userId) const newEvent = await prisma.event.create({ - data: eventData, + data: { + ...baseEventData, + calendarId: syncConfig.calendarId, + userId: syncConfig.calendar.userId, + }, }); created++; logger.debug('Created new event', {