From 5a4e746bf4465f39d50de60a277bb5ed5b7cf987 Mon Sep 17 00:00:00 2001 From: alma Date: Wed, 14 Jan 2026 17:24:35 +0100 Subject: [PATCH] Agenda refactor --- app/agenda/page.tsx | 210 ++++++++++++++++++++++++++++-------- lib/services/caldav-sync.ts | 36 ++++++- 2 files changed, 199 insertions(+), 47 deletions(-) diff --git a/app/agenda/page.tsx b/app/agenda/page.tsx index ec615e0..77f3a1f 100644 --- a/app/agenda/page.tsx +++ b/app/agenda/page.tsx @@ -131,6 +131,50 @@ export default async function CalendarPage() { } }); + // Clean up orphaned syncs FIRST (before creating new ones) + // This handles the case where a user deleted and re-added an email account + const allMailCredentialIds = new Set([ + ...infomaniakAccounts.map(acc => acc.id), + ...microsoftAccounts.map(acc => acc.id) + ]); + + const orphanedSyncs = await prisma.calendarSync.findMany({ + where: { + calendar: { + userId: session?.user?.id || '' + }, + mailCredentialId: { + not: null + } + }, + include: { + calendar: true, + mailCredential: true + } + }); + + // Delete syncs where mailCredential no longer exists + for (const sync of orphanedSyncs) { + if (sync.mailCredentialId && !allMailCredentialIds.has(sync.mailCredentialId)) { + console.log(`[AGENDA] Deleting orphaned sync for non-existent mailCredentialId: ${sync.mailCredentialId}`); + // Delete the calendar if it has no events + const eventCount = await prisma.event.count({ + where: { calendarId: sync.calendarId } + }); + if (eventCount === 0) { + await prisma.calendar.delete({ + where: { id: sync.calendarId } + }); + } else { + // Just disable the sync, keep the calendar + await prisma.calendarSync.update({ + where: { id: sync.id }, + data: { syncEnabled: false } + }); + } + } + } + // For each Infomaniak account, ensure there's a synced calendar // Skip if no Infomaniak accounts exist (user may only have Microsoft accounts) if (infomaniakAccounts.length > 0) { @@ -148,18 +192,46 @@ 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 && !existingSync.syncEnabled) { - const isAuthError = existingSync.lastSyncError?.includes('401') || - existingSync.lastSyncError?.includes('Unauthorized') || - existingSync.lastSyncError?.includes('invalid'); + 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}`); - if (!isAuthError) { - // Only re-enable if it's not an authentication error + // Check if calendar still exists + if (!existingSync.calendar) { + console.log(`[AGENDA] Calendar for sync ${existingSync.id} does not exist, creating new calendar`); + // Calendar was deleted, create a new one + const calendar = await prisma.calendar.create({ + data: { + name: "Privée", + color: "#4F46E5", + description: `Calendrier synchronisé avec ${account.display_name || account.email}`, + userId: session?.user?.id || '', + } + }); + + // Update sync to point to new calendar await prisma.calendarSync.update({ where: { id: existingSync.id }, - data: { syncEnabled: true } + data: { + calendarId: calendar.id, + syncEnabled: true + } }); - } else { + continue; + } + + if (!existingSync.syncEnabled) { + const isAuthError = existingSync.lastSyncError?.includes('401') || + existingSync.lastSyncError?.includes('Unauthorized') || + existingSync.lastSyncError?.includes('invalid'); + + if (!isAuthError) { + // Only re-enable if it's not an authentication error + console.log(`[AGENDA] Re-enabling sync ${existingSync.id} for Infomaniak account ${account.email}`); + await prisma.calendarSync.update({ + where: { id: existingSync.id }, + data: { syncEnabled: true } + }); + } else { // Try to discover calendars to verify if credentials are now valid // But if discovery fails and we have an existing URL, re-enable sync anyway // The existing URL might still work even if discovery fails @@ -221,18 +293,23 @@ export default async function CalendarPage() { } if (!existingSync) { - // Try to discover calendars for this account + // No sync exists for this account - try to discover and create calendar + // Only create calendar if discovery succeeds try { const { discoverInfomaniakCalendars } = await import('@/lib/services/caldav-sync'); const externalCalendars = await discoverInfomaniakCalendars( account.email, account.password! ); + + console.log(`[AGENDA] Discovered ${externalCalendars.length} calendars for Infomaniak account ${account.email}`); if (externalCalendars.length > 0) { // Use the first calendar (usually the main calendar) const mainCalendar = externalCalendars[0]; + console.log(`[AGENDA] Creating Infomaniak calendar for ${account.email} with URL: ${mainCalendar.url}`); + // Create a private calendar for this account const calendar = await prisma.calendar.create({ data: { @@ -244,7 +321,7 @@ export default async function CalendarPage() { }); // Create sync configuration - await prisma.calendarSync.create({ + const syncConfig = await prisma.calendarSync.create({ data: { calendarId: calendar.id, mailCredentialId: account.id, @@ -256,50 +333,28 @@ export default async function CalendarPage() { } }); + console.log(`[AGENDA] Created Infomaniak calendar sync: ${syncConfig.id} for calendar: ${calendar.id}`); + // Trigger initial sync try { const { syncInfomaniakCalendar } = await import('@/lib/services/caldav-sync'); - const syncConfig = await prisma.calendarSync.findUnique({ - where: { calendarId: calendar.id }, - include: { - calendar: true, - mailCredential: true + await syncInfomaniakCalendar(syncConfig.id, true); + console.log(`[AGENDA] Initial sync completed for Infomaniak calendar: ${calendar.id}`); + } catch (syncError) { + const syncErrorMessage = syncError instanceof Error ? syncError.message : 'Unknown error'; + console.log(`[AGENDA] Initial sync failed for Infomaniak calendar: ${calendar.id} - ${syncErrorMessage}`); + await prisma.calendarSync.update({ + where: { id: syncConfig.id }, + data: { + lastSyncError: `Erreur de synchronisation: ${syncErrorMessage}` } }); - if (syncConfig) { - await syncInfomaniakCalendar(syncConfig.id, true); - } - } catch (syncError) { - console.error('Error during initial sync:', syncError); - // Don't fail if sync fails, calendar is still created } } } catch (error) { - // Log error but don't fail the page - account may not have calendar access or credentials may be invalid + // Discovery failed - don't create calendar const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - console.log(`Infomaniak calendar sync not available for ${account.email} - ${errorMessage}`); - - // If it's a 401 error, the credentials are likely invalid - update lastSyncError in existing sync if any - if (errorMessage.includes('401') || errorMessage.includes('Unauthorized')) { - // Check if there's a disabled sync for this account - const disabledSync = await prisma.calendarSync.findFirst({ - where: { - mailCredentialId: account.id, - provider: 'infomaniak', - syncEnabled: false - } - }); - - if (disabledSync) { - // Update the error message - await prisma.calendarSync.update({ - where: { id: disabledSync.id }, - data: { - lastSyncError: `Identifiants invalides ou expirés (401 Unauthorized). Veuillez vérifier vos identifiants Infomaniak dans la page courrier.` - } - }); - } - } + console.log(`[AGENDA] Infomaniak calendar discovery failed for ${account.email} - ${errorMessage}. Calendar will not be created.`); // Continue with other accounts even if one fails } } @@ -392,12 +447,59 @@ export default async function CalendarPage() { } } + // Clean up orphaned syncs (syncs with mailCredentialId that no longer exists) + // This can happen when a user deletes and re-adds an email account + const allMailCredentialIds = new Set([ + ...infomaniakAccounts.map(acc => acc.id), + ...microsoftAccounts.map(acc => acc.id) + ]); + + const orphanedSyncs = await prisma.calendarSync.findMany({ + where: { + calendar: { + userId: session?.user?.id || '' + }, + mailCredentialId: { + not: null + } + }, + include: { + calendar: true, + mailCredential: true + } + }); + + // Delete syncs where mailCredential no longer exists + for (const sync of orphanedSyncs) { + if (sync.mailCredentialId && !allMailCredentialIds.has(sync.mailCredentialId)) { + console.log(`[AGENDA] Deleting orphaned sync for non-existent mailCredentialId: ${sync.mailCredentialId}`); + // Delete the calendar if it has no events + const eventCount = await prisma.event.count({ + where: { calendarId: sync.calendarId } + }); + if (eventCount === 0) { + await prisma.calendar.delete({ + where: { id: sync.calendarId } + }); + } else { + // Just disable the sync, keep the calendar + await prisma.calendarSync.update({ + where: { id: sync.id }, + data: { syncEnabled: false } + }); + } + } + } + // Clean up duplicate calendars for the same mailCredentialId // Keep only the most recent one with syncEnabled=true, delete others const allSyncs = await prisma.calendarSync.findMany({ where: { calendar: { userId: session?.user?.id || '' + }, + mailCredentialId: { + in: Array.from(allMailCredentialIds) } }, include: { @@ -507,6 +609,24 @@ export default async function CalendarPage() { // No default calendar creation - only synced calendars from courrier + // Debug: Verify Infomaniak calendars exist in database + const allInfomaniakSyncs = await prisma.calendarSync.findMany({ + where: { + provider: 'infomaniak', + calendar: { + userId: session?.user?.id || '' + } + }, + include: { + calendar: true, + mailCredential: true + } + }); + console.log(`[AGENDA] Found ${allInfomaniakSyncs.length} Infomaniak syncs in database`); + allInfomaniakSyncs.forEach(sync => { + console.log(`[AGENDA] Infomaniak sync: id=${sync.id}, calendarId=${sync.calendarId}, calendarName=${sync.calendar?.name}, syncEnabled=${sync.syncEnabled}, mailCredentialId=${sync.mailCredentialId}, hasMailCredential=${!!sync.mailCredential}`); + }); + // Debug: Log calendars before filtering console.log(`[AGENDA] Calendars before filtering: ${calendars.length}`); const infomaniakBeforeFilter = calendars.filter(cal => cal.syncConfig?.provider === 'infomaniak'); diff --git a/lib/services/caldav-sync.ts b/lib/services/caldav-sync.ts index c3bbbd6..3f9a26a 100644 --- a/lib/services/caldav-sync.ts +++ b/lib/services/caldav-sync.ts @@ -48,8 +48,40 @@ export async function discoverInfomaniakCalendars( try { const client = await getInfomaniakCalDAVClient(email, password); - // List all calendars using PROPFIND on /caldav path - const items = await client.getDirectoryContents('/caldav'); + // List all calendars using PROPFIND + // Try different paths: root, /caldav, /calendars/{username} + let items; + let triedPaths: string[] = []; + + // Try root path first + try { + logger.debug('Trying CalDAV discovery on root path /'); + items = await client.getDirectoryContents('/'); + logger.debug(`CalDAV discovery succeeded on root path, found ${items.length} items`); + } catch (rootError) { + triedPaths.push('/'); + logger.debug('Root path failed, trying /caldav path'); + + // Try /caldav path + try { + items = await client.getDirectoryContents('/caldav'); + logger.debug(`CalDAV discovery succeeded on /caldav path, found ${items.length} items`); + } catch (caldavError) { + triedPaths.push('/caldav'); + + // Try /calendars/{username} path + const username = email.split('@')[0]; + const calendarsPath = `/calendars/${username}`; + logger.debug(`Trying CalDAV discovery on ${calendarsPath} path`); + try { + items = await client.getDirectoryContents(calendarsPath); + logger.debug(`CalDAV discovery succeeded on ${calendarsPath} path, found ${items.length} items`); + } catch (calendarsError) { + triedPaths.push(calendarsPath); + throw new Error(`CalDAV discovery failed on all paths (${triedPaths.join(', ')}). Last error: ${calendarsError instanceof Error ? calendarsError.message : String(calendarsError)}`); + } + } + } const calendars: CalDAVCalendar[] = [];