Agenda Sync refactor

This commit is contained in:
alma 2026-01-14 15:55:36 +01:00
parent 50cdca1ac2
commit 1bdafdf408
3 changed files with 61 additions and 88 deletions

View File

@ -273,10 +273,11 @@ export default async function CalendarPage() {
} }
} }
} catch (error) { } catch (error) {
console.error(`Error auto-setting up sync for Microsoft account ${account.email}:`, error); // Microsoft sync setup failed - likely because account doesn't have calendar scope yet
// Don't fail the page if Microsoft sync setup fails // This is expected for accounts authenticated before calendar scope was added
// The account might not have the calendar scope yet, or there might be a token issue // User will need to re-authenticate their Microsoft account to get calendar access
// User can manually set up sync later if needed console.log(`Microsoft calendar sync not available for ${account.email} - account may need re-authentication with calendar permissions`);
// Don't fail the page - continue with other accounts
} }
} }
} }

View File

@ -47,92 +47,52 @@ export async function discoverInfomaniakCalendars(
try { try {
const client = await getInfomaniakCalDAVClient(email, password); const client = await getInfomaniakCalDAVClient(email, password);
// Try to discover calendar home set using CalDAV discovery // List all calendars using PROPFIND on root
// First, try to find the calendar home set using current-user-principal const items = await client.getDirectoryContents('/');
let calendarHomePath = '/calendars/';
// Extract username from email (before @) or use email as username const calendars: CalDAVCalendar[] = [];
// Infomaniak might use the email as username or require the Infomaniak username
const username = email.split('@')[0];
// Try different paths for (const item of items) {
const possiblePaths = [ if (item.type === 'directory' && item.filename !== '/') {
`/calendars/${username}/`, // Get calendar properties
`/calendars/${email}/`, try {
'/calendars/', const props = await client.customRequest(item.filename, {
'/', method: 'PROPFIND',
]; headers: {
Depth: '0',
let calendars: CalDAVCalendar[] = []; 'Content-Type': 'application/xml',
},
for (const path of possiblePaths) { data: `<?xml version="1.0" encoding="utf-8" ?>
try {
// List all calendars using PROPFIND with Depth: 1
const items = await client.getDirectoryContents(path);
for (const item of items) {
if (item.type === 'directory' && item.filename !== '/' && item.filename !== path) {
// Get calendar properties
try {
const props = await client.customRequest(item.filename, {
method: 'PROPFIND',
headers: {
Depth: '0',
'Content-Type': 'application/xml',
},
data: `<?xml version="1.0" encoding="utf-8" ?>
<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav"> <d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
<d:prop> <d:prop>
<d:displayname /> <d:displayname />
<c:calendar-color /> <c:calendar-color />
<d:resourcetype />
</d:prop> </d:prop>
</d:propfind>`, </d:propfind>`,
}); });
// Check if it's a calendar (has calendar resource type) // Parse XML response to extract calendar name and color
const isCalendar = typeof props.data === 'string' && const displayName = extractDisplayName(props.data);
(props.data.includes('<c:calendar') || props.data.includes('calendar')); const color = extractCalendarColor(props.data);
if (isCalendar || item.type === 'directory') { calendars.push({
// Parse XML response to extract calendar name and color id: item.filename.replace(/^\//, '').replace(/\/$/, ''),
const displayName = extractDisplayName(props.data); name: displayName || item.basename || 'Calendrier',
const color = extractCalendarColor(props.data); url: item.filename,
color: color,
calendars.push({ });
id: item.filename.replace(/^\//, '').replace(/\/$/, ''), } catch (error) {
name: displayName || item.basename || 'Calendrier', logger.error('Error fetching calendar properties', {
url: item.filename, calendar: item.filename,
color: color, error: error instanceof Error ? error.message : String(error),
}); });
} // Still add the calendar with default name
} catch (error) { calendars.push({
logger.debug('Error fetching calendar properties', { id: item.filename.replace(/^\//, '').replace(/\/$/, ''),
calendar: item.filename, name: item.basename || 'Calendrier',
error: error instanceof Error ? error.message : String(error), url: item.filename,
}); });
// Still add the calendar with default name if it's a directory
if (item.type === 'directory') {
calendars.push({
id: item.filename.replace(/^\//, '').replace(/\/$/, ''),
name: item.basename || 'Calendrier',
url: item.filename,
});
}
}
}
} }
// If we found calendars, break
if (calendars.length > 0) {
break;
}
} catch (pathError) {
// Try next path
logger.debug(`Path ${path} failed, trying next`, {
error: pathError instanceof Error ? pathError.message : String(pathError),
});
continue;
} }
} }

View File

@ -39,10 +39,12 @@ async function getMicrosoftGraphClient(
email: string email: string
): Promise<string> { ): Promise<string> {
// Ensure we have a fresh access token // Ensure we have a fresh access token
// Note: The token might not have calendar scope if the account was authenticated before calendar scope was added
// In that case, the user will need to re-authenticate
const { accessToken, success } = await ensureFreshToken(userId, email); const { accessToken, success } = await ensureFreshToken(userId, email);
if (!success || !accessToken) { if (!success || !accessToken) {
throw new Error('Failed to obtain valid Microsoft access token'); throw new Error('Failed to obtain valid Microsoft access token. The account may need to be re-authenticated with calendar permissions.');
} }
return accessToken; return accessToken;
@ -84,21 +86,31 @@ export async function discoverMicrosoftCalendars(
return calendars; return calendars;
} catch (error: any) { } catch (error: any) {
// Check if error is due to missing calendar scope // Check if error is due to missing calendar scope or invalid audience
if (error.response?.status === 403 || error.response?.status === 401) { if (error.response?.status === 403 || error.response?.status === 401) {
logger.warn('Microsoft calendar access denied - may need to re-authenticate with calendar scope', { const errorMessage = error.response?.data?.error?.message || error.message || '';
userId, const needsReauth = errorMessage.includes('Invalid audience') ||
email, errorMessage.includes('insufficient_privileges') ||
error: error.response?.data?.error?.message || error.message, errorMessage.includes('invalid_token');
});
// Return empty array instead of throwing - user can re-authenticate later if (needsReauth) {
return []; logger.warn('Microsoft calendar access denied - account needs re-authentication with calendar scope', {
userId,
email,
error: errorMessage,
});
// Return empty array - user needs to re-authenticate their Microsoft account
// The account was authenticated before calendar scope was added
return [];
}
} }
logger.error('Error discovering Microsoft calendars', { logger.error('Error discovering Microsoft calendars', {
userId, userId,
email, email,
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),
responseStatus: error.response?.status,
responseData: error.response?.data,
}); });
// Return empty array instead of throwing to avoid breaking the page // Return empty array instead of throwing to avoid breaking the page
return []; return [];