Agenda refactor

This commit is contained in:
alma 2026-01-14 17:24:35 +01:00
parent 5d90bc6989
commit 5a4e746bf4
2 changed files with 199 additions and 47 deletions

View File

@ -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 // For each Infomaniak account, ensure there's a synced calendar
// Skip if no Infomaniak accounts exist (user may only have Microsoft accounts) // Skip if no Infomaniak accounts exist (user may only have Microsoft accounts)
if (infomaniakAccounts.length > 0) { 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 // 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) // Don't re-enable if the last error was 401 (invalid credentials)
if (existingSync && !existingSync.syncEnabled) { if (existingSync) {
const isAuthError = existingSync.lastSyncError?.includes('401') || console.log(`[AGENDA] Found existing sync for Infomaniak account ${account.email}: syncId=${existingSync.id}, calendarId=${existingSync.calendarId}, syncEnabled=${existingSync.syncEnabled}, hasCalendar=${!!existingSync.calendar}`);
existingSync.lastSyncError?.includes('Unauthorized') ||
existingSync.lastSyncError?.includes('invalid');
if (!isAuthError) { // Check if calendar still exists
// Only re-enable if it's not an authentication error 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({ await prisma.calendarSync.update({
where: { id: existingSync.id }, 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 // 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 // But if discovery fails and we have an existing URL, re-enable sync anyway
// The existing URL might still work even if discovery fails // The existing URL might still work even if discovery fails
@ -221,7 +293,8 @@ export default async function CalendarPage() {
} }
if (!existingSync) { 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 { try {
const { discoverInfomaniakCalendars } = await import('@/lib/services/caldav-sync'); const { discoverInfomaniakCalendars } = await import('@/lib/services/caldav-sync');
const externalCalendars = await discoverInfomaniakCalendars( const externalCalendars = await discoverInfomaniakCalendars(
@ -229,10 +302,14 @@ export default async function CalendarPage() {
account.password! account.password!
); );
console.log(`[AGENDA] Discovered ${externalCalendars.length} calendars for Infomaniak account ${account.email}`);
if (externalCalendars.length > 0) { if (externalCalendars.length > 0) {
// Use the first calendar (usually the main calendar) // Use the first calendar (usually the main calendar)
const mainCalendar = externalCalendars[0]; const mainCalendar = externalCalendars[0];
console.log(`[AGENDA] Creating Infomaniak calendar for ${account.email} with URL: ${mainCalendar.url}`);
// Create a private calendar for this account // Create a private calendar for this account
const calendar = await prisma.calendar.create({ const calendar = await prisma.calendar.create({
data: { data: {
@ -244,7 +321,7 @@ export default async function CalendarPage() {
}); });
// Create sync configuration // Create sync configuration
await prisma.calendarSync.create({ const syncConfig = await prisma.calendarSync.create({
data: { data: {
calendarId: calendar.id, calendarId: calendar.id,
mailCredentialId: account.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 // Trigger initial sync
try { try {
const { syncInfomaniakCalendar } = await import('@/lib/services/caldav-sync'); const { syncInfomaniakCalendar } = await import('@/lib/services/caldav-sync');
const syncConfig = await prisma.calendarSync.findUnique({ await syncInfomaniakCalendar(syncConfig.id, true);
where: { calendarId: calendar.id }, console.log(`[AGENDA] Initial sync completed for Infomaniak calendar: ${calendar.id}`);
include: { } catch (syncError) {
calendar: true, const syncErrorMessage = syncError instanceof Error ? syncError.message : 'Unknown error';
mailCredential: true 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) { } 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'; const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.log(`Infomaniak calendar sync not available for ${account.email} - ${errorMessage}`); console.log(`[AGENDA] Infomaniak calendar discovery failed for ${account.email} - ${errorMessage}. Calendar will not be created.`);
// 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.`
}
});
}
}
// Continue with other accounts even if one fails // 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 // Clean up duplicate calendars for the same mailCredentialId
// Keep only the most recent one with syncEnabled=true, delete others // Keep only the most recent one with syncEnabled=true, delete others
const allSyncs = await prisma.calendarSync.findMany({ const allSyncs = await prisma.calendarSync.findMany({
where: { where: {
calendar: { calendar: {
userId: session?.user?.id || '' userId: session?.user?.id || ''
},
mailCredentialId: {
in: Array.from(allMailCredentialIds)
} }
}, },
include: { include: {
@ -507,6 +609,24 @@ export default async function CalendarPage() {
// No default calendar creation - only synced calendars from courrier // 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 // Debug: Log calendars before filtering
console.log(`[AGENDA] Calendars before filtering: ${calendars.length}`); console.log(`[AGENDA] Calendars before filtering: ${calendars.length}`);
const infomaniakBeforeFilter = calendars.filter(cal => cal.syncConfig?.provider === 'infomaniak'); const infomaniakBeforeFilter = calendars.filter(cal => cal.syncConfig?.provider === 'infomaniak');

View File

@ -48,8 +48,40 @@ export async function discoverInfomaniakCalendars(
try { try {
const client = await getInfomaniakCalDAVClient(email, password); const client = await getInfomaniakCalDAVClient(email, password);
// List all calendars using PROPFIND on /caldav path // List all calendars using PROPFIND
const items = await client.getDirectoryContents('/caldav'); // 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[] = []; const calendars: CalDAVCalendar[] = [];