diff --git a/apps/web/pages/api/book/event.ts b/apps/web/pages/api/book/event.ts index a56ca2e051..ba2f2828a0 100644 --- a/apps/web/pages/api/book/event.ts +++ b/apps/web/pages/api/book/event.ts @@ -237,7 +237,8 @@ async function handler(req: NextApiRequest) { throw new HttpError({ statusCode: 400, message: error.message }); } - const eventType = !eventTypeId ? getDefaultEvent(eventTypeSlug) : await getEventTypesFromDB(eventTypeId); + const eventType = + !eventTypeId && !!eventTypeSlug ? getDefaultEvent(eventTypeSlug) : await getEventTypesFromDB(eventTypeId); if (!eventType) throw new HttpError({ statusCode: 404, message: "eventType.notFound" }); let users = !eventTypeId diff --git a/packages/app-store/slackmessaging/api/add.ts b/packages/app-store/slackmessaging/api/add.ts index ec3c190ff5..7cac58db40 100644 --- a/packages/app-store/slackmessaging/api/add.ts +++ b/packages/app-store/slackmessaging/api/add.ts @@ -1,45 +1,41 @@ -import type { NextApiRequest, NextApiResponse } from "next"; +import type { NextApiRequest } from "next"; import { stringify } from "querystring"; +import { HttpError } from "@calcom/lib/http-error"; +import { defaultHandler, defaultResponder } from "@calcom/lib/server"; import prisma from "@calcom/prisma"; -import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug"; +import { getSlackAppKeys } from "../lib/utils"; -let client_id = ""; const scopes = ["commands", "users:read", "users:read.email", "chat:write", "chat:write.public"]; -export default async function handler(req: NextApiRequest, res: NextApiResponse) { +async function handler(req: NextApiRequest) { if (!req.session?.user?.id) { - return res.status(401).json({ message: "You must be logged in to do this" }); + throw new HttpError({ statusCode: 401, message: "You must be logged in to do this" }); } - if (req.method === "GET") { - if (!req.session?.user?.id) { - return res.status(401).json({ message: "You must be logged in to do this" }); - } - - const appKeys = await getAppKeysFromSlug("slack"); - if (typeof appKeys.client_id === "string") client_id = appKeys.client_id; - if (!client_id) return res.status(400).json({ message: "Slack client_id missing" }); - // Get user - await prisma.user.findFirst({ - rejectOnNotFound: true, - where: { - id: req.session.user.id, - }, - select: { - id: true, - }, - }); - const params = { - client_id, - scope: scopes.join(","), - }; - const query = stringify(params); - const url = `https://slack.com/oauth/v2/authorize?${query}&user_`; - // const url = - // "https://slack.com/oauth/v2/authorize?client_id=3194129032064.3178385871204&scope=chat:write,commands&user_scope="; - return res.status(200).json({ url }); - } - return res.status(404).json({ error: "Not Found" }); + const { client_id } = await getSlackAppKeys(); + // Get user + await prisma.user.findFirst({ + rejectOnNotFound: true, + where: { + id: req.session.user.id, + }, + select: { + id: true, + }, + }); + const params = { + client_id, + scope: scopes.join(","), + }; + const query = stringify(params); + const url = `https://slack.com/oauth/v2/authorize?${query}&user_`; + // const url = + // "https://slack.com/oauth/v2/authorize?client_id=3194129032064.3178385871204&scope=chat:write,commands&user_scope="; + return { url }; } + +export default defaultHandler({ + GET: Promise.resolve({ default: defaultResponder(handler) }), +}); diff --git a/packages/app-store/slackmessaging/api/callback.ts b/packages/app-store/slackmessaging/api/callback.ts index ee2f37189f..5e4300168a 100644 --- a/packages/app-store/slackmessaging/api/callback.ts +++ b/packages/app-store/slackmessaging/api/callback.ts @@ -1,57 +1,60 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { stringify } from "querystring"; +import { z } from "zod"; +import { HttpError } from "@calcom/lib/http-error"; +import { defaultHandler, defaultResponder } from "@calcom/lib/server"; import prisma from "@calcom/prisma"; -import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug"; +import { getSlackAppKeys } from "../lib/utils"; -let client_id = ""; -let client_secret = ""; +const callbackQuerySchema = z.object({ + code: z.string().min(1), +}); -export default async function handler(req: NextApiRequest, res: NextApiResponse) { +async function handler(req: NextApiRequest, res: NextApiResponse) { if (!req.session?.user?.id) { - return res.status(401).json({ message: "You must be logged in to do this" }); + throw new HttpError({ statusCode: 401, message: "You must be logged in to do this" }); } - if (req.method === "GET") { - // Get user - const { code } = req.query; + // Get user + const parsedCallbackQuery = callbackQuerySchema.safeParse(req.query); - if (!code) { - res.redirect("/apps/installed"); // Redirect to where the user was if they cancel the signup or if the oauth fails - } + if (!parsedCallbackQuery.success) { + return res.redirect("/apps/installed"); // Redirect to where the user was if they cancel the signup or if the oauth fails + } - const appKeys = await getAppKeysFromSlug("slack"); - if (typeof appKeys.client_id === "string") client_id = appKeys.client_id; - if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret; - if (!client_id) return res.status(400).json({ message: "Slack client_id missing" }); - if (!client_secret) return res.status(400).json({ message: "Slack client_secret missing" }); + const { code } = parsedCallbackQuery.data; + const { client_id, client_secret } = await getSlackAppKeys(); - const query = { - client_secret, - client_id, - code, - }; - const params = stringify(query); - console.log("params", params); + const query = { + client_secret, + client_id, + code, + }; + const params = stringify(query); + const url = `https://slack.com/api/oauth.v2.access?${params}`; + const result = await fetch(url); + const responseBody = await result.json(); - const url = `https://slack.com/api/oauth.v2.access?${params}`; - const result = await fetch(url); - const responseBody = await result.json(); - - await prisma.user.update({ - where: { - id: req.session.user.id, - }, - data: { - credentials: { - create: { - type: "slack_app", - key: responseBody, - }, + await prisma.user.update({ + where: { + id: req.session.user.id, + }, + data: { + credentials: { + create: { + type: "slack_app", + key: responseBody, + appId: "slack", }, }, - }); - return res.redirect("/apps/installed"); - } + }, + }); + + return res.redirect("/apps/installed"); } + +export default defaultHandler({ + GET: Promise.resolve({ default: defaultResponder(handler) }), +}); diff --git a/packages/app-store/slackmessaging/api/commandHandler.ts b/packages/app-store/slackmessaging/api/commandHandler.ts index 6b78eecd17..f3436b08ae 100644 --- a/packages/app-store/slackmessaging/api/commandHandler.ts +++ b/packages/app-store/slackmessaging/api/commandHandler.ts @@ -1,8 +1,10 @@ import type { NextApiRequest, NextApiResponse } from "next"; +import { z } from "zod"; + +import { defaultHandler, defaultResponder } from "@calcom/lib/server"; import { showCreateEventMessage, showTodayMessage } from "../lib"; import showLinksMessage from "../lib/showLinksMessage"; -import slackVerify from "../lib/slackVerify"; export enum SlackAppCommands { CREATE_EVENT = "create-event", @@ -10,20 +12,28 @@ export enum SlackAppCommands { LINKS = "links", } -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method === "POST") { - const command = req.body.command.split("/").pop(); - await slackVerify(req, res); - switch (command) { - case SlackAppCommands.CREATE_EVENT: - return await showCreateEventMessage(req, res); - case SlackAppCommands.TODAY: - return await showTodayMessage(req, res); - case SlackAppCommands.LINKS: - return await showLinksMessage(req, res); - default: - return res.status(404).json({ message: `Command not found` }); - } +const commandHandlerBodySchema = z.object({ + command: z.string().min(1), + user_id: z.string(), + trigger_id: z.string(), + channel_id: z.string().optional(), +}); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const body = commandHandlerBodySchema.parse(req.body); + const command = body.command.split("/").pop(); + switch (command) { + case SlackAppCommands.CREATE_EVENT: + return await showCreateEventMessage(req, res); + case SlackAppCommands.TODAY: + return await showTodayMessage(req, res); + case SlackAppCommands.LINKS: + return await showLinksMessage(req, res); + default: + return res.status(404).json({ message: `Command not found` }); } - return res.status(400).json({ message: "Invalid request" }); } + +export default defaultHandler({ + POST: Promise.resolve({ default: defaultResponder(handler) }), +}); diff --git a/packages/app-store/slackmessaging/api/interactiveHandler.ts b/packages/app-store/slackmessaging/api/interactiveHandler.ts index 25c07b16c8..70968b469d 100644 --- a/packages/app-store/slackmessaging/api/interactiveHandler.ts +++ b/packages/app-store/slackmessaging/api/interactiveHandler.ts @@ -1,7 +1,6 @@ import { NextApiRequest, NextApiResponse } from "next"; import createEvent from "../lib/actions/createEvent"; -import slackVerify from "../lib/slackVerify"; enum InteractionEvents { CREATE_EVENT = "cal.event.create", @@ -9,14 +8,11 @@ enum InteractionEvents { export default async function interactiveHandler(req: NextApiRequest, res: NextApiResponse) { if (req.method === "POST") { - await slackVerify(req, res); const payload = JSON.parse(req.body.payload); const actions = payload.view.callback_id; - - // I've not found a case where actions is ever > than 1 when this function is called. switch (actions) { case InteractionEvents.CREATE_EVENT: - return await createEvent(req, res); + await createEvent(req, res); default: return res.status(200).end(); // Techincally an invalid request but we don't want to return an throw an error to slack - 200 just does nothing } diff --git a/packages/app-store/slackmessaging/lib/actions/createEvent.ts b/packages/app-store/slackmessaging/lib/actions/createEvent.ts index 3530e73686..47456d0da2 100644 --- a/packages/app-store/slackmessaging/lib/actions/createEvent.ts +++ b/packages/app-store/slackmessaging/lib/actions/createEvent.ts @@ -1,46 +1,24 @@ import { WebClient } from "@slack/web-api"; import dayjs from "dayjs"; import { NextApiRequest, NextApiResponse } from "next"; +import { z } from "zod"; import { WEBAPP_URL } from "@calcom/lib/constants"; import db from "@calcom/prisma"; +import type { BookingCreateBody } from "@calcom/prisma/zod-utils"; import { WhereCredsEqualsId } from "../WhereCredsEqualsID"; import { getUserEmail } from "../utils"; -import BookingSuccess from "../views/BookingSuccess"; - -// TODO: Move this type to a shared location - being used in more than one package. -export type BookingCreateBody = { - email: string; - end: string; - web3Details?: { - userWallet: string; - userSignature: unknown; - }; - eventTypeId: number; - guests?: string[]; - location: string; - name: string; - notes?: string; - rescheduleUid?: string; - start: string; - timeZone: string; - user?: string | string[]; - language: string; - customInputs: { label: string; value: string }[]; - metadata: { - [key: string]: string; - }; -}; export default async function createEvent(req: NextApiRequest, res: NextApiResponse) { const { user, view: { state: { values }, + id: view_id, }, + response_url, } = JSON.parse(req.body.payload); - // This is a mess I have no idea why slack makes getting infomation this hard. const { eventName: { @@ -90,9 +68,11 @@ export default async function createEvent(req: NextApiRequest, res: NextApiRespo }, }); - const slackCredentials = foundUser?.credentials[0].key; // Only one slack credential for user + const SlackCredentialsSchema = z.object({ + access_token: z.string(), + }); - // @ts-ignore access_token must exist on slackCredentials otherwise we have wouldnt have reached this endpoint + const slackCredentials = SlackCredentialsSchema.parse(foundUser?.credentials[0].key); // Only one slack credential for user const access_token = slackCredentials?.access_token; // https://api.slack.com/authentication/best-practices#verifying since we verify the request is coming from slack we can store the access_token in the DB. @@ -100,9 +80,7 @@ export default async function createEvent(req: NextApiRequest, res: NextApiRespo // This could get a bit weird as there is a 3 second limit until the post times ou // Compute all users that have been selected and get their email. - const invitedGuestsEmails = selected_users.map( - async (userId: string) => await getUserEmail(client, userId) - ); + const invitedGuestsEmails = selected_users.map((userId: string) => getUserEmail(client, userId)); const startDate = dayjs(`${selected_date} ${selected_time}`, "YYYY-MM-DD HH:mm"); @@ -124,36 +102,18 @@ export default async function createEvent(req: NextApiRequest, res: NextApiRespo notes: "This event was created with slack.", }; - if (startDate < dayjs()) { - client.chat.postMessage({ - token: access_token, - channel: user.id, - text: `Error: Day must not be in the past`, - }); - return res.status(200).send(""); - } - - fetch(`${WEBAPP_URL}/api/book/event`, { + const response = await fetch(`${WEBAPP_URL}/api/book/event`, { method: "POST", body: JSON.stringify(PostData), headers: { "Content-Type": "application/json", }, - }) - .then(() => { - client.chat.postMessage({ - token: access_token, - channel: user.id, // We just dm the user here as there is no point posting this message publicly - In future it might be worth pinging all the members of the invite also? - text: "Booking has been created.", - }); - return res.status(200).send(""); // Slack requires a 200 to be sent to clear the modal. This makes it massive pain to update the user that the event has been created. - }) - .catch((e) => { - client.chat.postMessage({ - token: access_token, - channel: user.id, - text: `Error: ${e}`, - }); - return res.status(200).send(""); - }); + }); + const body = await response.json(); + client.chat.postMessage({ + token: access_token, + channel: user.id, + text: body.errorCode ? `Error: ${body.errorCode}` : "Booking has been created.", + }); + return res.status(200).send(""); } diff --git a/packages/app-store/slackmessaging/lib/showCreateEventMessage.ts b/packages/app-store/slackmessaging/lib/showCreateEventMessage.ts index a712af81b5..4d9b8baeed 100644 --- a/packages/app-store/slackmessaging/lib/showCreateEventMessage.ts +++ b/packages/app-store/slackmessaging/lib/showCreateEventMessage.ts @@ -5,10 +5,12 @@ import { NextApiRequest, NextApiResponse } from "next"; import prisma from "@calcom/prisma"; import { WhereCredsEqualsId } from "./WhereCredsEqualsID"; +import slackVerify from "./slackVerify"; import { CreateEventModal, NoUserMessage } from "./views"; export default async function showCreateEventMessage(req: NextApiRequest, res: NextApiResponse) { const body = req.body; + await slackVerify(req, res); const data = await prisma.credential.findFirst({ ...WhereCredsEqualsId(body.user_id), @@ -35,5 +37,5 @@ export default async function showCreateEventMessage(req: NextApiRequest, res: N trigger_id: body.trigger_id, view: CreateEventModal(data), }); - res.status(200).end(); + return res.status(200).end(); } diff --git a/packages/app-store/slackmessaging/lib/showLinksMessage.ts b/packages/app-store/slackmessaging/lib/showLinksMessage.ts index 6747180dbf..8ef711bed3 100644 --- a/packages/app-store/slackmessaging/lib/showLinksMessage.ts +++ b/packages/app-store/slackmessaging/lib/showLinksMessage.ts @@ -1,15 +1,17 @@ import { Prisma } from "@prisma/client"; -import { KnownBlock, WebClient } from "@slack/web-api"; +import { WebClient } from "@slack/web-api"; import { NextApiRequest, NextApiResponse } from "next"; import prisma from "@calcom/prisma"; import { WhereCredsEqualsId } from "./WhereCredsEqualsID"; -import { CreateEventModal, NoUserMessage } from "./views"; +import slackVerify from "./slackVerify"; +import { NoUserMessage } from "./views"; import ShowLinks from "./views/ShowLinks"; export default async function showLinksMessage(req: NextApiRequest, res: NextApiResponse) { const body = req.body; + await slackVerify(req, res); const data = await prisma.credential.findFirst({ ...WhereCredsEqualsId(body.user_id), @@ -40,9 +42,8 @@ export default async function showLinksMessage(req: NextApiRequest, res: NextApi slackClient.chat.postMessage({ channel: body.channel_id, text: `${data.user?.username}'s Cal.com Links`, - //@ts-ignore this doesnt need to be of type Block[] - an object works completely fine blocks, }); - res.status(200).end(); + return res.status(200).end(); } diff --git a/packages/app-store/slackmessaging/lib/showTodayMessage.ts b/packages/app-store/slackmessaging/lib/showTodayMessage.ts index 9809c1075a..3a62b83325 100644 --- a/packages/app-store/slackmessaging/lib/showTodayMessage.ts +++ b/packages/app-store/slackmessaging/lib/showTodayMessage.ts @@ -5,11 +5,12 @@ import { NextApiRequest, NextApiResponse } from "next"; import prisma from "@calcom/prisma"; import { WhereCredsEqualsId } from "./WhereCredsEqualsID"; +import slackVerify from "./slackVerify"; import { NoUserMessage, TodayMessage } from "./views"; export default async function showCreateEventMessage(req: NextApiRequest, res: NextApiResponse) { const body = req.body; - + await slackVerify(req, res); const foundUser = await prisma.credential.findFirst({ ...WhereCredsEqualsId(body.user_id), include: { @@ -50,5 +51,5 @@ export default async function showCreateEventMessage(req: NextApiRequest, res: N }, }); - res.status(200).json(TodayMessage(bookings)); + return res.status(200).json(TodayMessage(bookings)); } diff --git a/packages/app-store/slackmessaging/lib/slackVerify.ts b/packages/app-store/slackmessaging/lib/slackVerify.ts index fc43b5020b..52116d6ff9 100644 --- a/packages/app-store/slackmessaging/lib/slackVerify.ts +++ b/packages/app-store/slackmessaging/lib/slackVerify.ts @@ -3,17 +3,14 @@ import dayjs from "dayjs"; import { NextApiRequest, NextApiResponse } from "next"; import { stringify } from "querystring"; -import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug"; - -let signingSecret = ""; +import { getSlackAppKeys } from "./utils"; export default async function slackVerify(req: NextApiRequest, res: NextApiResponse) { - const body = req.body; const timeStamp = req.headers["x-slack-request-timestamp"] as string; // Always returns a string and not a string[] const slackSignature = req.headers["x-slack-signature"] as string; const currentTime = dayjs().unix(); - let { signing_secret } = await getAppKeysFromSlug("slack"); - if (typeof signing_secret === "string") signingSecret = signing_secret; + const { signing_secret: signingSecret } = await getSlackAppKeys(); + const [version, hash] = slackSignature.split("="); if (!timeStamp) { return res.status(400).json({ message: "Missing X-Slack-Request-Timestamp header" }); @@ -27,10 +24,13 @@ export default async function slackVerify(req: NextApiRequest, res: NextApiRespo return res.status(400).json({ message: "Request is too old" }); } - const signature_base = `v0:${timeStamp}:${stringify(body)}`; - const signed_sig = "v0=" + createHmac("sha256", signingSecret).update(signature_base).digest("hex"); + const hmac = createHmac("sha256", signingSecret); - if (signed_sig !== slackSignature) { - return res.status(400).json({ message: "Invalid signature" }); + hmac.update(`${version}:${timeStamp}:${stringify(req.body)}`); + + const signed_sig = hmac.digest("hex"); + console.log({ signed_sig, hash, match: signed_sig === hash }); + if (signed_sig !== hash) { + throw new Error("Hashes do not match "); } } diff --git a/packages/app-store/slackmessaging/lib/utils.ts b/packages/app-store/slackmessaging/lib/utils.ts index 0a518377ef..6345e7cf51 100644 --- a/packages/app-store/slackmessaging/lib/utils.ts +++ b/packages/app-store/slackmessaging/lib/utils.ts @@ -1,8 +1,18 @@ import { WebClient } from "@slack/web-api"; +import { z } from "zod"; -const getUserEmail = async (client: WebClient, userId: string) => - await ( - await client.users.info({ user: userId }) - ).user?.profile?.email; +import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug"; -export { getUserEmail }; +export const getUserEmail = async (client: WebClient, userId: string) => + (await client.users.info({ user: userId })).user?.profile?.email; + +const slackAppKeysSchema = z.object({ + client_id: z.string(), + client_secret: z.string(), + signing_secret: z.string(), +}); + +export const getSlackAppKeys = async () => { + const appKeys = await getAppKeysFromSlug("slack"); + return slackAppKeysSchema.parse(appKeys); +}; diff --git a/packages/app-store/slackmessaging/lib/views/CreateEventModal.ts b/packages/app-store/slackmessaging/lib/views/CreateEventModal.ts index 70196a988c..c7997c38ca 100644 --- a/packages/app-store/slackmessaging/lib/views/CreateEventModal.ts +++ b/packages/app-store/slackmessaging/lib/views/CreateEventModal.ts @@ -13,7 +13,7 @@ const CreateEventModal = ( } | null; }) | null, - invalidInput: boolean = false + invalidInput = false ) => { return Modal({ title: "Create Booking", submit: "Create", callbackId: "cal.event.create" }) .blocks( diff --git a/packages/app-store/slackmessaging/lib/views/TodayMessage.ts b/packages/app-store/slackmessaging/lib/views/TodayMessage.ts index 8105c4efae..af304d0638 100644 --- a/packages/app-store/slackmessaging/lib/views/TodayMessage.ts +++ b/packages/app-store/slackmessaging/lib/views/TodayMessage.ts @@ -1,15 +1,15 @@ import { Booking } from "@prisma/client"; import dayjs from "dayjs"; -import { Modal, Blocks, Elements, Bits, Message } from "slack-block-builder"; +import { Blocks, Elements, Message } from "slack-block-builder"; -import { BASE_URL } from "@calcom/lib/constants"; +import { WEBAPP_URL } from "@calcom/lib/constants"; const TodayMessage = (bookings: Booking[]) => { if (bookings.length === 0) { return Message() .blocks(Blocks.Section({ text: "You do not have any bookings for today." })) .asUser() - .buildToJSON(); + .buildToObject(); } return Message() .blocks( @@ -18,7 +18,7 @@ const TodayMessage = (bookings: Booking[]) => { bookings.map((booking) => Blocks.Section({ text: `${booking.title} | ${dayjs(booking.startTime).format("HH:mm")}`, - }).accessory(Elements.Button({ text: "Cancel", url: `${BASE_URL}/cancel/${booking.uid}` })) + }).accessory(Elements.Button({ text: "Cancel", url: `${WEBAPP_URL}/cancel/${booking.uid}` })) ) ) .buildToObject(); diff --git a/packages/app-store/slackmessaging/package.json b/packages/app-store/slackmessaging/package.json index 9d9cb9c0a7..92ee6b72f1 100644 --- a/packages/app-store/slackmessaging/package.json +++ b/packages/app-store/slackmessaging/package.json @@ -8,7 +8,8 @@ "dependencies": { "@calcom/prisma": "*", "@slack/web-api": "^6.7.0", - "slack-block-builder": "^2.5.0" + "slack-block-builder": "^2.5.0", + "zod": "^3.17.3" }, "devDependencies": { "@calcom/types": "*" diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index 162e760ef4..da4cef381c 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -67,7 +67,7 @@ export const bookingCreateBodySchema = z.object({ }) .optional(), eventTypeId: z.number(), - eventTypeSlug: z.string(), + eventTypeSlug: z.string().optional(), guests: z.array(z.string()).optional(), location: z.string(), name: z.string(), @@ -81,10 +81,12 @@ export const bookingCreateBodySchema = z.object({ bookingUid: z.string().optional(), customInputs: z.array(z.object({ label: z.string(), value: z.union([z.string(), z.boolean()]) })), metadata: z.record(z.string()), - hasHashedBookingLink: z.boolean(), + hasHashedBookingLink: z.boolean().optional(), hashedLink: z.string().nullish(), }); +export type BookingCreateBody = z.input; + export const extendedBookingCreateBody = bookingCreateBodySchema.merge( z.object({ noEmail: z.boolean().optional(),