UNPKG

@checkfirst/nestjs-outlook

Version:

An opinionated NestJS module for Microsoft Outlook integration that provides easy access to Microsoft Graph API for emails, calendars, and more.

347 lines 18.2 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; var CalendarService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.CalendarService = void 0; const common_1 = require("@nestjs/common"); const event_emitter_1 = require("@nestjs/event-emitter"); const microsoft_graph_client_1 = require("@microsoft/microsoft-graph-client"); const axios_1 = require("axios"); const microsoft_auth_service_1 = require("../auth/microsoft-auth.service"); const schedule_1 = require("@nestjs/schedule"); const outlook_webhook_subscription_repository_1 = require("../../repositories/outlook-webhook-subscription.repository"); const outlook_delta_link_repository_1 = require("../../repositories/outlook-delta-link.repository"); const constants_1 = require("../../constants"); const event_types_enum_1 = require("../../enums/event-types.enum"); const typeorm_1 = require("@nestjs/typeorm"); const microsoft_user_entity_1 = require("../../entities/microsoft-user.entity"); const typeorm_2 = require("typeorm"); const delta_sync_service_1 = require("../shared/delta-sync.service"); const shared_user_service_1 = require("../shared/shared-user.service"); let CalendarService = CalendarService_1 = class CalendarService { constructor(microsoftAuthService, webhookSubscriptionRepository, eventEmitter, microsoftConfig, deltaLinkRepository, microsoftUserRepository, deltaSyncService) { this.microsoftAuthService = microsoftAuthService; this.webhookSubscriptionRepository = webhookSubscriptionRepository; this.eventEmitter = eventEmitter; this.microsoftConfig = microsoftConfig; this.deltaLinkRepository = deltaLinkRepository; this.microsoftUserRepository = microsoftUserRepository; this.deltaSyncService = deltaSyncService; this.logger = new common_1.Logger(CalendarService_1.name); } async getDefaultCalendarId(externalUserId) { try { const accessToken = await this.microsoftAuthService.getUserAccessTokenByExternalUserId(externalUserId); const response = await axios_1.default.get("https://graph.microsoft.com/v1.0/me/calendar", { headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, }); if (!response.data.id) { throw new Error("Failed to retrieve calendar ID"); } return response.data.id; } catch (error) { this.logger.error("Error getting default calendar ID:", error); throw new Error("Failed to get calendar ID from Microsoft"); } } async createEvent(event, externalUserId, calendarId) { try { const accessToken = await this.microsoftAuthService.getUserAccessTokenByExternalUserId(externalUserId); const client = microsoft_graph_client_1.Client.init({ authProvider: (done) => { done(null, accessToken); }, }); const createdEvent = (await client .api(`/me/calendars/${calendarId}/events`) .post(event)); return { event: createdEvent, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; this.logger.error(`Failed to create Outlook calendar event: ${errorMessage}`); throw new Error(`Failed to create Outlook calendar event: ${errorMessage}`); } } async createWebhookSubscription(externalUserId) { try { const accessToken = await this.microsoftAuthService.getUserAccessTokenByExternalUserId(externalUserId); const expirationDateTime = new Date(); expirationDateTime.setHours(expirationDateTime.getHours() + 72); const appUrl = this.microsoftConfig.backendBaseUrl || "http://localhost:3000"; const basePath = this.microsoftConfig.basePath; const basePathUrl = basePath ? `${appUrl}/${basePath}` : appUrl; const notificationUrl = `${basePathUrl}/calendar/webhook`; const subscriptionData = { changeType: "created,updated,deleted", notificationUrl, lifecycleNotificationUrl: notificationUrl, resource: "/me/events", expirationDateTime: expirationDateTime.toISOString(), clientState: `user_${externalUserId}_${Math.random().toString(36).substring(2, 15)}`, }; this.logger.debug(`Creating webhook subscription with notificationUrl: ${notificationUrl}`); this.logger.debug(`Subscription data: ${JSON.stringify(subscriptionData)}`); const response = await axios_1.default.post("https://graph.microsoft.com/v1.0/subscriptions", subscriptionData, { headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, }); this.logger.log(`Created webhook subscription ${response.data.id || "unknown"} for user ${externalUserId}`); const internalUserId = parseInt(externalUserId, 10); await this.webhookSubscriptionRepository.saveSubscription({ subscriptionId: response.data.id, userId: internalUserId, resource: response.data.resource, changeType: response.data.changeType, clientState: response.data.clientState || "", notificationUrl: response.data.notificationUrl, expirationDateTime: response.data.expirationDateTime ? new Date(response.data.expirationDateTime) : new Date(), }); this.logger.debug(`Stored subscription`); return response.data; } catch (error) { this.logger.error("Failed to create webhook subscription:", error); throw new Error("Failed to create webhook subscription"); } } async renewWebhookSubscription(subscriptionId, externalUserId) { try { const accessToken = await this.microsoftAuthService.getUserAccessTokenByExternalUserId(externalUserId); const expirationDateTime = new Date(); expirationDateTime.setHours(expirationDateTime.getHours() + 72); const renewalData = { expirationDateTime: expirationDateTime.toISOString(), }; const response = await axios_1.default.patch(`https://graph.microsoft.com/v1.0/subscriptions/${subscriptionId}`, renewalData, { headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, }); if (response.data.expirationDateTime) { await this.webhookSubscriptionRepository.updateSubscriptionExpiration(subscriptionId, new Date(response.data.expirationDateTime)); } this.logger.log(`Renewed webhook subscription: ${subscriptionId}`); return response.data; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; this.logger.error(`Failed to renew webhook subscription: ${errorMessage}`); throw new Error(`Failed to renew webhook subscription: ${errorMessage}`); } } async renewWebhookSubscriptionByUserId(subscriptionId, internalUserId) { try { const accessToken = await this.microsoftAuthService.getUserAccessTokenByUserId(internalUserId); const expirationDateTime = new Date(); expirationDateTime.setHours(expirationDateTime.getHours() + 72); const renewalData = { expirationDateTime: expirationDateTime.toISOString(), }; const response = await axios_1.default.patch(`https://graph.microsoft.com/v1.0/subscriptions/${subscriptionId}`, renewalData, { headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, }); if (response.data.expirationDateTime) { await this.webhookSubscriptionRepository.updateSubscriptionExpiration(subscriptionId, new Date(response.data.expirationDateTime)); } this.logger.log(`Renewed webhook subscription: ${subscriptionId}`); return response.data; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; this.logger.error(`Failed to renew webhook subscription: ${errorMessage}`); throw new Error(`Failed to renew webhook subscription: ${errorMessage}`); } } async deleteWebhookSubscription(subscriptionId, externalUserId) { var _a; try { const accessToken = await this.microsoftAuthService.getUserAccessTokenByExternalUserId(externalUserId); await axios_1.default.delete(`https://graph.microsoft.com/v1.0/subscriptions/${subscriptionId}`, { headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, }); await this.webhookSubscriptionRepository.deactivateSubscription(subscriptionId); this.logger.log(`Deleted webhook subscription: ${subscriptionId}`); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; this.logger.error(`Failed to delete webhook subscription: ${errorMessage}`); if (axios_1.default.isAxiosError(error) && ((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 404) { await this.webhookSubscriptionRepository.deactivateSubscription(subscriptionId); this.logger.log(`Subscription not found, removed from database: ${subscriptionId}`); return true; } throw new Error(`Failed to delete webhook subscription: ${errorMessage}`); } } async renewSubscriptions() { try { const expiringSubscriptions = await this.webhookSubscriptionRepository.findSubscriptionsNeedingRenewal(24); if (expiringSubscriptions.length === 0) { this.logger.debug("No subscriptions need renewal"); return; } this.logger.log(`Found ${String(expiringSubscriptions.length)} subscriptions that need renewal`); for (const subscription of expiringSubscriptions) { try { await this.renewWebhookSubscriptionByUserId(subscription.subscriptionId, subscription.userId); } catch (error) { this.logger.error(`Failed to renew subscription ${subscription.subscriptionId}:`, error); } } } catch (error) { this.logger.error("Error in subscription renewal job:", error); } } async handleOutlookWebhook(notificationItem) { var _a; try { const { subscriptionId, clientState, resource, changeType } = notificationItem; this.logger.debug(`Received webhook notification for subscription: ${subscriptionId || "unknown"}`); this.logger.debug(`Resource: ${resource || "unknown"}, ChangeType: ${String(changeType || "unknown")}`); const subscription = await this.webhookSubscriptionRepository.findBySubscriptionId(subscriptionId || ""); if (!subscription) { this.logger.warn(`Unknown subscription ID: ${subscriptionId || "unknown"}`); return { success: false, message: "Unknown subscription" }; } if (subscription.clientState && clientState !== subscription.clientState) { this.logger.warn("Client state mismatch"); return { success: false, message: "Client state mismatch" }; } const userId = subscription.userId; if (!userId) { this.logger.warn("Could not determine user ID from client state"); return { success: false, message: "Invalid client state format" }; } const externalUserId = await (0, shared_user_service_1.getExternalUserIdFromUserId)(userId, this.microsoftUserRepository, this.logger); if (!externalUserId) { this.logger.warn(`Could not determine externalUserId for user ID ${String(userId)}`); return { success: false, message: "Could not determine external user ID", }; } const sortedChanges = await this.fetchAndSortChanges(String(externalUserId)); for (const change of sortedChanges) { let eventType; if (change["@removed"]) { eventType = event_types_enum_1.OutlookEventTypes.EVENT_DELETED; } else if (!change.createdDateTime || new Date(change.createdDateTime).getTime() === new Date((_a = change.lastModifiedDateTime) !== null && _a !== void 0 ? _a : change.createdDateTime).getTime()) { eventType = event_types_enum_1.OutlookEventTypes.EVENT_CREATED; } else { eventType = event_types_enum_1.OutlookEventTypes.EVENT_UPDATED; } const resourceData = { id: change.id || "", userId, subscriptionId, resource, changeType: eventType === "outlook.event.deleted" ? "deleted" : eventType === "outlook.event.created" ? "created" : "updated", data: change, }; this.eventEmitter.emit(eventType, resourceData); this.logger.log(`Processed calendar change: ${eventType} for event ID: ${change.id || "unknown"}`); } return { success: true, message: "Notification processed" }; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; this.logger.error(`Error processing webhook notification: ${errorMessage}`); return { success: false, message: errorMessage }; } } async fetchAndSortChanges(externalUserId) { const client = await this.getAuthenticatedClient(externalUserId); const requestUrl = "/me/events/delta"; try { const events = await this.deltaSyncService.fetchAndSortChanges(client, requestUrl); return events; } catch (error) { this.logger.error("Error fetching delta changes:", error); throw error; } } async getAuthenticatedClient(externalUserId) { const accessToken = await this.microsoftAuthService.getUserAccessTokenByExternalUserId(externalUserId); return microsoft_graph_client_1.Client.init({ authProvider: (done) => { done(null, accessToken); }, }); } async getEventDetails(resource, externalUserId) { try { const accessToken = await this.microsoftAuthService.getUserAccessTokenByExternalUserId(externalUserId); const response = await axios_1.default.get(`https://graph.microsoft.com/v1.0/${resource}`, { headers: { Authorization: `Bearer ${accessToken}`, }, }); return response.data; } catch (error) { this.logger.error("Error fetching event details:", error); throw error; } } }; exports.CalendarService = CalendarService; __decorate([ (0, schedule_1.Cron)(schedule_1.CronExpression.EVERY_HOUR), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], CalendarService.prototype, "renewSubscriptions", null); exports.CalendarService = CalendarService = CalendarService_1 = __decorate([ (0, common_1.Injectable)(), __param(0, (0, common_1.Inject)((0, common_1.forwardRef)(() => microsoft_auth_service_1.MicrosoftAuthService))), __param(3, (0, common_1.Inject)(constants_1.MICROSOFT_CONFIG)), __param(5, (0, typeorm_1.InjectRepository)(microsoft_user_entity_1.MicrosoftUser)), __metadata("design:paramtypes", [microsoft_auth_service_1.MicrosoftAuthService, outlook_webhook_subscription_repository_1.OutlookWebhookSubscriptionRepository, event_emitter_1.EventEmitter2, Object, outlook_delta_link_repository_1.OutlookDeltaLinkRepository, typeorm_2.Repository, delta_sync_service_1.DeltaSyncService]) ], CalendarService); //# sourceMappingURL=calendar.service.js.map