@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
JavaScript
;
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