diff --git a/app/api/events/[id]/route.ts b/app/api/events/[id]/route.ts index 33597ff..21a8e54 100644 --- a/app/api/events/[id]/route.ts +++ b/app/api/events/[id]/route.ts @@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "next-auth/next"; import { authOptions } from "@/app/api/auth/options"; import { prisma } from "@/lib/prisma"; +import { deleteMicrosoftEvent } from "@/lib/services/microsoft-calendar-sync"; +import { logger } from "@/lib/logger"; // Helper function to check if user can manage events in a mission calendar async function canManageMissionCalendar(userId: string, calendarId: string): Promise { @@ -45,10 +47,20 @@ export async function DELETE(req: NextRequest, props: { params: Promise<{ id: st } try { - // First, find the event and its associated calendar + // First, find the event and its associated calendar with sync config const event = await prisma.event.findUnique({ where: { id: params.id }, - include: { calendar: true }, + include: { + calendar: { + include: { + syncConfig: { + include: { + mailCredential: true + } + } + } + } + }, }); if (!event) { @@ -77,7 +89,41 @@ export async function DELETE(req: NextRequest, props: { params: Promise<{ id: st } } - // Delete the event + // If event has externalEventId and calendar has Microsoft sync, delete from Microsoft too + if (event.externalEventId && event.calendar.syncConfig && event.calendar.syncConfig.provider === 'microsoft' && event.calendar.syncConfig.syncEnabled) { + const syncConfig = event.calendar.syncConfig; + const mailCredential = syncConfig.mailCredential; + + if (mailCredential && mailCredential.use_oauth && mailCredential.refresh_token) { + try { + await deleteMicrosoftEvent( + session.user.id, + mailCredential.email, + syncConfig.externalCalendarId || '', + event.externalEventId + ); + + logger.info('Successfully synced event deletion to Microsoft', { + eventId: params.id, + externalEventId: event.externalEventId, + email: mailCredential.email, + }); + } catch (syncError: any) { + // Log error but don't fail the request - local deletion will proceed + // Don't disable syncConfig for permission errors (403) - user just needs to re-authenticate + const isPermissionError = syncError.response?.status === 403; + logger.error('Failed to sync event deletion to Microsoft', { + eventId: params.id, + externalEventId: event.externalEventId, + error: syncError instanceof Error ? syncError.message : String(syncError), + isPermissionError, + suggestion: isPermissionError ? 'User needs to re-authenticate with Calendars.ReadWrite scope' : undefined, + }); + } + } + } + + // Delete the event from local database await prisma.event.delete({ where: { id: params.id }, }); diff --git a/app/api/events/route.ts b/app/api/events/route.ts index 52488d8..ed49874 100644 --- a/app/api/events/route.ts +++ b/app/api/events/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "next-auth/next"; import { authOptions } from "@/app/api/auth/options"; import { prisma } from "@/lib/prisma"; -import { updateMicrosoftEvent } from "@/lib/services/microsoft-calendar-sync"; +import { updateMicrosoftEvent, deleteMicrosoftEvent } from "@/lib/services/microsoft-calendar-sync"; import { logger } from "@/lib/logger"; // Helper function to check if user can manage events in a mission calendar @@ -263,10 +263,14 @@ export async function PUT(req: NextRequest) { }); } catch (syncError: any) { // Log error but don't fail the request - local update succeeded + // Don't disable syncConfig for permission errors (403) - user just needs to re-authenticate + const isPermissionError = syncError.response?.status === 403; logger.error('Failed to sync event update to Microsoft', { eventId: id, externalEventId: existingEvent.externalEventId, error: syncError instanceof Error ? syncError.message : String(syncError), + isPermissionError, + suggestion: isPermissionError ? 'User needs to re-authenticate with Calendars.ReadWrite scope' : undefined, }); } } diff --git a/lib/services/microsoft-calendar-sync.ts b/lib/services/microsoft-calendar-sync.ts index e230b7f..9cbf96e 100644 --- a/lib/services/microsoft-calendar-sync.ts +++ b/lib/services/microsoft-calendar-sync.ts @@ -448,6 +448,54 @@ export async function updateMicrosoftEvent( } } +/** + * Delete a Microsoft calendar event via Graph API + */ +export async function deleteMicrosoftEvent( + userId: string, + email: string, + calendarId: string, + eventId: string +): Promise { + try { + const accessToken = await getMicrosoftGraphClient(userId, email); + + const url = `https://graph.microsoft.com/v1.0/me/calendars/${calendarId}/events/${eventId}`; + + logger.info('Deleting Microsoft event', { + userId, + email, + calendarId, + eventId, + url, + }); + + await axios.delete(url, { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }); + + logger.info('Successfully deleted Microsoft event', { + userId, + email, + eventId, + }); + } catch (error: any) { + logger.error('Error deleting Microsoft event', { + userId, + email, + calendarId, + eventId, + error: error instanceof Error ? error.message : String(error), + responseStatus: error.response?.status, + responseData: error.response?.data, + }); + throw error; + } +} + /** * Sync events from Microsoft calendar to local Prisma calendar */ diff --git a/lib/services/microsoft-oauth.ts b/lib/services/microsoft-oauth.ts index 172e400..a590a45 100644 --- a/lib/services/microsoft-oauth.ts +++ b/lib/services/microsoft-oauth.ts @@ -21,6 +21,7 @@ const REQUIRED_SCOPES = [ 'https://graph.microsoft.com/Mail.Read', // Read mail via Graph API 'https://graph.microsoft.com/Mail.Send', // Send mail via Graph API 'https://graph.microsoft.com/Calendars.Read', // Read calendars via Graph API + 'https://graph.microsoft.com/Calendars.ReadWrite', // Read and write calendars via Graph API (for updates/deletes) ].join(' '); /**