From 91c39a030698c7068ffddb57767d255de5c7c409 Mon Sep 17 00:00:00 2001 From: Sascha Schworm <1959209+saschaschworm@users.noreply.github.com> Date: Wed, 14 Sep 2022 18:09:00 +0200 Subject: [PATCH] feat: Exchange with NTLM support (#4127) * add exchange package * fix conflicts * add setup page for v2 * refactor setup page for v1 * return exchange error messages to user if applicable Co-authored-by: Peer Richelsen Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> --- apps/web/public/static/locales/de/common.json | 5 + apps/web/public/static/locales/en/common.json | 5 + packages/app-store/_pages/setup/index.tsx | 1 + packages/app-store/_pages/v2/setup/index.tsx | 1 + packages/app-store/apps.browser.generated.tsx | 3 + packages/app-store/apps.server.generated.ts | 1 + .../app-store/exchangecalendar/README.mdx | 7 + .../app-store/exchangecalendar/_metadata.ts | 15 + .../app-store/exchangecalendar/api/_getAdd.ts | 5 + .../exchangecalendar/api/_postAdd.ts | 45 + .../app-store/exchangecalendar/api/add.ts | 6 + .../app-store/exchangecalendar/api/index.ts | 1 + .../components/InstallAppButton.tsx | 18 + .../exchangecalendar/components/index.ts | 1 + .../app-store/exchangecalendar/config.json | 16 + packages/app-store/exchangecalendar/enums.ts | 4 + packages/app-store/exchangecalendar/index.ts | 3 + .../exchangecalendar/lib/CalendarService.ts | 204 +++++ .../app-store/exchangecalendar/lib/index.ts | 1 + .../app-store/exchangecalendar/package.json | 17 + .../exchangecalendar/pages/setup/index.tsx | 145 ++++ .../exchangecalendar/pages/v2/setup/index.tsx | 149 ++++ .../exchangecalendar/static/icon.svg | 1 + packages/app-store/index.ts | 2 + packages/prisma/seed-app-store.config.json | 8 +- yarn.lock | 786 ++---------------- 26 files changed, 724 insertions(+), 726 deletions(-) create mode 100644 packages/app-store/exchangecalendar/README.mdx create mode 100644 packages/app-store/exchangecalendar/_metadata.ts create mode 100644 packages/app-store/exchangecalendar/api/_getAdd.ts create mode 100644 packages/app-store/exchangecalendar/api/_postAdd.ts create mode 100644 packages/app-store/exchangecalendar/api/add.ts create mode 100644 packages/app-store/exchangecalendar/api/index.ts create mode 100644 packages/app-store/exchangecalendar/components/InstallAppButton.tsx create mode 100644 packages/app-store/exchangecalendar/components/index.ts create mode 100644 packages/app-store/exchangecalendar/config.json create mode 100644 packages/app-store/exchangecalendar/enums.ts create mode 100644 packages/app-store/exchangecalendar/index.ts create mode 100644 packages/app-store/exchangecalendar/lib/CalendarService.ts create mode 100644 packages/app-store/exchangecalendar/lib/index.ts create mode 100644 packages/app-store/exchangecalendar/package.json create mode 100644 packages/app-store/exchangecalendar/pages/setup/index.tsx create mode 100644 packages/app-store/exchangecalendar/pages/v2/setup/index.tsx create mode 100644 packages/app-store/exchangecalendar/static/icon.svg diff --git a/apps/web/public/static/locales/de/common.json b/apps/web/public/static/locales/de/common.json index 6a878e66af..9553ab840e 100644 --- a/apps/web/public/static/locales/de/common.json +++ b/apps/web/public/static/locales/de/common.json @@ -1159,5 +1159,10 @@ "connect_calendar_later": "Ich verbinde meinen Kalender später", "set_my_availability_later": "Ich werde meine Verfügbarkeit später festlegen", "problem_saving_user_profile": "Beim Speichern Ihrer Daten ist ein Problem aufgetreten. Bitte versuchen Sie es erneut oder wenden Sie sich an den Support.", + "exchange_add": "Mit Microsoft Exchange verbinden", + "exchange_authentication": "Authentifizierungsmethode", + "exchange_authentication_standard": "Basisauthentifizierung", + "exchange_authentication_ntlm": "NTLM-Authentifizierung", + "exchange_compression": "GZip-Kompression", "token_invalid_expired": "Token ist entweder ungültig oder abgelaufen." } diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 19ebed1c33..c9f9738acd 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1200,6 +1200,11 @@ "team_disable_cal_branding_description": "Removes any Cal related brandings, i.e. 'Powered by Cal'", "invited_by_team": "{{teamName}} has invited you to join their team as a {{role}}", "token_invalid_expired": "Token is either invalid or expired.", + "exchange_add": "Connect to Microsoft Exchange", + "exchange_authentication": "Authentication method", + "exchange_authentication_standard": "Basic authentication", + "exchange_authentication_ntlm": "NTLM authentication", + "exchange_compression": "GZip compression", "routing_forms_description": "You can see all forms and routes you have created here.", "add_new_form": "Add new form", "form_description": "Create your form to route a booker", diff --git a/packages/app-store/_pages/setup/index.tsx b/packages/app-store/_pages/setup/index.tsx index 09818d602c..bb08cc9e6c 100644 --- a/packages/app-store/_pages/setup/index.tsx +++ b/packages/app-store/_pages/setup/index.tsx @@ -4,6 +4,7 @@ import { DynamicComponent } from "../../_components/DynamicComponent"; export const AppSetupMap = { "apple-calendar": dynamic(() => import("../../applecalendar/pages/setup")), + exchange: dynamic(() => import("../../exchangecalendar/pages/setup")), "exchange2013-calendar": dynamic(() => import("../../exchange2013calendar/pages/setup")), "exchange2016-calendar": dynamic(() => import("../../exchange2016calendar/pages/setup")), "caldav-calendar": dynamic(() => import("../../caldavcalendar/pages/setup")), diff --git a/packages/app-store/_pages/v2/setup/index.tsx b/packages/app-store/_pages/v2/setup/index.tsx index 05de0f9478..4fa3a5e1ac 100644 --- a/packages/app-store/_pages/v2/setup/index.tsx +++ b/packages/app-store/_pages/v2/setup/index.tsx @@ -4,6 +4,7 @@ import { DynamicComponent } from "../../../_components/DynamicComponent"; export const AppSetupMap = { "apple-calendar-V2": dynamic(() => import("../../../applecalendar/pages/v2/setup")), + "exchange-V2": dynamic(() => import("../../../exchangecalendar/pages/v2/setup")), "exchange2013-calendar-V2": dynamic(() => import("../../../exchange2013calendar/pages/v2/setup")), "exchange2016-calendar-V2": dynamic(() => import("../../../exchange2016calendar/pages/v2/setup")), "caldav-calendar-V2": dynamic(() => import("../../../caldavcalendar/pages/v2/setup")), diff --git a/packages/app-store/apps.browser.generated.tsx b/packages/app-store/apps.browser.generated.tsx index dfea6e2e2e..f52c18d67e 100644 --- a/packages/app-store/apps.browser.generated.tsx +++ b/packages/app-store/apps.browser.generated.tsx @@ -13,6 +13,7 @@ import { metadata as dailyvideo_meta } from "./dailyvideo/_metadata"; import { metadata as routing_forms_meta } from "./ee/routing_forms/_metadata"; import { metadata as exchange2013calendar_meta } from "./exchange2013calendar/_metadata"; import { metadata as exchange2016calendar_meta } from "./exchange2016calendar/_metadata"; +import { metadata as exchangecalendar_meta } from "./exchangecalendar/_metadata"; import { metadata as giphy_meta } from "./giphy/_metadata"; import { metadata as googlecalendar_meta } from "./googlecalendar/_metadata"; import { metadata as googlevideo_meta } from "./googlevideo/_metadata"; @@ -47,6 +48,7 @@ export const appStoreMetadata = { routing_forms: routing_forms_meta, exchange2013calendar: exchange2013calendar_meta, exchange2016calendar: exchange2016calendar_meta, + exchangecalendar: exchangecalendar_meta, giphy: giphy_meta, googlecalendar: googlecalendar_meta, googlevideo: googlevideo_meta, @@ -78,6 +80,7 @@ export const InstallAppButtonMap = { closecomothercalendar: dynamic(() => import("./closecomothercalendar/components/InstallAppButton")), exchange2013calendar: dynamic(() => import("./exchange2013calendar/components/InstallAppButton")), exchange2016calendar: dynamic(() => import("./exchange2016calendar/components/InstallAppButton")), + exchangecalendar: dynamic(() => import("./exchangecalendar/components/InstallAppButton")), giphy: dynamic(() => import("./giphy/components/InstallAppButton")), googlecalendar: dynamic(() => import("./googlecalendar/components/InstallAppButton")), hubspotothercalendar: dynamic(() => import("./hubspotothercalendar/components/InstallAppButton")), diff --git a/packages/app-store/apps.server.generated.ts b/packages/app-store/apps.server.generated.ts index 3fbd5a1954..3e0cf5c318 100644 --- a/packages/app-store/apps.server.generated.ts +++ b/packages/app-store/apps.server.generated.ts @@ -11,6 +11,7 @@ export const apiHandlers = { routing_forms: import("./ee/routing_forms/api"), exchange2013calendar: import("./exchange2013calendar/api"), exchange2016calendar: import("./exchange2016calendar/api"), + exchangecalendar: import("./exchangecalendar/api"), giphy: import("./giphy/api"), googlecalendar: import("./googlecalendar/api"), hubspotothercalendar: import("./hubspotothercalendar/api"), diff --git a/packages/app-store/exchangecalendar/README.mdx b/packages/app-store/exchangecalendar/README.mdx new file mode 100644 index 0000000000..3779ddf210 --- /dev/null +++ b/packages/app-store/exchangecalendar/README.mdx @@ -0,0 +1,7 @@ +--- +description: Fetch Microsoft Exchange calendars and availabilities using Exchange Web Services (EWS). +--- + +
+{description} +
\ No newline at end of file diff --git a/packages/app-store/exchangecalendar/_metadata.ts b/packages/app-store/exchangecalendar/_metadata.ts new file mode 100644 index 0000000000..5812a65828 --- /dev/null +++ b/packages/app-store/exchangecalendar/_metadata.ts @@ -0,0 +1,15 @@ +import type { App } from "@calcom/types/App"; + +import config from "./config.json"; + +export const metadata = { + category: "calendar", + installed: true, + rating: 0, + reviews: 0, + trending: true, + verified: true, + ...config, +} as App; + +export default metadata; diff --git a/packages/app-store/exchangecalendar/api/_getAdd.ts b/packages/app-store/exchangecalendar/api/_getAdd.ts new file mode 100644 index 0000000000..e13408668b --- /dev/null +++ b/packages/app-store/exchangecalendar/api/_getAdd.ts @@ -0,0 +1,5 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + return res.status(200).json({ url: "/apps/exchange/setup" }); +} diff --git a/packages/app-store/exchangecalendar/api/_postAdd.ts b/packages/app-store/exchangecalendar/api/_postAdd.ts new file mode 100644 index 0000000000..e7f70a82de --- /dev/null +++ b/packages/app-store/exchangecalendar/api/_postAdd.ts @@ -0,0 +1,45 @@ +import { SoapFaultDetails } from "ews-javascript-api"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { z } from "zod"; + +import { symmetricEncrypt } from "@calcom/lib/crypto"; +import logger from "@calcom/lib/logger"; +import { defaultResponder } from "@calcom/lib/server"; +import prisma from "@calcom/prisma"; + +import checkSession from "../../_utils/auth"; +import { ExchangeAuthentication } from "../enums"; +import { CalendarService } from "../lib"; + +const formSchema = z + .object({ + url: z.string().url(), + username: z.string().email(), + password: z.string(), + authenticationMethod: z.number().default(ExchangeAuthentication.STANDARD), + useCompression: z.boolean().default(false), + }) + .strict(); + +export async function getHandler(req: NextApiRequest, res: NextApiResponse) { + const session = checkSession(req); + const body = formSchema.parse(req.body); + const encrypted = symmetricEncrypt(JSON.stringify(body), process.env.CALENDSO_ENCRYPTION_KEY || ""); + const data = { type: "exchange_calendar", key: encrypted, userId: session.user?.id, appId: "exchange" }; + + try { + const service = new CalendarService({ id: 0, ...data }); + await service?.listCalendars(); + await prisma.credential.create({ data }); + } catch (reason) { + logger.info(reason); + if (reason instanceof SoapFaultDetails && reason.message != "") { + return res.status(500).json({ message: reason.message }); + } + return res.status(500).json({ message: "Could not add this exchange account" }); + } + + return res.status(200).json({ url: "/apps/installed" }); +} + +export default defaultResponder(getHandler); diff --git a/packages/app-store/exchangecalendar/api/add.ts b/packages/app-store/exchangecalendar/api/add.ts new file mode 100644 index 0000000000..9480fb9259 --- /dev/null +++ b/packages/app-store/exchangecalendar/api/add.ts @@ -0,0 +1,6 @@ +import { defaultHandler } from "@calcom/lib/server"; + +export default defaultHandler({ + GET: import("./_getAdd"), + POST: import("./_postAdd"), +}); diff --git a/packages/app-store/exchangecalendar/api/index.ts b/packages/app-store/exchangecalendar/api/index.ts new file mode 100644 index 0000000000..4c0d2ead01 --- /dev/null +++ b/packages/app-store/exchangecalendar/api/index.ts @@ -0,0 +1 @@ +export { default as add } from "./add"; diff --git a/packages/app-store/exchangecalendar/components/InstallAppButton.tsx b/packages/app-store/exchangecalendar/components/InstallAppButton.tsx new file mode 100644 index 0000000000..3e9fa78269 --- /dev/null +++ b/packages/app-store/exchangecalendar/components/InstallAppButton.tsx @@ -0,0 +1,18 @@ +import type { InstallAppButtonProps } from "@calcom/app-store/types"; + +import useAddAppMutation from "../../_utils/useAddAppMutation"; + +export default function InstallAppButton(props: InstallAppButtonProps) { + const mutation = useAddAppMutation("exchange_calendar"); + + return ( + <> + {props.render({ + onClick() { + mutation.mutate(""); + }, + loading: mutation.isLoading, + })} + + ); +} diff --git a/packages/app-store/exchangecalendar/components/index.ts b/packages/app-store/exchangecalendar/components/index.ts new file mode 100644 index 0000000000..0d6008d4ca --- /dev/null +++ b/packages/app-store/exchangecalendar/components/index.ts @@ -0,0 +1 @@ +export { default as InstallAppButton } from "./InstallAppButton"; diff --git a/packages/app-store/exchangecalendar/config.json b/packages/app-store/exchangecalendar/config.json new file mode 100644 index 0000000000..934899c856 --- /dev/null +++ b/packages/app-store/exchangecalendar/config.json @@ -0,0 +1,16 @@ +{ + "/*": "Don't modify slug - If required, do it using cli edit command", + "title": "Microsoft Exchange", + "name": "Microsoft Exchange", + "slug": "exchange", + "type": "exchange_calendar", + "imageSrc": "/api/app-store/exchangecalendar/icon.svg", + "logo": "/api/app-store/exchangecalendar/icon.svg", + "url": "https://cal.com/apps/exchange", + "variant": "calendar", + "categories": ["calendar"], + "publisher": "Cal.com", + "email": "help@cal.com", + "description": "Fetch Microsoft Exchange calendars and availabilities using Exchange Web Services (EWS).", + "__createdUsingCli": true +} diff --git a/packages/app-store/exchangecalendar/enums.ts b/packages/app-store/exchangecalendar/enums.ts new file mode 100644 index 0000000000..795f264163 --- /dev/null +++ b/packages/app-store/exchangecalendar/enums.ts @@ -0,0 +1,4 @@ +export enum ExchangeAuthentication { + STANDARD = 0, + NTLM = 1, +} diff --git a/packages/app-store/exchangecalendar/index.ts b/packages/app-store/exchangecalendar/index.ts new file mode 100644 index 0000000000..5373eb04ef --- /dev/null +++ b/packages/app-store/exchangecalendar/index.ts @@ -0,0 +1,3 @@ +export * as api from "./api"; +export * as lib from "./lib"; +export { metadata } from "./_metadata"; diff --git a/packages/app-store/exchangecalendar/lib/CalendarService.ts b/packages/app-store/exchangecalendar/lib/CalendarService.ts new file mode 100644 index 0000000000..1bb9b3210e --- /dev/null +++ b/packages/app-store/exchangecalendar/lib/CalendarService.ts @@ -0,0 +1,204 @@ +import { XhrApi } from "@ewsjs/xhr"; +import { Credential } from "@prisma/client"; +import { + Appointment, + Attendee, + BasePropertySet, + CalendarView, + ConflictResolutionMode, + DateTime, + DeleteMode, + ExchangeService, + FindFoldersResults, + FindItemsResults, + Folder, + FolderId, + FolderSchema, + FolderTraversal, + FolderView, + ItemId, + LegacyFreeBusyStatus, + LogicalOperator, + MessageBody, + PropertySet, + SearchFilter, + SendInvitationsMode, + SendInvitationsOrCancellationsMode, + Uri, + WebCredentials, + WellKnownFolderName, +} from "ews-javascript-api"; + +import { symmetricDecrypt } from "@calcom/lib/crypto"; +import logger from "@calcom/lib/logger"; +import { + Calendar, + CalendarEvent, + EventBusyDate, + IntegrationCalendar, + NewCalendarEventType, + Person, +} from "@calcom/types/Calendar"; + +import { ExchangeAuthentication } from "../enums"; + +export default class ExchangeCalendarService implements Calendar { + private integrationName = ""; + private log: typeof logger; + private payload; + + constructor(credential: Credential) { + this.integrationName = "exchange_calendar"; + this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] }); + this.payload = JSON.parse( + symmetricDecrypt(credential.key?.toString() || "", process.env.CALENDSO_ENCRYPTION_KEY || "") + ); + } + + async createEvent(event: CalendarEvent): Promise { + const appointment: Appointment = new Appointment(this.getExchangeService()); + appointment.Subject = event.title; + appointment.Start = DateTime.Parse(event.startTime); + appointment.End = DateTime.Parse(event.endTime); + appointment.Location = event.location || ""; + appointment.Body = new MessageBody(event.description || ""); + event.attendees.forEach((attendee: Person) => { + appointment.RequiredAttendees.Add(new Attendee(attendee.email)); + }); + return appointment + .Save(SendInvitationsMode.SendToAllAndSaveCopy) + .then(() => { + return { + uid: appointment.Id.UniqueId, + id: appointment.Id.UniqueId, + password: "", + type: "", + url: "", + additionalInfo: {}, + }; + }) + .catch((reason) => { + this.log.error(reason); + throw reason; + }); + } + + async updateEvent( + uid: string, + event: CalendarEvent + ): Promise { + const appointment: Appointment = await Appointment.Bind(this.getExchangeService(), new ItemId(uid)); + appointment.Subject = event.title; + appointment.Start = DateTime.Parse(event.startTime); + appointment.End = DateTime.Parse(event.endTime); + appointment.Location = event.location || ""; + appointment.Body = new MessageBody(event.description || ""); + event.attendees.forEach((attendee: Person) => { + appointment.RequiredAttendees.Add(new Attendee(attendee.email)); + }); + return appointment + .Update( + ConflictResolutionMode.AlwaysOverwrite, + SendInvitationsOrCancellationsMode.SendToChangedAndSaveCopy + ) + .then(() => { + return { + uid: appointment.Id.UniqueId, + id: appointment.Id.UniqueId, + password: "", + type: "", + url: "", + additionalInfo: {}, + }; + }) + .catch((reason) => { + this.log.error(reason); + throw reason; + }); + } + + async deleteEvent(uid: string): Promise { + const appointment: Appointment = await Appointment.Bind(this.getExchangeService(), new ItemId(uid)); + return appointment.Delete(DeleteMode.MoveToDeletedItems).catch((reason) => { + this.log.error(reason); + throw reason; + }); + } + + async getAvailability( + dateFrom: string, + dateTo: string, + selectedCalendars: IntegrationCalendar[] + ): Promise { + const calendars: IntegrationCalendar[] = await this.listCalendars(); + const promises: Promise[] = calendars + .filter((lcal) => selectedCalendars.some((rcal) => lcal.externalId == rcal.externalId)) + .map(async (calendar) => { + return this.getExchangeService() + .FindAppointments( + new FolderId(calendar.externalId), + new CalendarView(DateTime.Parse(dateFrom), DateTime.Parse(dateTo)) + ) + .then((results: FindItemsResults) => { + return results.Items.filter((appointment: Appointment) => { + return appointment.LegacyFreeBusyStatus != LegacyFreeBusyStatus.Free; + }).map((appointment: Appointment) => { + return { + start: new Date(appointment.Start.ToISOString()), + end: new Date(appointment.End.ToISOString()), + }; + }); + }) + .catch((reason) => { + this.log.error(reason); + throw reason; + }); + }); + return Promise.all(promises).then((x) => x.flat()); + } + + async listCalendars(): Promise { + const service: ExchangeService = this.getExchangeService(); + const view: FolderView = new FolderView(1000); + view.PropertySet = new PropertySet(BasePropertySet.IdOnly); + view.PropertySet.Add(FolderSchema.ParentFolderId); + view.PropertySet.Add(FolderSchema.DisplayName); + view.PropertySet.Add(FolderSchema.ChildFolderCount); + view.Traversal = FolderTraversal.Deep; + const deletedItemsFolder: Folder = await Folder.Bind(service, WellKnownFolderName.DeletedItems); + const searchFilterCollection = new SearchFilter.SearchFilterCollection(LogicalOperator.And); + searchFilterCollection.Add(new SearchFilter.IsEqualTo(FolderSchema.FolderClass, "IPF.Appointment")); + return service + .FindFolders(WellKnownFolderName.MsgFolderRoot, searchFilterCollection, view) + .then((res: FindFoldersResults) => { + return res.Folders.filter((folder: Folder) => { + return folder.ParentFolderId.UniqueId != deletedItemsFolder.Id.UniqueId; + }).map((folder: Folder) => { + return { + externalId: folder.Id.UniqueId, + name: folder.DisplayName ?? "", + primary: folder.ChildFolderCount > 0, + integration: this.integrationName, + }; + }); + }) + .catch((reason) => { + this.log.error(reason); + throw reason; + }); + } + + private getExchangeService(): ExchangeService { + const service: ExchangeService = new ExchangeService(); + service.Credentials = new WebCredentials(this.payload.username, this.payload.password); + service.Url = new Uri(this.payload.url); + if (this.payload.authenticationMethod === ExchangeAuthentication.NTLM) { + const xhr: XhrApi = new XhrApi({ + rejectUnauthorized: false, + gzip: this.payload.useCompression, + }).useNtlmAuthentication(this.payload.username, this.payload.password); + service.XHRApi = xhr; + } + return service; + } +} diff --git a/packages/app-store/exchangecalendar/lib/index.ts b/packages/app-store/exchangecalendar/lib/index.ts new file mode 100644 index 0000000000..e168c149df --- /dev/null +++ b/packages/app-store/exchangecalendar/lib/index.ts @@ -0,0 +1 @@ +export { default as CalendarService } from "./CalendarService"; diff --git a/packages/app-store/exchangecalendar/package.json b/packages/app-store/exchangecalendar/package.json new file mode 100644 index 0000000000..6def8aaa79 --- /dev/null +++ b/packages/app-store/exchangecalendar/package.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "private": true, + "name": "@calcom/exchangecalendar", + "version": "0.0.0", + "main": "./index.ts", + "description": "Fetch Microsoft Exchange calendars and availabilities using Exchange Web Services (EWS).", + "dependencies": { + "@calcom/lib": "*", + "@calcom/prisma": "*", + "@ewsjs/xhr": "^1.4.4", + "ews-javascript-api": "^0.11.0" + }, + "devDependencies": { + "@calcom/types": "*" + } +} diff --git a/packages/app-store/exchangecalendar/pages/setup/index.tsx b/packages/app-store/exchangecalendar/pages/setup/index.tsx new file mode 100644 index 0000000000..4f0750f353 --- /dev/null +++ b/packages/app-store/exchangecalendar/pages/setup/index.tsx @@ -0,0 +1,145 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useRouter } from "next/router"; +import { useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { Toaster } from "react-hot-toast"; +import z from "zod"; + +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Button, Select, Switch } from "@calcom/ui"; +import { Alert } from "@calcom/ui/Alert"; +import { EmailField, Form, Label, PasswordField, TextField } from "@calcom/ui/form/fields"; + +import { ExchangeAuthentication } from "../../enums"; + +interface IFormData { + url: string; + username: string; + password: string; + authenticationMethod: ExchangeAuthentication; + useCompression: boolean; +} + +const schema = z + .object({ + url: z.string().url(), + username: z.string().email(), + password: z.string(), + authenticationMethod: z.number().default(ExchangeAuthentication.STANDARD), + useCompression: z.boolean().default(false), + }) + .strict(); + +export default function ExchangeSetup() { + const { t } = useLocale(); + const router = useRouter(); + const [errorMessage, setErrorMessage] = useState(""); + const form = useForm({ + defaultValues: { authenticationMethod: ExchangeAuthentication.STANDARD }, + resolver: zodResolver(schema), + }); + const authenticationMethods = [ + { value: ExchangeAuthentication.STANDARD, label: t("exchange_authentication_standard") }, + { value: ExchangeAuthentication.NTLM, label: t("exchange_authentication_ntlm") }, + ]; + + return ( + <> +
+
+
+
+ Microsoft Exchange Calendar +
+
+

{t("exchange_add")}

+
{t("credentials_stored_encrypted")}
+
+
{ + setErrorMessage(""); + const res = await fetch("/api/integrations/exchangecalendar/add", { + method: "POST", + body: JSON.stringify(values), + headers: { + "Content-Type": "application/json", + }, + }); + const json = await res.json(); + if (!res.ok) { + setErrorMessage(json?.message || t("something_went_wrong")); + } else { + router.push(json.url); + } + }}> +
+ + + +
+ + ( +