App/exchange v2 (#2493)

* Create 2013 package

* Create 2016 package

* Add ews

* Update package.json

* Translate 2013 app to new structure

* Translate 2013 app to new structure

* Translate 2016 app to new structure

* Add appId

* Move setup to a seperate page

* RHF dependency version mismatch

* Move exchange 2016 setup to new page

* Add translations

* Relying on AppSetupMap not defined static pages

* Console build fixes

* Resolved node version to 16

* Prisma errors can't be handled on client

* Fixes node version mismatches

* Improvements

* Endpoint fixes

* Revert "Endpoint fixes"

This reverts commit c0320e3f3d320091ee9a049e3abaf285169ed555.

* Fixes

Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
This commit is contained in:
Omar López
2022-06-13 16:12:27 -06:00
committed by GitHub
co-authored by Joe Au-Yeung Joe Au-Yeung Peer Richelsen Leo Giovanetti
parent 3c9cf81fb3
commit 1960046994
31 changed files with 975 additions and 14 deletions
+3 -3
View File
@@ -2,8 +2,8 @@ import { InferGetStaticPropsType } from "next";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { AppSetupPage } from "@calcom/app-store/_pages/setup";
import { AppSetupPageMap, getStaticProps } from "@calcom/app-store/_pages/setup/_getStaticProps";
import { AppSetupPage, AppSetupMap } from "@calcom/app-store/_pages/setup";
import { getStaticProps } from "@calcom/app-store/_pages/setup/_getStaticProps";
import prisma from "@calcom/prisma";
import Loader from "@calcom/ui/Loader";
@@ -34,7 +34,7 @@ export default function SetupInformation(props: InferGetStaticPropsType<typeof g
export const getStaticPaths = async () => {
const appStore = await prisma.app.findMany({ select: { slug: true } });
const paths = appStore.filter((a) => a.slug in AppSetupPageMap).map((app) => app.slug);
const paths = appStore.filter((a) => a.slug in AppSetupMap).map((app) => app.slug);
return {
paths: paths.map((slug) => ({ params: { slug } })),
@@ -900,5 +900,7 @@
"yes_remove_app": "Yes, remove app",
"are_you_sure_you_want_to_remove_this_app": "Are you sure you want to remove this app?",
"web_conference": "Web conference",
"requires_confirmation": "Requires confirmation"
"requires_confirmation": "Requires confirmation",
"add_exchange2013": "Connect Exchange 2013 Server",
"add_exchange2016": "Connect Exchange 2016 Server"
}
@@ -2,12 +2,6 @@ import { GetStaticPropsContext } from "next";
export const AppSetupPageMap = {
zapier: import("../../zapier/pages/setup/_getStaticProps"),
"apple-calendar": {
getStaticProps: null,
},
"caldav-calendar": {
getStaticProps: null,
},
};
export const getStaticProps = async (ctx: GetStaticPropsContext) => {
@@ -4,6 +4,8 @@ import { DynamicComponent } from "../../_components/DynamicComponent";
export const AppSetupMap = {
"apple-calendar": dynamic(() => import("../../applecalendar/pages/setup")),
"exchange2013-calendar": dynamic(() => import("../../exchange2013calendar/pages/setup")),
"exchange2016-calendar": dynamic(() => import("../../exchange2016calendar/pages/setup")),
"caldav-calendar": dynamic(() => import("../../caldavcalendar/pages/setup")),
zapier: dynamic(() => import("../../zapier/pages/setup")),
};
@@ -0,0 +1,26 @@
import type { App } from "@calcom/types/App";
import _package from "./package.json";
export const metadata = {
name: "Microsoft Exchange 2013 Calendar",
description: _package.description,
installed: true,
type: "exchange2013_calendar",
title: "Microsoft Exchange 2013 Calendar",
imageSrc: "/api/app-store/exchange2013calendar/icon.svg",
variant: "calendar",
category: "calendar",
label: "Exchange Calendar",
logo: "/api/app-store/exchange2013calendar/icon.svg",
publisher: "Cal.com",
rating: 5,
reviews: 69,
slug: "exchange2013-calendar",
trending: false,
url: "https://cal.com/",
verified: true,
email: "help@cal.com",
} as App;
export default metadata;
@@ -0,0 +1,63 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { symmetricEncrypt } from "@calcom/lib/crypto";
import logger from "@calcom/lib/logger";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { CalendarService } from "../lib";
const bodySchema = z
.object({
username: z.string(),
password: z.string(),
url: z.string().url(),
})
.strict();
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const body = bodySchema.parse(req.body);
// Get user
const user = await prisma.user.findFirst({
rejectOnNotFound: true,
where: {
id: req.session?.user?.id,
},
select: {
id: true,
},
});
const data = {
type: "exchange2013_calendar",
key: symmetricEncrypt(JSON.stringify(body), process.env.CALENDSO_ENCRYPTION_KEY!),
userId: user.id,
appId: "exchange2013_calendar",
};
try {
const dav = new CalendarService({
id: 0,
...data,
});
await dav?.listCalendars();
await prisma.credential.create({
data,
});
} catch (reason) {
logger.error("Could not add this exchange account", reason);
return res.status(500).json({ message: "Could not add this exchange account" });
}
return { url: "/apps/installed" };
}
async function getHandler() {
return { url: "/apps/exchange2013-calendar/setup" };
}
export default defaultHandler({
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
});
@@ -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("exchange2013_calendar");
return (
<>
{props.render({
onClick() {
mutation.mutate("");
},
loading: mutation.isLoading,
})}
</>
);
}
@@ -0,0 +1 @@
export { default as InstallAppButton } from "./InstallAppButton";
@@ -0,0 +1,3 @@
export * as api from "./api";
export * as lib from "./lib";
export { metadata } from "./_metadata";
@@ -0,0 +1,218 @@
import { Credential } from "@prisma/client";
import {
Appointment,
Attendee,
CalendarView,
ConflictResolutionMode,
DateTime,
DeleteMode,
ExchangeService,
ExchangeVersion,
FolderId,
FolderView,
ItemId,
LegacyFreeBusyStatus,
MessageBody,
PropertySet,
SendInvitationsMode,
SendInvitationsOrCancellationsMode,
Uri,
WebCredentials,
WellKnownFolderName,
} from "ews-javascript-api";
import { symmetricDecrypt } from "@calcom/lib/crypto";
// Probably don't need
// import { CALENDAR_INTEGRATIONS_TYPES } from "@calcom/lib/integrations/calendar/constants/generals";
import logger from "@calcom/lib/logger";
import { Calendar, CalendarEvent, EventBusyDate, IntegrationCalendar } from "@calcom/types/Calendar";
export default class ExchangeCalendarService implements Calendar {
private url = "";
private integrationName = "";
private log: typeof logger;
private readonly exchangeVersion: ExchangeVersion;
private credentials: Record<string, string>;
constructor(credential: Credential) {
this.integrationName = "exchange2013_calendar";
this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
const decryptedCredential = JSON.parse(
symmetricDecrypt(credential.key?.toString() || "", process.env.CALENDSO_ENCRYPTION_KEY || "")
);
const username = decryptedCredential.username;
const url = decryptedCredential.url;
const password = decryptedCredential.password;
this.url = url;
this.credentials = {
username,
password,
};
this.exchangeVersion = ExchangeVersion.Exchange2013;
}
async createEvent(event: CalendarEvent) {
try {
const appointment = new Appointment(this.getExchangeService()); // service instance of ExchangeService
appointment.Subject = event.title;
appointment.Start = DateTime.Parse(event.startTime); // moment string
appointment.End = DateTime.Parse(event.endTime); // moment string
appointment.Location = event.location || "Location not defined!";
appointment.Body = new MessageBody(event.description || ""); // you can not use any special character or escape the content
for (let i = 0; i < event.attendees.length; i++) {
appointment.RequiredAttendees.Add(new Attendee(event.attendees[i].email));
}
await appointment.Save(SendInvitationsMode.SendToAllAndSaveCopy);
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) {
try {
const appointment = await Appointment.Bind(
this.getExchangeService(),
new ItemId(uid),
new PropertySet()
);
appointment.Subject = event.title;
appointment.Start = DateTime.Parse(event.startTime); // moment string
appointment.End = DateTime.Parse(event.endTime); // moment string
appointment.Location = event.location || "Location not defined!";
appointment.Body = new MessageBody(event.description || ""); // you can not use any special character or escape the content
for (let i = 0; i < event.attendees.length; i++) {
appointment.RequiredAttendees.Add(new Attendee(event.attendees[i].email));
}
appointment.Update(
ConflictResolutionMode.AlwaysOverwrite,
SendInvitationsOrCancellationsMode.SendToAllAndSaveCopy
);
} catch (reason) {
this.log.error(reason);
throw reason;
}
}
async deleteEvent(uid: string) {
try {
const appointment = await Appointment.Bind(
this.getExchangeService(),
new ItemId(uid),
new PropertySet()
);
// Delete the appointment. Note that the item ID will change when the item is moved to the Deleted Items folder.
appointment.Delete(DeleteMode.MoveToDeletedItems);
} catch (reason) {
this.log.error(reason);
throw reason;
}
}
async getAvailability(dateFrom: string, dateTo: string, selectedCalendars: IntegrationCalendar[]) {
try {
const externalCalendars = await this.listCalendars();
const calendarsToGetAppointmentsFrom = [];
for (let i = 0; i < selectedCalendars.length; i++) {
//Only select vaild calendars! (We get all all active calendars on the instance! even from different users!)
for (let k = 0; k < externalCalendars.length; k++) {
if (selectedCalendars[i].externalId == externalCalendars[k].externalId) {
calendarsToGetAppointmentsFrom.push(selectedCalendars[i]);
}
}
}
const finaleRet = [];
for (let i = 0; i < calendarsToGetAppointmentsFrom.length; i++) {
const calendarFolderId = new FolderId(calendarsToGetAppointmentsFrom[i].externalId);
const localReturn = await this.getExchangeService()
.FindAppointments(
calendarFolderId,
new CalendarView(DateTime.Parse(dateFrom), DateTime.Parse(dateTo))
)
.then(function (params) {
const ret: EventBusyDate[] = [];
for (let k = 0; k < params.Items.length; k++) {
if (params.Items[k].LegacyFreeBusyStatus != LegacyFreeBusyStatus.Free) {
//Dont use this appointment if "ShowAs" was set to "free"
ret.push({
start: new Date(params.Items[k].Start.ToISOString()),
end: new Date(params.Items[k].End.ToISOString()),
});
}
}
return ret;
});
finaleRet.push(...localReturn);
}
return finaleRet;
} catch (reason) {
this.log.error(reason);
throw reason;
}
}
async listCalendars() {
try {
const allFolders: IntegrationCalendar[] = [];
return this.getExchangeService()
.FindFolders(WellKnownFolderName.MsgFolderRoot, new FolderView(1000))
.then(async (res) => {
for (const k in res.Folders) {
const f = res.Folders[k];
if (f.FolderClass == "IPF.Appointment") {
//Select parent folder for all calendars
allFolders.push({
externalId: f.Id.UniqueId,
name: f.DisplayName ?? "",
primary: true, //The first one is prime
integration: this.integrationName,
});
return await this.getExchangeService()
.FindFolders(f.Id, new FolderView(1000))
.then((res) => {
//Find all calendars inside calendar folder
res.Folders.forEach((fs) => {
allFolders.push({
externalId: fs.Id.UniqueId,
name: fs.DisplayName ?? "",
primary: false,
integration: this.integrationName,
});
});
return allFolders;
});
}
}
return allFolders;
});
} catch (reason) {
this.log.error(reason);
throw reason;
}
}
private getExchangeService(): ExchangeService {
const exch1 = new ExchangeService(this.exchangeVersion);
exch1.Credentials = new WebCredentials(this.credentials.username, this.credentials.password);
exch1.Url = new Uri(this.url);
return exch1;
}
}
@@ -0,0 +1 @@
export { default as CalendarService } from "./CalendarService";
@@ -0,0 +1,18 @@
{
"private": true,
"name": "@calcom/exchange2013calendar",
"version": "0.0.0",
"main": "./index.ts",
"description": "For calendars hosted on on-premises Microsoft Exchange 2013 servers",
"dependencies": {
"@calcom/core": "*",
"@calcom/lib": "*",
"@calcom/prisma": "*",
"@calcom/ui": "*",
"react-hook-form": "^7.31.1",
"ews-javascript-api": "^0.11.0"
},
"devDependencies": {
"@calcom/types": "*"
}
}
@@ -0,0 +1,97 @@
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { Toaster } from "react-hot-toast";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
import { Form, TextField } from "@calcom/ui/form/fields";
export default function Exchange2013CalendarSetup() {
const { t } = useLocale();
const router = useRouter();
const form = useForm({
defaultValues: {
username: "",
password: "",
url: process.env.EXCHANGE_DEFAULT_EWS_URL || "",
},
});
const [errorMessage, setErrorMessage] = useState("");
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>
<img
src="/api/app-store/exchange2013calendar/icon.svg"
alt="Exchange 2013 Calendar"
className="h-12 w-12 max-w-2xl"
/>
</div>
<div className="flex w-10/12 flex-col">
<h1 className="text-gray-600">{t("add_exchange2013")}</h1>
<div className="mt-1 text-sm">{t("credentials_stored_encrypted")}</div>
<div className="my-2 mt-3">
<Form
form={form}
handleSubmit={async (values) => {
setErrorMessage("");
const res = await fetch("/api/integrations/exchange2013calendar/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-2" disabled={form.formState.isSubmitting}>
<TextField
required
type="text"
{...form.register("url")}
label={t("calendar_url")}
placeholder="https://example.com/Ews/Exchange.asmx"
/>
<TextField
required
type="text"
{...form.register("username")}
label="E-Mail"
placeholder="rickroll@example.com"
/>
<TextField
required
type="password"
{...form.register("password")}
label="Password"
placeholder="•••••••••••••"
autoComplete="password"
/>
</fieldset>
{errorMessage && <Alert severity="error" title={errorMessage} className="my-4" />}
<div className="mt-5 justify-end space-x-2 sm:mt-4 sm:flex">
<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>
<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

@@ -0,0 +1,26 @@
import type { App } from "@calcom/types/App";
import _package from "./package.json";
export const metadata = {
name: "Microsoft Exchange 2016 Calendar",
description: _package.description,
installed: true,
type: "exchange2016_calendar",
title: "Microsoft Exchange 2016 Calendar",
imageSrc: "/api/app-store/exchange2016calendar/icon.svg",
variant: "calendar",
category: "calendar",
label: "Exchange Calendar",
logo: "/api/app-store/exchange2016calendar/icon.svg",
publisher: "Cal.com",
rating: 5,
reviews: 69,
slug: "exchange2016-calendar",
trending: false,
url: "https://cal.com/",
verified: true,
email: "help@cal.com",
} as App;
export default metadata;
@@ -0,0 +1,63 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { symmetricEncrypt } from "@calcom/lib/crypto";
import logger from "@calcom/lib/logger";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { CalendarService } from "../lib";
const bodySchema = z
.object({
username: z.string(),
password: z.string(),
url: z.string().url(),
})
.strict();
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const body = bodySchema.parse(req.body);
// Get user
const user = await prisma.user.findFirst({
rejectOnNotFound: true,
where: {
id: req.session?.user?.id,
},
select: {
id: true,
},
});
const data = {
type: "exchange2016_calendar",
key: symmetricEncrypt(JSON.stringify(body), process.env.CALENDSO_ENCRYPTION_KEY!),
userId: user.id,
appId: "exchange2016_calendar",
};
try {
const dav = new CalendarService({
id: 0,
...data,
});
await dav?.listCalendars();
await prisma.credential.create({
data,
});
} catch (reason) {
logger.error("Could not add this exchange account", reason);
return res.status(500).json({ message: "Could not add this exchange account" });
}
return { url: "/apps/installed" };
}
async function getHandler() {
return { url: "/apps/exchange2016-calendar/setup" };
}
export default defaultHandler({
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
});
@@ -0,0 +1 @@
export { default as add } from "./add";
@@ -0,0 +1,16 @@
import useAddAppMutation from "../../_utils/useAddAppMutation";
import { InstallAppButtonProps } from "../../types";
export default function InstallAppButton(props: InstallAppButtonProps) {
const mutation = useAddAppMutation("exchange2016_calendar");
return (
<>
{props.render({
onClick() {
mutation.mutate("");
},
loading: mutation.isLoading,
})}
</>
);
}
@@ -0,0 +1 @@
export { default as InstallAppButton } from "./InstallAppButton";
@@ -0,0 +1,3 @@
export * as api from "./api";
export * as lib from "./lib";
export { metadata } from "./_metadata";
@@ -0,0 +1,230 @@
import { Credential } from "@prisma/client";
import {
Appointment,
Attendee,
CalendarView,
ConflictResolutionMode,
DateTime,
DeleteMode,
ExchangeService,
ExchangeVersion,
FolderId,
FolderView,
ItemId,
LegacyFreeBusyStatus,
MessageBody,
PropertySet,
SendInvitationsMode,
SendInvitationsOrCancellationsMode,
Uri,
WebCredentials,
WellKnownFolderName,
} from "ews-javascript-api";
import { symmetricDecrypt } from "@calcom/lib/crypto";
// Probably don't need
// import { CALENDAR_INTEGRATIONS_TYPES } from "@calcom/lib/integrations/calendar/constants/generals";
import logger from "@calcom/lib/logger";
import {
Calendar,
CalendarEvent,
EventBusyDate,
IntegrationCalendar,
NewCalendarEventType,
} from "@calcom/types/Calendar";
export default class ExchangeCalendarService implements Calendar {
private url = "";
private integrationName = "";
private log: typeof logger;
private readonly exchangeVersion: ExchangeVersion;
private credentials: Record<string, string>;
constructor(credential: Credential) {
// this.integrationName = CALENDAR_INTEGRATIONS_TYPES.exchange;
this.integrationName = "exchange2016_calendar";
this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
const decryptedCredential = JSON.parse(
symmetricDecrypt(credential.key?.toString() || "", process.env.CALENDSO_ENCRYPTION_KEY || "")
);
const username = decryptedCredential.username;
const url = decryptedCredential.url;
const password = decryptedCredential.password;
this.url = url;
this.credentials = {
username,
password,
};
this.exchangeVersion = ExchangeVersion.Exchange2016;
}
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
try {
const appointment = new Appointment(this.getExchangeService()); // service instance of ExchangeService
appointment.Subject = event.title;
appointment.Start = DateTime.Parse(event.startTime); // moment string
appointment.End = DateTime.Parse(event.endTime); // moment string
appointment.Location = event.location || "Location not defined!";
appointment.Body = new MessageBody(event.description || ""); // you can not use any special character or escape the content
for (let i = 0; i < event.attendees.length; i++) {
appointment.RequiredAttendees.Add(new Attendee(event.attendees[i].email));
}
await appointment.Save(SendInvitationsMode.SendToAllAndSaveCopy);
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<any> {
try {
const appointment = await Appointment.Bind(
this.getExchangeService(),
new ItemId(uid),
new PropertySet()
);
appointment.Subject = event.title;
appointment.Start = DateTime.Parse(event.startTime); // moment string
appointment.End = DateTime.Parse(event.endTime); // moment string
appointment.Location = event.location || "Location not defined!";
appointment.Body = new MessageBody(event.description || ""); // you can not use any special character or escape the content
for (let i = 0; i < event.attendees.length; i++) {
appointment.RequiredAttendees.Add(new Attendee(event.attendees[i].email));
}
appointment.Update(
ConflictResolutionMode.AlwaysOverwrite,
SendInvitationsOrCancellationsMode.SendToAllAndSaveCopy
);
} catch (reason) {
this.log.error(reason);
throw reason;
}
}
async deleteEvent(uid: string): Promise<void> {
try {
const appointment = await Appointment.Bind(
this.getExchangeService(),
new ItemId(uid),
new PropertySet()
);
// Delete the appointment. Note that the item ID will change when the item is moved to the Deleted Items folder.
appointment.Delete(DeleteMode.MoveToDeletedItems);
} catch (reason) {
this.log.error(reason);
throw reason;
}
}
async getAvailability(
dateFrom: string,
dateTo: string,
selectedCalendars: IntegrationCalendar[]
): Promise<EventBusyDate[]> {
try {
const externalCalendars = await this.listCalendars();
const calendarsToGetAppointmentsFrom = [];
for (let i = 0; i < selectedCalendars.length; i++) {
//Only select vaild calendars! (We get all all active calendars on the instance! even from different users!)
for (let k = 0; k < externalCalendars.length; k++) {
if (selectedCalendars[i].externalId == externalCalendars[k].externalId) {
calendarsToGetAppointmentsFrom.push(selectedCalendars[i]);
}
}
}
const finaleRet = [];
for (let i = 0; i < calendarsToGetAppointmentsFrom.length; i++) {
const calendarFolderId = new FolderId(calendarsToGetAppointmentsFrom[i].externalId);
const localReturn = await this.getExchangeService()
.FindAppointments(
calendarFolderId,
new CalendarView(DateTime.Parse(dateFrom), DateTime.Parse(dateTo))
)
.then(function (params) {
const ret: EventBusyDate[] = [];
for (let k = 0; k < params.Items.length; k++) {
if (params.Items[k].LegacyFreeBusyStatus != LegacyFreeBusyStatus.Free) {
//Dont use this appointment if "ShowAs" was set to "free"
ret.push({
start: new Date(params.Items[k].Start.ToISOString()),
end: new Date(params.Items[k].End.ToISOString()),
});
}
}
return ret;
});
finaleRet.push(...localReturn);
}
return finaleRet;
} catch (reason) {
this.log.error(reason);
throw reason;
}
}
async listCalendars(): Promise<IntegrationCalendar[]> {
console.log("This triggers");
try {
const allFolders: IntegrationCalendar[] = [];
return this.getExchangeService()
.FindFolders(WellKnownFolderName.MsgFolderRoot, new FolderView(1000))
.then(async (res) => {
for (const k in res.Folders) {
const f = res.Folders[k];
if (f.FolderClass == "IPF.Appointment") {
//Select parent folder for all calendars
allFolders.push({
externalId: f.Id.UniqueId,
name: f.DisplayName ?? "",
primary: true, //The first one is prime
integration: this.integrationName,
});
return await this.getExchangeService()
.FindFolders(f.Id, new FolderView(1000))
.then((res) => {
//Find all calendars inside calendar folder
res.Folders.forEach((fs) => {
allFolders.push({
externalId: fs.Id.UniqueId,
name: fs.DisplayName ?? "",
primary: false,
integration: this.integrationName,
});
});
return allFolders;
});
}
}
return allFolders;
});
} catch (reason) {
this.log.error(reason);
throw reason;
}
}
private getExchangeService(): ExchangeService {
const exch1 = new ExchangeService(this.exchangeVersion);
exch1.Credentials = new WebCredentials(this.credentials.username, this.credentials.password);
exch1.Url = new Uri(this.url);
return exch1;
}
}
@@ -0,0 +1 @@
export { default as CalendarService } from "./CalendarService";
@@ -0,0 +1,18 @@
{
"private": true,
"name": "@calcom/exchange2016calendar",
"version": "0.0.0",
"main": "./index.ts",
"description": "For calendars hosted on on-premises Microsoft Exchange 2016 servers",
"dependencies": {
"@calcom/core": "*",
"@calcom/lib": "*",
"@calcom/prisma": "*",
"@calcom/ui": "*",
"react-hook-form": "^7.31.1",
"ews-javascript-api": "^0.11.0"
},
"devDependencies": {
"@calcom/types": "*"
}
}
@@ -0,0 +1,97 @@
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { Toaster } from "react-hot-toast";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
import { Form, TextField } from "@calcom/ui/form/fields";
export default function Exchange2016CalendarSetup() {
const { t } = useLocale();
const router = useRouter();
const form = useForm({
defaultValues: {
username: "",
password: "",
url: process.env.EXCHANGE_DEFAULT_EWS_URL || "",
},
});
const [errorMessage, setErrorMessage] = useState("");
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>
<img
src="/api/app-store/exchange2016calendar/icon.svg"
alt="Exchange 2016 Calendar"
className="h-12 w-12 max-w-2xl"
/>
</div>
<div className="flex w-10/12 flex-col">
<h1 className="text-gray-600">{t("add_exchange2016")}</h1>
<div className="mt-1 text-sm">{t("credentials_stored_encrypted")}</div>
<div className="my-2 mt-3">
<Form
form={form}
handleSubmit={async (values) => {
setErrorMessage("");
const res = await fetch("/api/integrations/exchange2016calendar/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-2" disabled={form.formState.isSubmitting}>
<TextField
required
type="text"
{...form.register("url")}
label={t("calendar_url")}
placeholder="https://example.com/Ews/Exchange.asmx"
/>
<TextField
required
type="text"
{...form.register("username")}
label="E-Mail"
placeholder="rickroll@example.com"
/>
<TextField
required
type="password"
{...form.register("password")}
label="Password"
placeholder="•••••••••••••"
autoComplete="password"
/>
</fieldset>
{errorMessage && <Alert severity="error" title={errorMessage} className="my-4" />}
<div className="mt-5 justify-end space-x-2 sm:mt-4 sm:flex">
<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>
<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

+4
View File
@@ -2,6 +2,8 @@
import * as applecalendar from "./applecalendar";
import * as caldavcalendar from "./caldavcalendar";
import * as dailyvideo from "./dailyvideo";
import * as exchange2013calendar from "./exchange2013calendar";
import * as exchange2016calendar from "./exchange2016calendar";
import * as giphy from "./giphy";
import * as googlecalendar from "./googlecalendar";
import * as googlevideo from "./googlevideo";
@@ -42,6 +44,8 @@ const appStore = {
metamask,
giphy,
zapier,
exchange2013calendar,
exchange2016calendar,
};
export default appStore;
+1 -1
View File
@@ -14,7 +14,7 @@ function defaultResponder<T>(f: Handle<T>) {
try {
performance.mark("Start");
const result = await f(req, res);
res.json(result);
if (result) res.json(result);
} catch (err) {
const error = getServerErrorFromUnkown(err);
res.statusCode = error.statusCode;
+1 -1
View File
@@ -1 +1 @@
[]
[]
+2
View File
@@ -32,6 +32,8 @@ async function main() {
// Calendar apps
await createApp("apple-calendar", "applecalendar", ["calendar"], "apple_calendar");
await createApp("caldav-calendar", "caldavcalendar", ["calendar"], "caldav_calendar");
await createApp("exchange2013-calendar", "exchange2013calendar", ["calendar"], "exchange2013_calendar");
await createApp("exchange2016-calendar", "exchange2016calendar", ["calendar"], "exchange2016_calendar");
try {
const { client_secret, client_id, redirect_uris } = JSON.parse(process.env.GOOGLE_API_CREDENTIALS).web;
await createApp("google-calendar", "googlecalendar", ["calendar"], "google_calendar", {
+55 -2
View File
@@ -3972,6 +3972,11 @@
resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.5.tgz#09fa51e356d07d0be200642b0e4f91d8e6dd408d"
integrity sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A==
"@xmldom/xmldom@^0.8.1":
version "0.8.2"
resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.2.tgz#b695ff674e8216efa632a3d36ad51ae9843380c0"
integrity sha512-+R0juSseERyoPvnBQ/cZih6bpF7IpCXlWbHRoCRzYzqpz6gWHOgf8o4MOEf6KBVuOyqU+gCNLkCWVIJAro8XyQ==
abab@^2.0.3, abab@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"
@@ -4688,6 +4693,13 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
biskviit@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/biskviit/-/biskviit-1.0.1.tgz#037a0cd4b71b9e331fd90a1122de17dc49e420a7"
integrity sha512-VGCXdHbdbpEkFgtjkeoBN8vRlbj1ZRX2/mxhE8asCCRalUx2nBzOomLJv8Aw/nRt5+ccDb+tPKidg4XxcfGW4w==
dependencies:
psl "^1.1.7"
bl@^1.0.0:
version "1.2.3"
resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.3.tgz#1e8dd80142eac80d7158c9dccc047fb620e035e7"
@@ -6464,6 +6476,13 @@ encodeurl@~1.0.2:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
encoding@0.1.12:
version "0.1.12"
resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
integrity sha512-bl1LAgiQc4ZWr++pNYUdRe/alecaHFeHxIJ/pNciqGdKXghaTCOwKkbKp6ye7pKZGu/GcaSXFk8PBVhgs+dJdA==
dependencies:
iconv-lite "~0.4.13"
end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
@@ -7231,6 +7250,20 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
md5.js "^1.3.4"
safe-buffer "^5.1.1"
ews-javascript-api@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/ews-javascript-api/-/ews-javascript-api-0.11.0.tgz#06d420040aedab6ba3e6e06448528ee8731c3e5e"
integrity sha512-qSPYhR0cVeBuYsQGPwVUlv80KrcWtqqR5Li7oms1eZ2+EXa7nKsDdeiM/dcUysRxnZOB0g/n7jFoIlldtK4zKw==
dependencies:
"@xmldom/xmldom" "^0.8.1"
base64-js "^1.5.1"
moment "^2.29.1"
moment-timezone "^0.5.34"
uuid "^8.3.2"
optionalDependencies:
bluebird "^3.7.2"
fetch "^1.1.0"
exec-sh@^0.3.2:
version "0.3.6"
resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.6.tgz#ff264f9e325519a60cb5e273692943483cca63bc"
@@ -7586,6 +7619,14 @@ fd-slicer@~1.1.0:
dependencies:
pend "~1.2.0"
fetch@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fetch/-/fetch-1.1.0.tgz#0a8279f06be37f9f0ebb567560a30a480da59a2e"
integrity sha512-5O8TwrGzoNblBG/jtK4NFuZwNCkZX6s5GfRNOaGtm+QGJEuNakSC/i2RW0R93KX6E0jVjNXm6O3CRN4Ql3K+yA==
dependencies:
biskviit "1.0.1"
encoding "0.1.12"
figures@^3.0.0, figures@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
@@ -8731,7 +8772,7 @@ iconv-lite@0.4.19:
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
integrity sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==
iconv-lite@0.4.24, iconv-lite@^0.4.24:
iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.13:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@@ -11947,6 +11988,18 @@ module-alias@^2.2.2:
resolved "https://registry.yarnpkg.com/module-alias/-/module-alias-2.2.2.tgz#151cdcecc24e25739ff0aa6e51e1c5716974c0e0"
integrity sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==
moment-timezone@^0.5.34:
version "0.5.34"
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.34.tgz#a75938f7476b88f155d3504a9343f7519d9a405c"
integrity sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==
dependencies:
moment ">= 2.9.0"
"moment@>= 2.9.0", moment@^2.29.1:
version "2.29.3"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.3.tgz#edd47411c322413999f7a5940d526de183c031f3"
integrity sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==
mongodb-connection-string-url@^2.3.2:
version "2.5.2"
resolved "https://registry.yarnpkg.com/mongodb-connection-string-url/-/mongodb-connection-string-url-2.5.2.tgz#f075c8d529e8d3916386018b8a396aed4f16e5ed"
@@ -13516,7 +13569,7 @@ pseudomap@^1.0.1, pseudomap@^1.0.2:
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
psl@^1.1.28, psl@^1.1.33:
psl@^1.1.28, psl@^1.1.33, psl@^1.1.7:
version "1.8.0"
resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==