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:
co-authored by
Joe Au-Yeung
Joe Au-Yeung
Peer Richelsen
Leo Giovanetti
parent
3c9cf81fb3
commit
1960046994
@@ -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 |
@@ -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;
|
||||
|
||||
@@ -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 @@
|
||||
[]
|
||||
[]
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user