// @ts-nocheck /* eslint-disable @typescript-eslint/no-explicit-any */ // import { GoogleCalendarEvent as CalendarEvent } from './google-calendar'; export interface OutlookCalendar { id: string; name: string; isDefaultCalendar: boolean; canEdit: boolean; color?: string; hexColor?: string; owner: { name: string; address: string; }; } // Microsoft Graph API calendar color names → hex values const OUTLOOK_COLOR_MAP: Record = { auto: '#0078d4', lightBlue: '#69afe5', lightGreen: '#7bd148', lightOrange: '#ffb878', lightGray: '#b3b3b3', lightYellow: '#fbd75b', lightTeal: '#92e1c0', lightPink: '#f691b2', lightBrown: '#c2a282', lightRed: '#ff887c', maxColor: '#0078d4', }; const GRAPH_ENDPOINT = 'https://graph.microsoft.com/v1.0'; const REDIRECT_URI = process.env.MICROSOFT_REDIRECT_URI || `${process.env.NEXTAUTH_URL}/api/calendar/outlook/callback`; /** * Generate OAuth2 Authorization URL */ export const getAuthUrl = () => { const tenant = 'common'; const clientId = process.env.MICROSOFT_CLIENT_ID; if (!clientId) throw new Error('MICROSOFT_CLIENT_ID is not defined'); const scopes = [ 'offline_access', 'user.read', 'Calendars.ReadWrite', 'Tasks.ReadWrite' ].join(' '); const params = new URLSearchParams({ client_id: clientId, response_type: 'code', redirect_uri: REDIRECT_URI, response_mode: 'query', scope: scopes, state: 'outlook-auth' // Can be random for security }); return `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize?${params.toString()}`; }; /** * Exchange Authorization Code for Tokens */ export const getTokens = async (code: string) => { const tenant = 'common'; const clientId = process.env.MICROSOFT_CLIENT_ID; const clientSecret = process.env.MICROSOFT_CLIENT_SECRET; if (!clientId || !clientSecret) throw new Error('Microsoft credentials not defined'); const params = new URLSearchParams({ client_id: clientId, scope: 'offline_access user.read Calendars.ReadWrite Tasks.ReadWrite', code: code, redirect_uri: REDIRECT_URI, grant_type: 'authorization_code', client_secret: clientSecret }); const response = await fetch(`https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: params.toString() }); if (!response.ok) { const error = await response.text(); console.error('Error getting Outlook tokens:', error); throw new Error(`Failed to get tokens: ${response.statusText}`); } return response.json(); }; /** * Refresh Access Token */ export const refreshAccessToken = async (refreshToken: string) => { const tenant = 'common'; const clientId = process.env.MICROSOFT_CLIENT_ID; const clientSecret = process.env.MICROSOFT_CLIENT_SECRET; if (!clientId || !clientSecret) throw new Error('Microsoft credentials not defined'); const params = new URLSearchParams({ client_id: clientId, scope: 'offline_access user.read Calendars.ReadWrite Tasks.ReadWrite', refresh_token: refreshToken, redirect_uri: REDIRECT_URI, grant_type: 'refresh_token', client_secret: clientSecret }); const response = await fetch(`https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: params.toString() }); if (!response.ok) { const error = await response.text(); console.error('Error refreshing Outlook token:', error); throw new Error(`Failed to refresh token: ${response.statusText}`); } return response.json(); }; /** * Get User's Calendars */ export const getUserCalendars = async (accessToken: string): Promise => { const response = await fetch(`${GRAPH_ENDPOINT}/me/calendars`, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Prefer': 'outlook.timezone="UTC"' } }); if (!response.ok) { throw new Error(`Failed to fetch calendars: ${response.statusText}`); } const data = await response.json(); return (data.value || []).map((cal: any) => ({ ...cal, hexColor: cal.hexColor || OUTLOOK_COLOR_MAP[cal.color] || OUTLOOK_COLOR_MAP['auto'], })); }; /** * Get Upcoming Events */ export const getUpcomingEvents = async ( accessToken: string, calendarId: string, startDateTime: string, endDateTime: string ) => { const params = new URLSearchParams({ startDateTime: startDateTime, endDateTime: endDateTime, '$select': 'subject,bodyPreview,start,end,location,webLink,isAllDay,seriesMasterId,type,reminderMinutesBeforeStart,isReminderOn,showAs,sensitivity,attendees', '$orderby': 'start/dateTime', '$top': '50' }); let response: Response | null = null; for (let attempt = 0; attempt < 3; attempt++) { response = await fetch( `${GRAPH_ENDPOINT}/me/calendars/${encodeURIComponent(calendarId)}/calendarView?${params.toString()}`, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Prefer': 'outlook.timezone="UTC"' } } ); if (response.status === 429) { const retryAfter = parseInt(response.headers.get('Retry-After') || '', 10); const delay = (retryAfter > 0 ? retryAfter : (attempt + 1) * 2) * 1000; console.warn(`[OUTLOOK] 429 throttled for calendar ${calendarId}, retrying in ${delay}ms (attempt ${attempt + 1}/3)`); await new Promise(r => setTimeout(r, delay)); continue; } break; } if (!response!.ok) { const errorText = await response!.text(); console.error(`[OUTLOOK] Failed to fetch events for calendar ${calendarId}:`, response!.status, response!.statusText, errorText); throw new Error(`Failed to fetch events: ${response!.status} ${response!.statusText}`); } const data = await response.json(); return data.value.map((event: any) => { // Map Outlook showAs to our busyStatus const showAsMap: Record = { 'free': 'free', 'tentative': 'tentative', 'busy': 'busy', 'oof': 'oof', 'workingElsewhere': 'workingElsewhere', }; // Map Outlook sensitivity to our visibility const sensitivityMap: Record = { 'normal': 'default', 'personal': 'default', 'private': 'private', 'confidential': 'confidential', }; return { id: event.seriesMasterId ? `${event.seriesMasterId}::${event.id}` : event.id, summary: event.subject, description: event.body?.content || event.bodyPreview, start: { dateTime: event.start.dateTime, timeZone: event.start.timeZone }, end: { dateTime: event.end.dateTime, timeZone: event.end.timeZone }, location: event.location?.displayName, htmlLink: event.webLink, allDay: event.isAllDay, reminders: event.isReminderOn && event.reminderMinutesBeforeStart != null ? [{ method: 'popup', minutes: event.reminderMinutesBeforeStart }] : undefined, busyStatus: showAsMap[event.showAs] || undefined, visibility: sensitivityMap[event.sensitivity] || undefined, attendees: event.attendees?.map((a: any) => ({ email: a.emailAddress?.address, displayName: a.emailAddress?.name, responseStatus: a.status?.response === 'accepted' ? 'accepted' : a.status?.response === 'declined' ? 'declined' : a.status?.response === 'tentativelyAccepted' ? 'tentative' : 'needsAction', })), }; }); }; const ensureTimeZone = (dateTimeObj: any) => { if (!dateTimeObj) return dateTimeObj; return { dateTime: dateTimeObj.dateTime, timeZone: dateTimeObj.timeZone || 'UTC' }; }; /** * Create Outlook Event */ export const createEvent = async ( accessToken: string, calendarId: string, event: any ) => { const response = await fetch(`${GRAPH_ENDPOINT}/me/calendars/${encodeURIComponent(calendarId)}/events`, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ subject: event.summary, body: { contentType: 'HTML', content: event.description || '' }, start: ensureTimeZone(event.start), end: ensureTimeZone(event.end), isAllDay: !!event.allDay, location: { displayName: event.location || '' }, ...(event.recurrence ? { recurrence: event.recurrence } : {}), ...(event.reminders?.length ? { isReminderOn: true, reminderMinutesBeforeStart: event.reminders[0].minutes, } : {}), ...(event.busyStatus ? { showAs: event.busyStatus === 'oof' ? 'oof' : event.busyStatus } : {}), ...(event.visibility ? { sensitivity: event.visibility === 'private' ? 'private' : event.visibility === 'confidential' ? 'confidential' : 'normal' } : {}), ...(event.attendees?.length ? { attendees: event.attendees.map((a: any) => ({ emailAddress: { address: a.email, name: a.displayName || a.email }, type: 'required', })), } : {}), }) }); if (!response.ok) { const err = await response.text(); throw new Error(`Failed to create Outlook event: ${err}`); } const created = await response.json(); return { id: created.id, summary: created.subject, description: created.bodyPreview, start: created.start, end: created.end, location: created.location?.displayName, allDay: created.isAllDay }; }; /** * Update Outlook Event */ export const updateEvent = async ( accessToken: string, calendarId: string, // Not strictly needed for Graph ID-based update but kept for interface consistency eventId: string, event: any ) => { const response = await fetch(`${GRAPH_ENDPOINT}/me/events/${encodeURIComponent(eventId)}`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ subject: event.summary, body: { contentType: 'HTML', content: event.description || '' }, start: ensureTimeZone(event.start), end: ensureTimeZone(event.end), isAllDay: event.allDay !== undefined ? !!event.allDay : undefined, location: { displayName: event.location || '' }, ...(event.recurrence ? { recurrence: event.recurrence } : {}), ...(event.reminders?.length ? { isReminderOn: true, reminderMinutesBeforeStart: event.reminders[0].minutes, } : {}), ...(event.busyStatus ? { showAs: event.busyStatus === 'oof' ? 'oof' : event.busyStatus } : {}), ...(event.visibility ? { sensitivity: event.visibility === 'private' ? 'private' : event.visibility === 'confidential' ? 'confidential' : 'normal' } : {}), ...(event.attendees?.length ? { attendees: event.attendees.map((a: any) => ({ emailAddress: { address: a.email, name: a.displayName || a.email }, type: 'required', })), } : {}), }) }); if (!response.ok) { const err = await response.text(); throw new Error(`Failed to update Outlook event: ${err}`); } const updated = await response.json(); return { id: updated.id, summary: updated.subject, description: updated.bodyPreview, start: updated.start, end: updated.end, location: updated.location?.displayName, allDay: updated.isAllDay }; }; /** * Delete Outlook Event */ export const deleteEvent = async ( accessToken: string, calendarId: string, eventId: string ) => { const response = await fetch(`${GRAPH_ENDPOINT}/me/events/${encodeURIComponent(eventId)}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${accessToken}` } }); if (!response.ok) { const err = await response.text(); throw new Error(`Failed to delete Outlook event: ${err}`); } };