-
Notifications
You must be signed in to change notification settings - Fork 12k
feat: outlook cache and change notifications webhook #21071
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
5e278ac
d943688
fedbc89
098bf9c
ca57c22
2838393
cce73d5
644928e
9f7f7d1
b091251
9cff138
eb5bc3b
9429428
706ca02
5d72ca7
fbe8ff2
983fe5b
a8a095a
fcb9e61
955f95b
27876cb
42a91ce
01cbb88
8af0a3a
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,106 @@ | ||
| import type { NextApiRequest, NextApiResponse } from "next"; | ||
|
|
||
| import { uniqueBy } from "@calcom/lib/array"; | ||
| import { getCredentialForCalendarCache } from "@calcom/lib/delegationCredential/server"; | ||
| import logger from "@calcom/lib/logger"; | ||
| import { safeStringify } from "@calcom/lib/safeStringify"; | ||
| import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar"; | ||
|
|
||
| import { getCalendar } from "../../_utils/getCalendar"; | ||
| import { graphValidationTokenChallengeSchema, changeNotificationWebhookPayloadSchema } from "../zod"; | ||
|
|
||
| const log = logger.getSubLogger({ prefix: ["Office365CalendarWebhook"] }); | ||
|
|
||
| interface WebhookResponse { | ||
| [key: string]: { | ||
| processed: boolean; | ||
| message?: string; | ||
| }; | ||
| } | ||
|
|
||
| function isApiKeyValid(clientState?: string) { | ||
| return clientState === process.env.OUTLOOK_WEBHOOK_TOKEN; | ||
| } | ||
|
|
||
| function getValidationToken(req: NextApiRequest) { | ||
| const graphValidationTokenChallengeParseRes = graphValidationTokenChallengeSchema.safeParse(req.query); | ||
| if (!graphValidationTokenChallengeParseRes.success) { | ||
| return null; | ||
| } | ||
|
|
||
| return graphValidationTokenChallengeParseRes.data.validationToken; | ||
| } | ||
|
|
||
| async function postHandler(req: NextApiRequest, res: NextApiResponse) { | ||
| const validationToken = getValidationToken(req); | ||
| if (validationToken) { | ||
| res.setHeader("Content-Type", "text/plain"); | ||
| return res.status(200).send(validationToken); | ||
| } | ||
|
Comment on lines
+35
to
+39
Contributor
Author
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. When you create a subscription, microsoft send a validation request to the webhook url with a More info here: https://learn.microsoft.com/en-us/graph/change-notifications-delivery-webhooks?tabs=http#notificationurl-validation |
||
|
|
||
| const webhookBodyParseRes = changeNotificationWebhookPayloadSchema.safeParse(req.body); | ||
|
|
||
| if (!webhookBodyParseRes.success) { | ||
| log.error("postHandler", safeStringify(webhookBodyParseRes.error)); | ||
|
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. Rule violated: Avoid Logging Sensitive Information
Contributor
Author
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. I can remove this or change to something else if wanted |
||
| return res.status(400).json({ | ||
| message: "Invalid request body", | ||
| }); | ||
| } | ||
|
|
||
| const events = webhookBodyParseRes.data.value; | ||
| const body: WebhookResponse = {}; | ||
|
|
||
| const uniqueEvents = uniqueBy(events, ["subscriptionId"]); | ||
|
|
||
| const promises = uniqueEvents.map(async (event) => { | ||
|
Comment on lines
+50
to
+55
Contributor
Author
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. Microsoft send a
I'm removing duplicate I'm not sure if the way I'm handling each event and adding the result to a body object is the best, but let me know if you have a different idea. |
||
| if (!isApiKeyValid(event.clientState)) { | ||
| body[event.subscriptionId] = { | ||
| processed: false, | ||
| message: "Invalid API key", | ||
| }; | ||
| return; | ||
| } | ||
|
|
||
| const selectedCalendar = await SelectedCalendarRepository.findByOutlookSubscriptionId( | ||
| event.subscriptionId | ||
| ); | ||
| if (!selectedCalendar) { | ||
| body[event.subscriptionId] = { | ||
| processed: false, | ||
| message: `No selected calendar found for outlookSubscriptionId: ${event.subscriptionId}`, | ||
| }; | ||
| return; | ||
| } | ||
|
|
||
| const { credential } = selectedCalendar; | ||
| if (!credential) { | ||
| body[event.subscriptionId] = { | ||
| processed: false, | ||
| message: `No credential found for selected calendar for outlookSubscriptionId: ${event.subscriptionId}`, | ||
| }; | ||
| return; | ||
| } | ||
|
|
||
| const { selectedCalendars } = credential; | ||
| const credentialForCalendarCache = await getCredentialForCalendarCache({ credentialId: credential.id }); | ||
| const calendarServiceForCalendarCache = await getCalendar(credentialForCalendarCache); | ||
|
|
||
| await calendarServiceForCalendarCache?.fetchAvailabilityAndSetCache?.(selectedCalendars); | ||
| body[event.subscriptionId] = { | ||
| processed: true, | ||
| message: "ok", | ||
| }; | ||
| return; | ||
| }); | ||
|
|
||
| await Promise.all(promises); | ||
|
|
||
| return res.status(200).json(body); | ||
| } | ||
|
|
||
| export default async function handler(req: NextApiRequest, res: NextApiResponse) { | ||
| if (req.method === "POST") return postHandler(req, res); | ||
|
|
||
| res.setHeader("Allow", "POST"); | ||
| return res.status(405).json({}); | ||
| } | ||
|
Comment on lines
+101
to
+106
Contributor
Author
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. I couldn't use the |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing guidance on token generation requirements