-
Notifications
You must be signed in to change notification settings - Fork 12k
feat: Outlook Calendar Caching with Webhook Notifications for Calendar Changes #22604
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d703a7f
65d0ea1
5207def
00b25a5
7058711
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| export { default as add } from "./add"; | ||
| export { default as callback } from "./callback"; | ||
| export { default as webhook } from "./webhook"; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,96 @@ | ||||||||||||||||||||||||||||||||||
| import type { NextApiRequest, NextApiResponse } from "next"; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| import { getCredentialForCalendarCache } from "@calcom/lib/delegationCredential/server"; | ||||||||||||||||||||||||||||||||||
| import { HttpError } from "@calcom/lib/http-error"; | ||||||||||||||||||||||||||||||||||
| import logger from "@calcom/lib/logger"; | ||||||||||||||||||||||||||||||||||
| import { safeStringify } from "@calcom/lib/safeStringify"; | ||||||||||||||||||||||||||||||||||
| import { defaultHandler } from "@calcom/lib/server/defaultHandler"; | ||||||||||||||||||||||||||||||||||
| import { defaultResponder } from "@calcom/lib/server/defaultResponder"; | ||||||||||||||||||||||||||||||||||
| import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar"; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| import { getCalendar } from "../../_utils/getCalendar"; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const log = logger.getSubLogger({ prefix: ["office365calendar", "webhook"] }); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| async function getHandler(req: NextApiRequest, res: NextApiResponse) { | ||||||||||||||||||||||||||||||||||
| const validationToken = req.query.validationToken as string; | ||||||||||||||||||||||||||||||||||
| if (!validationToken) { | ||||||||||||||||||||||||||||||||||
| throw new HttpError({ statusCode: 400, message: "Missing validation token" }); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| res.setHeader("Content-Type", "text/plain"); | ||||||||||||||||||||||||||||||||||
| res.status(200).send(validationToken); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| async function postHandler(req: NextApiRequest, res: NextApiResponse) { | ||||||||||||||||||||||||||||||||||
| const validationToken = req.query.validationToken; | ||||||||||||||||||||||||||||||||||
| if (validationToken && typeof validationToken === "string") { | ||||||||||||||||||||||||||||||||||
| res.setHeader("Content-Type", "text/plain"); | ||||||||||||||||||||||||||||||||||
| res.status(200).send(validationToken); | ||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const { value: notifications } = req.body; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (!notifications || !Array.isArray(notifications)) { | ||||||||||||||||||||||||||||||||||
| throw new HttpError({ statusCode: 400, message: "Invalid notification payload" }); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const expectedClientState = process.env.OFFICE365_WEBHOOK_CLIENT_STATE; | ||||||||||||||||||||||||||||||||||
| if (!expectedClientState) { | ||||||||||||||||||||||||||||||||||
| log.error("OFFICE365_WEBHOOK_CLIENT_STATE not configured"); | ||||||||||||||||||||||||||||||||||
| throw new HttpError({ statusCode: 500, message: "Webhook not configured" }); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| for (const notification of notifications) { | ||||||||||||||||||||||||||||||||||
| if (notification.clientState !== expectedClientState) { | ||||||||||||||||||||||||||||||||||
| throw new HttpError({ statusCode: 403, message: "Invalid client state" }); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+45
to
+49
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add protection against timing attacks in client state validation. The client state comparison should use constant-time comparison to prevent timing attacks. +import crypto from "crypto";
+
for (const notification of notifications) {
- if (notification.clientState !== expectedClientState) {
+ // Use constant-time comparison to prevent timing attacks
+ if (!crypto.timingSafeEqual(
+ Buffer.from(notification.clientState || ''),
+ Buffer.from(expectedClientState)
+ )) {
throw new HttpError({ statusCode: 403, message: "Invalid client state" });
}
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| for (const notification of notifications) { | ||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||
| const { subscriptionId, resource } = notification; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (!subscriptionId || !resource) { | ||||||||||||||||||||||||||||||||||
| log.warn("Notification missing required fields"); | ||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| log.debug("Processing notification", { subscriptionId }); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const selectedCalendar = await SelectedCalendarRepository.findFirstByOffice365SubscriptionId( | ||||||||||||||||||||||||||||||||||
| subscriptionId | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (!selectedCalendar) { | ||||||||||||||||||||||||||||||||||
| log.debug("No selected calendar found for subscription", { subscriptionId }); | ||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const { credential } = selectedCalendar; | ||||||||||||||||||||||||||||||||||
| if (!credential) { | ||||||||||||||||||||||||||||||||||
| log.debug("No credential found for selected calendar", { subscriptionId }); | ||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const { selectedCalendars } = credential; | ||||||||||||||||||||||||||||||||||
| const credentialForCalendarCache = await getCredentialForCalendarCache({ credentialId: credential.id }); | ||||||||||||||||||||||||||||||||||
| const calendarServiceForCalendarCache = await getCalendar(credentialForCalendarCache); | ||||||||||||||||||||||||||||||||||
| await calendarServiceForCalendarCache?.fetchAvailabilityAndSetCache?.(selectedCalendars); | ||||||||||||||||||||||||||||||||||
| log.debug("Successfully updated calendar cache", { subscriptionId }); | ||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||
| log.error( | ||||||||||||||||||||||||||||||||||
| "Error processing notification", | ||||||||||||||||||||||||||||||||||
| safeStringify({ error, subscriptionId: notification.subscriptionId }) | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return { message: "ok" }; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+25
to
+91
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add request size and rate limiting protection. The webhook endpoint should validate request size and implement rate limiting to prevent abuse. Consider implementing:
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| export default defaultHandler({ | ||||||||||||||||||||||||||||||||||
| GET: Promise.resolve({ default: defaultResponder(getHandler) }), | ||||||||||||||||||||||||||||||||||
| POST: Promise.resolve({ default: defaultResponder(postHandler) }), | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.