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 <peeroke@gmail.com> Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
This commit is contained in:
co-authored by
Peer Richelsen
Joe Au-Yeung
parent
f1c15f4e8a
commit
91c39a0306
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")),
|
||||
|
||||
@@ -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")),
|
||||
|
||||
@@ -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")),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
description: Fetch Microsoft Exchange calendars and availabilities using Exchange Web Services (EWS).
|
||||
---
|
||||
|
||||
<div>
|
||||
{description}
|
||||
</div>
|
||||
@@ -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;
|
||||
@@ -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" });
|
||||
}
|
||||
@@ -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);
|
||||
@@ -0,0 +1,6 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
export default defaultHandler({
|
||||
GET: import("./_getAdd"),
|
||||
POST: import("./_postAdd"),
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export { default as add } from "./add";
|
||||
@@ -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,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as InstallAppButton } from "./InstallAppButton";
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum ExchangeAuthentication {
|
||||
STANDARD = 0,
|
||||
NTLM = 1,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * as api from "./api";
|
||||
export * as lib from "./lib";
|
||||
export { metadata } from "./_metadata";
|
||||
@@ -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<NewCalendarEventType> {
|
||||
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<NewCalendarEventType | NewCalendarEventType[]> {
|
||||
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<void> {
|
||||
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<EventBusyDate[]> {
|
||||
const calendars: IntegrationCalendar[] = await this.listCalendars();
|
||||
const promises: Promise<EventBusyDate[]>[] = 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<Appointment>) => {
|
||||
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<IntegrationCalendar[]> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as CalendarService } from "./CalendarService";
|
||||
@@ -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": "*"
|
||||
}
|
||||
}
|
||||
@@ -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<IFormData>({
|
||||
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 (
|
||||
<>
|
||||
<div className="flex h-screen bg-gray-200">
|
||||
<div className="m-auto rounded bg-white p-5 md:w-[560px] md:p-10">
|
||||
<div className="flex flex-col space-y-5 md:flex-row md:space-y-0 md:space-x-5">
|
||||
<div className="flex">
|
||||
<img
|
||||
src="/api/app-store/exchangecalendar/icon.svg"
|
||||
alt="Microsoft Exchange Calendar"
|
||||
className="h-12 w-12 max-w-2xl"
|
||||
/>
|
||||
</div>
|
||||
<div className="grow">
|
||||
<h1 className="text-gray-600">{t("exchange_add")}</h1>
|
||||
<div className="text-sm">{t("credentials_stored_encrypted")}</div>
|
||||
<div className="my-2 mt-5">
|
||||
<Form
|
||||
form={form}
|
||||
handleSubmit={async (values) => {
|
||||
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);
|
||||
}
|
||||
}}>
|
||||
<fieldset className="space-y-4" disabled={form.formState.isSubmitting}>
|
||||
<TextField
|
||||
required
|
||||
type="url"
|
||||
{...form.register("url")}
|
||||
label={t("url")}
|
||||
placeholder="https://example.com/Ews/Exchange.asmx"
|
||||
inputMode="url"
|
||||
/>
|
||||
<EmailField
|
||||
required
|
||||
{...form.register("username")}
|
||||
label={t("email_address")}
|
||||
placeholder="john.doe@example.com"
|
||||
/>
|
||||
<PasswordField
|
||||
required
|
||||
{...form.register("password")}
|
||||
label={t("password")}
|
||||
autoComplete="password"
|
||||
/>
|
||||
<div>
|
||||
<Label>{t("exchange_authentication")}</Label>
|
||||
<Controller
|
||||
name="authenticationMethod"
|
||||
control={form.control}
|
||||
render={({ field: { onChange } }) => (
|
||||
<Select
|
||||
options={authenticationMethods}
|
||||
defaultValue={authenticationMethods[0]}
|
||||
className="mt-1"
|
||||
onChange={async (authentication) => {
|
||||
onChange(authentication?.value);
|
||||
form.setValue("authenticationMethod", authentication!.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Switch
|
||||
label={t("exchange_compression")}
|
||||
name="useCompression"
|
||||
onCheckedChange={async (alt) => {
|
||||
form.setValue("useCompression", alt);
|
||||
}}
|
||||
/>
|
||||
</fieldset>
|
||||
{errorMessage && <Alert severity="error" title={errorMessage} className="my-4" />}
|
||||
<div className="mt-4 flex justify-end space-x-2">
|
||||
<Button type="button" color="secondary" onClick={() => router.back()}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||
{t("save")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
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 {
|
||||
Alert,
|
||||
Button,
|
||||
EmailField,
|
||||
Form,
|
||||
PasswordField,
|
||||
TextField,
|
||||
SelectField,
|
||||
Switch,
|
||||
} from "@calcom/ui/v2";
|
||||
|
||||
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<IFormData>({
|
||||
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 (
|
||||
<>
|
||||
<div className="flex h-screen bg-gray-200">
|
||||
<div className="m-auto rounded bg-white p-5 md:w-[560px] md:p-10">
|
||||
<div className="flex flex-col space-y-5 md:flex-row md:space-y-0 md:space-x-5">
|
||||
<div>
|
||||
{/* eslint-disable @next/next/no-img-element */}
|
||||
<img
|
||||
src="/api/app-store/exchangecalendar/icon.svg"
|
||||
alt="Microsoft Exchange"
|
||||
className="h-12 w-12 max-w-2xl"
|
||||
/>
|
||||
</div>
|
||||
<div className="grow">
|
||||
<h1 className="text-gray-600">{t("exchange_add")}</h1>
|
||||
<div className="text-sm">{t("credentials_stored_encrypted")}</div>
|
||||
<div className="my-2 mt-5">
|
||||
<Form
|
||||
form={form}
|
||||
handleSubmit={async (values) => {
|
||||
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);
|
||||
}
|
||||
}}>
|
||||
<fieldset className="space-y-4" disabled={form.formState.isSubmitting}>
|
||||
<TextField
|
||||
required
|
||||
type="url"
|
||||
{...form.register("url")}
|
||||
label={t("url")}
|
||||
placeholder="https://example.com/Ews/Exchange.asmx"
|
||||
inputMode="url"
|
||||
/>
|
||||
<EmailField
|
||||
required
|
||||
{...form.register("username")}
|
||||
label={t("email_address")}
|
||||
placeholder="john.doe@example.com"
|
||||
/>
|
||||
<PasswordField
|
||||
required
|
||||
{...form.register("password")}
|
||||
label={t("password")}
|
||||
autoComplete="password"
|
||||
/>
|
||||
<Controller
|
||||
name="authenticationMethod"
|
||||
control={form.control}
|
||||
render={({ field: { onChange } }) => (
|
||||
<SelectField
|
||||
label={t("exchange_authentication")}
|
||||
options={authenticationMethods}
|
||||
defaultValue={authenticationMethods[0]}
|
||||
onChange={async (authentication) => {
|
||||
onChange(authentication?.value);
|
||||
form.setValue("authenticationMethod", authentication!.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Switch
|
||||
label={t("exchange_compression")}
|
||||
name="useCompression"
|
||||
onCheckedChange={async (alt) => {
|
||||
form.setValue("useCompression", alt);
|
||||
}}
|
||||
/>
|
||||
</fieldset>
|
||||
{errorMessage && <Alert severity="error" title={errorMessage} className="my-4" />}
|
||||
<div className="mt-4 flex justify-end space-x-2">
|
||||
<Button type="button" color="secondary" onClick={() => router.back()}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||
{t("save")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="4 8 55 48"><title>Exchange_64x</title><path d="M55.50977,8h-12.207A3.48835,3.48835,0,0,0,40.835,9.02246L12.02246,37.835A3.48835,3.48835,0,0,0,11,40.30273v12.207A3.49006,3.49006,0,0,0,14.49023,56h12.207A3.48835,3.48835,0,0,0,29.165,54.97754L57.978,26.165A3.48994,3.48994,0,0,0,59,23.69727v-12.207A3.49007,3.49007,0,0,0,55.50977,8Z" fill="#28a8ea"/><path d="M55.51,56H43.30275a3.49,3.49,0,0,1-2.4678-1.0222L35,49.14286V38.24A6.24,6.24,0,0,1,41.24,32H52.14286L57.9778,37.835A3.49,3.49,0,0,1,59,40.30275V52.51A3.49,3.49,0,0,1,55.51,56Z" fill="#0078d4"/><path d="M14.49,8H26.69725a3.49,3.49,0,0,1,2.4678,1.0222L35,14.85714V25.76A6.24,6.24,0,0,1,28.76,32H17.85714L12.0222,26.16505A3.49,3.49,0,0,1,11,23.69725V11.49A3.49,3.49,0,0,1,14.49,8Z" fill="#50d9ff"/><path d="M33,20.33008V46.66992a1.73444,1.73444,0,0,1-.04.3999A2.31378,2.31378,0,0,1,30.66992,49H11V18H30.66992A2.326,2.326,0,0,1,33,20.33008Z" opacity="0.2"/><path d="M34,20.33008V44.66992A3.36171,3.36171,0,0,1,30.66992,48H11V17H30.66992A3.34177,3.34177,0,0,1,34,20.33008Z" opacity="0.1"/><path d="M33,20.33008V44.66992A2.326,2.326,0,0,1,30.66992,47H11V18H30.66992A2.326,2.326,0,0,1,33,20.33008Z" opacity="0.2"/><path d="M32,20.33008V44.66992A2.326,2.326,0,0,1,29.66992,47H11V18H29.66992A2.326,2.326,0,0,1,32,20.33008Z" opacity="0.1"/><rect x="4.00022" y="18" width="28" height="28" rx="2.33333" fill="#0078d4"/><path d="M22.58533,26.88121h-6.5472V30.7098h6.14535v2.45375H16.03813V37.1401h6.89609v2.4434h-9.868V24.4165h9.5191Z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -5,6 +5,7 @@ import * as closecomothercalendar from "./closecomothercalendar";
|
||||
import * as dailyvideo from "./dailyvideo";
|
||||
import * as exchange2013calendar from "./exchange2013calendar";
|
||||
import * as exchange2016calendar from "./exchange2016calendar";
|
||||
import * as exchangecalendar from "./exchangecalendar";
|
||||
import * as giphy from "./giphy";
|
||||
import * as googlecalendar from "./googlecalendar";
|
||||
import * as googlevideo from "./googlevideo";
|
||||
@@ -46,6 +47,7 @@ const appStore = {
|
||||
zapier,
|
||||
exchange2013calendar,
|
||||
exchange2016calendar,
|
||||
exchangecalendar,
|
||||
};
|
||||
|
||||
export default appStore;
|
||||
|
||||
@@ -55,9 +55,15 @@
|
||||
"type": "raycast_other"
|
||||
},
|
||||
{
|
||||
"dirName": "n8n",
|
||||
"dirName": "n8n",
|
||||
"categories": ["other"],
|
||||
"slug": "n8n",
|
||||
"type": "n8n_other"
|
||||
},
|
||||
{
|
||||
"dirName": "exchangecalendar",
|
||||
"categories": ["calendar"],
|
||||
"slug": "exchange",
|
||||
"type": "exchange_calendar"
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user