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
// 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');

View File

@ -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[] = [];