From 2372f8c3e8f1554318209059edf386a53f494a8c Mon Sep 17 00:00:00 2001 From: Andrea Santillana Date: Sun, 24 Nov 2024 00:38:49 -0600 Subject: [PATCH] add webhooks for calendly events --- .../migration.sql | 8 +++ prisma/schema.prisma | 19 +++--- src/lib/oauth.ts | 59 +++++++++++++++++++ src/pages/api/oauth/callback.ts | 35 +++++++++++ src/types/oauth.schema.ts | 19 ++++++ 5 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 prisma/migrations/20241124062556_oauth_external_user_id/migration.sql create mode 100644 src/pages/api/oauth/callback.ts diff --git a/prisma/migrations/20241124062556_oauth_external_user_id/migration.sql b/prisma/migrations/20241124062556_oauth_external_user_id/migration.sql new file mode 100644 index 00000000..b5d26ef1 --- /dev/null +++ b/prisma/migrations/20241124062556_oauth_external_user_id/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `externalUserId` to the `UserOauth` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "UserOauth" ADD COLUMN "externalUserId" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index fa82f686..8939f6a6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -30,15 +30,16 @@ enum OauthProvider { } model UserOauth { - id String @id @default(uuid()) - userAuthId String - UserAuth UserAuth @relation(fields: [userAuthId], references: [id]) - provider OauthProvider - accessToken String - expiresAt DateTime - refreshToken String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(uuid()) + userAuthId String + UserAuth UserAuth @relation(fields: [userAuthId], references: [id]) + provider OauthProvider + accessToken String + expiresAt DateTime + refreshToken String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + externalUserId String @@unique([userAuthId, provider]) } diff --git a/src/lib/oauth.ts b/src/lib/oauth.ts index 567ffb4b..1302b6b9 100644 --- a/src/lib/oauth.ts +++ b/src/lib/oauth.ts @@ -57,6 +57,7 @@ export class GCloud { accessToken: json["access_token"] as string, refreshToken: refreshToken || (json["refresh_token"] as string), expiresAt: new Date(Date.now() + expiresInSeconds * 1000), + externalUserId: "test", }; } @@ -146,11 +147,13 @@ export class Calendly { throw new Error("Calendly API error"); } const expiresInSeconds = Number(json["expires_in"]); + const externalUserId = json["owner"].split("/").pop(); return { provider: OauthProvider.CALENDLY, accessToken: json["access_token"] as string, refreshToken: json["refresh_token"] as string, expiresAt: new Date(Date.now() + expiresInSeconds * 1000), + externalUserId: externalUserId, }; } @@ -268,6 +271,18 @@ async function findUserOauth( }); } +async function findUserOauthByExternalId( + oauthExternalUserId: string, + provider: OauthProvider, +): Promise { + return prisma.userOauth.findUnique({ + where: { + externalUserId: oauthExternalUserId, + provider: provider, + }, + }); +} + export async function getAccessToken( userAuthId: string, provider: OauthProvider, @@ -326,3 +341,47 @@ export async function disconnectOauth({ }); return res; } + +export async function oauthProcessCallback({ + uri, + callback_url, + created_at, + updated_at, + retry_started_at, + state, + events, + scope, + organization, + user, + group, + creator, + }: { + uri: string, + callback_url: string, + created_at: string, + updated_at: string, + retry_started_at: string, + state: string, + events: string, + scope: string, + organization: string, + user: string, + group: string, + creator: string, +}): Promise { + const oauthInfo = await findUserOauth(userAuthId, provider); + if (!oauthInfo) { + return false; + } + const intf = providerIntf(provider); + const res = await intf.revoke(oauthInfo); + await prisma.userOauth.delete({ + where: { + userAuthId_provider: { + userAuthId, + provider, + }, + }, + }); + return res; +} \ No newline at end of file diff --git a/src/pages/api/oauth/callback.ts b/src/pages/api/oauth/callback.ts new file mode 100644 index 00000000..8178521e --- /dev/null +++ b/src/pages/api/oauth/callback.ts @@ -0,0 +1,35 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { Value } from "@sinclair/typebox/value"; +import { CalendlyWebhookSubscriptionRequestSchema, DisconnectOauthProviderRequestSchema } from "@/types/oauth.schema"; +import { BadRequestError } from "@/types/errors"; +import { disconnectOauth } from "@/lib/oauth"; +import { provider } from "std-env"; + +async function OauthProviderCallbackHandler( + req: NextApiRequest, + res: NextApiResponse<{ success: boolean } | BadRequestError>, +): Promise { + const { body } = req; + if (!Value.Check(CalendlyWebhookSubscriptionRequestSchema, body)) { + return res.status(400).json({ message: "Invalid inputs" }); + } + const { uri, callback_url, created_at, updated_at, retry_started_at, state, events, scope, organization, user, group, creator} = body; + + const status = await disconnectOauth({ + userAuthId, + provider, + }); + + return res.status(200).json({ success: status }); +} + +export default async function handle( + req: NextApiRequest, + res: NextApiResponse<{ success: boolean } | BadRequestError>, +): Promise { + if (req.method === "POST") { + await disconnectOauthProviderHandler(req, res); + } else { + return res.status(405).json({ message: "Method Not allowed" }); + } +} diff --git a/src/types/oauth.schema.ts b/src/types/oauth.schema.ts index f138e6a1..d01e4ed4 100644 --- a/src/types/oauth.schema.ts +++ b/src/types/oauth.schema.ts @@ -10,3 +10,22 @@ export const DisconnectOauthProviderRequestSchema = Type.Object({ // Password of user provider: Type.Enum(OauthProvider), }); + +export type CalendlyWebhookSubscriptionRequestSchema = Static< + typeof CalendlyWebhookSubscriptionRequestSchema +>; + +export const CalendlyWebhookSubscriptionRequestSchema = Type.Object({ + uri: Type.String(), + callback_url: Type.String(), + created_at: Type.String(), + updated_at: Type.String(), + retry_started_at: Type.String(), + state: Type.String(), + events: Type.Array(Type.String()), + scope: Type.String(), + organization: Type.String(), + user: Type.String(), + group: Type.String(), + creator: Type.String(), +});