Monorepo/app store MS Teams Integration (#2080)
* Create teamsvideo package * Remove zoom specific refrences * Add teams video files * Rename to office365_video * Add call back to add crednetial type office365_teams * Rename to office_video to match type * Add MS Teams as a location option * Rename files * Add teams reponse interface and create meeting * Comment out Daily imports * Add check for Teams integration * Add token checking functions * Change template to create event rather than meeting * Add comment to test between create link and event * Add teams URL to booking * Ask for just onlineMeeting permission * Add MS Teams logo * Add message to have an enterprise account * Remove comments * Comment back hasDailyIntegration * Comment back daily credentials * Update link to MS Graph section of README * Move API calls to package Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
parent
26e5904d00
commit
26db39f98b
|
@ -141,6 +141,7 @@ const BookingPage = (props: BookingPageProps) => {
|
||||||
[LocationType.Daily]: "Daily.co Video",
|
[LocationType.Daily]: "Daily.co Video",
|
||||||
[LocationType.Huddle01]: "Huddle01 Video",
|
[LocationType.Huddle01]: "Huddle01 Video",
|
||||||
[LocationType.Tandem]: "Tandem Video",
|
[LocationType.Tandem]: "Tandem Video",
|
||||||
|
[LocationType.Teams]: "MS Teams",
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValues = () => {
|
const defaultValues = () => {
|
||||||
|
|
|
@ -54,13 +54,17 @@ export const isTandem = (location: string): boolean => {
|
||||||
return location === "integrations:tandem";
|
return location === "integrations:tandem";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isTeams = (location: string): boolean => {
|
||||||
|
return location === "integrations:office365_video";
|
||||||
|
};
|
||||||
|
|
||||||
export const isJitsi = (location: string): boolean => {
|
export const isJitsi = (location: string): boolean => {
|
||||||
return location === "integrations:jitsi";
|
return location === "integrations:jitsi";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isDedicatedIntegration = (location: string): boolean => {
|
export const isDedicatedIntegration = (location: string): boolean => {
|
||||||
return (
|
return (
|
||||||
isZoom(location) || isDaily(location) || isHuddle01(location) || isTandem(location) || isJitsi(location)
|
isZoom(location) || isDaily(location) || isHuddle01(location) || isTandem(location) || isJitsi(location) || isTeams(location)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -71,7 +75,8 @@ export const getLocationRequestFromIntegration = (location: string) => {
|
||||||
location === LocationType.Daily.valueOf() ||
|
location === LocationType.Daily.valueOf() ||
|
||||||
location === LocationType.Jitsi.valueOf() ||
|
location === LocationType.Jitsi.valueOf() ||
|
||||||
location === LocationType.Huddle01.valueOf() ||
|
location === LocationType.Huddle01.valueOf() ||
|
||||||
location === LocationType.Tandem.valueOf()
|
location === LocationType.Tandem.valueOf() ||
|
||||||
|
location === LocationType.Teams.valueOf()
|
||||||
) {
|
) {
|
||||||
const requestId = uuidv5(location, uuidv5.URL);
|
const requestId = uuidv5(location, uuidv5.URL);
|
||||||
|
|
||||||
|
|
|
@ -287,6 +287,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
return <p className="text-sm">{t("cal_provide_huddle01_meeting_url")}</p>;
|
return <p className="text-sm">{t("cal_provide_huddle01_meeting_url")}</p>;
|
||||||
case LocationType.Tandem:
|
case LocationType.Tandem:
|
||||||
return <p className="text-sm">{t("cal_provide_tandem_meeting_url")}</p>;
|
return <p className="text-sm">{t("cal_provide_tandem_meeting_url")}</p>;
|
||||||
|
case LocationType.Teams:
|
||||||
|
return <p className="text-sm">{t("cal_provide_teams_meeting_url")}</p>;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -410,7 +412,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
{formMethods.getValues("locations").map((location) => (
|
{formMethods.getValues("locations").map((location) => (
|
||||||
<li
|
<li
|
||||||
key={location.type}
|
key={location.type}
|
||||||
className="mb-2 rounded-sm border border-neutral-300 py-1.5 px-2 shadow-sm">
|
className="border-neutral-300 mb-2 rounded-sm border py-1.5 px-2 shadow-sm">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
{location.type === LocationType.InPerson && (
|
{location.type === LocationType.InPerson && (
|
||||||
<div className="flex flex-grow items-center">
|
<div className="flex flex-grow items-center">
|
||||||
|
@ -610,6 +612,78 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<span className="ml-2 text-sm">Jitsi Meet</span>
|
<span className="ml-2 text-sm">Jitsi Meet</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{location.type === LocationType.Teams && (
|
||||||
|
<div className="flex flex-grow items-center">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="h-6 w-6"
|
||||||
|
viewBox="0 0 2228.833 2073.333">
|
||||||
|
<path
|
||||||
|
fill="#5059C9"
|
||||||
|
d="M1554.637,777.5h575.713c54.391,0,98.483,44.092,98.483,98.483c0,0,0,0,0,0v524.398 c0,199.901-162.051,361.952-361.952,361.952h0h-1.711c-199.901,0.028-361.975-162-362.004-361.901c0-0.017,0-0.034,0-0.052V828.971 C1503.167,800.544,1526.211,777.5,1554.637,777.5L1554.637,777.5z"
|
||||||
|
/>
|
||||||
|
<circle fill="#5059C9" cx="1943.75" cy="440.583" r="233.25" />
|
||||||
|
<circle fill="#7B83EB" cx="1218.083" cy="336.917" r="336.917" />
|
||||||
|
<path
|
||||||
|
fill="#7B83EB"
|
||||||
|
d="M1667.323,777.5H717.01c-53.743,1.33-96.257,45.931-95.01,99.676v598.105 c-7.505,322.519,247.657,590.16,570.167,598.053c322.51-7.893,577.671-275.534,570.167-598.053V877.176 C1763.579,823.431,1721.066,778.83,1667.323,777.5z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
opacity=".1"
|
||||||
|
d="M1244,777.5v838.145c-0.258,38.435-23.549,72.964-59.09,87.598 c-11.316,4.787-23.478,7.254-35.765,7.257H667.613c-6.738-17.105-12.958-34.21-18.142-51.833 c-18.144-59.477-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1244z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
opacity=".2"
|
||||||
|
d="M1192.167,777.5v889.978c-0.002,12.287-2.47,24.449-7.257,35.765 c-14.634,35.541-49.163,58.833-87.598,59.09H691.975c-8.812-17.105-17.105-34.21-24.362-51.833 c-7.257-17.623-12.958-34.21-18.142-51.833c-18.144-59.476-27.402-121.307-27.472-183.49V877.02 c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
opacity=".2"
|
||||||
|
d="M1192.167,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855h-447.84 c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
opacity=".2"
|
||||||
|
d="M1140.333,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855H649.472 c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1140.333z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
opacity=".1"
|
||||||
|
d="M1244,509.522v163.275c-8.812,0.518-17.105,1.037-25.917,1.037 c-8.812,0-17.105-0.518-25.917-1.037c-17.496-1.161-34.848-3.937-51.833-8.293c-104.963-24.857-191.679-98.469-233.25-198.003 c-7.153-16.715-12.706-34.071-16.587-51.833h258.648C1201.449,414.866,1243.801,457.217,1244,509.522z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
opacity=".2"
|
||||||
|
d="M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293 c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
opacity=".2"
|
||||||
|
d="M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293 c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
opacity=".2"
|
||||||
|
d="M1140.333,561.355v103.148c-104.963-24.857-191.679-98.469-233.25-198.003 h138.395C1097.783,466.699,1140.134,509.051,1140.333,561.355z"
|
||||||
|
/>
|
||||||
|
<linearGradient
|
||||||
|
id="a"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="198.099"
|
||||||
|
y1="1683.0726"
|
||||||
|
x2="942.2344"
|
||||||
|
y2="394.2607"
|
||||||
|
gradientTransform="matrix(1 0 0 -1 0 2075.3333)">
|
||||||
|
<stop offset="0" stopColor="#5a62c3" />
|
||||||
|
<stop offset=".5" stopColor="#4d55bd" />
|
||||||
|
<stop offset="1" stopColor="#3940ab" />
|
||||||
|
</linearGradient>
|
||||||
|
<path
|
||||||
|
fill="url(#a)"
|
||||||
|
d="M95.01,466.5h950.312c52.473,0,95.01,42.538,95.01,95.01v950.312c0,52.473-42.538,95.01-95.01,95.01 H95.01c-52.473,0-95.01-42.538-95.01-95.01V561.51C0,509.038,42.538,466.5,95.01,466.5z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#FFF"
|
||||||
|
d="M820.211,828.193H630.241v517.297H509.211V828.193H320.123V727.844h500.088V828.193z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="ml-2 text-sm">MS Teams</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -631,8 +705,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
type="button"
|
type="button"
|
||||||
className="flex rounded-sm py-2 hover:bg-gray-100"
|
className="flex rounded-sm py-2 hover:bg-gray-100"
|
||||||
onClick={() => setShowLocationModal(true)}>
|
onClick={() => setShowLocationModal(true)}>
|
||||||
<PlusIcon className="mt-0.5 h-4 w-4 text-neutral-900" />
|
<PlusIcon className="text-neutral-900 mt-0.5 h-4 w-4" />
|
||||||
<span className="ml-1 text-sm font-medium text-neutral-700">{t("add_location")}</span>
|
<span className="text-neutral-700 ml-1 text-sm font-medium">{t("add_location")}</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
|
@ -668,7 +742,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
autoFocus
|
autoFocus
|
||||||
style={{ top: -6, fontSize: 22 }}
|
style={{ top: -6, fontSize: 22 }}
|
||||||
required
|
required
|
||||||
className="relative h-10 w-full cursor-pointer border-none bg-transparent pl-0 text-gray-900 hover:text-gray-700 focus:text-black focus:outline-none focus:ring-0"
|
className="focus:outline-none relative h-10 w-full cursor-pointer border-none bg-transparent pl-0 text-gray-900 hover:text-gray-700 focus:text-black focus:ring-0"
|
||||||
placeholder={t("quick_chat")}
|
placeholder={t("quick_chat")}
|
||||||
{...formMethods.register("title")}
|
{...formMethods.register("title")}
|
||||||
defaultValue={eventType.title}
|
defaultValue={eventType.title}
|
||||||
|
@ -681,7 +755,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<ClientSuspense fallback={<Loader />}>
|
<ClientSuspense fallback={<Loader />}>
|
||||||
<div className="mx-auto block sm:flex md:max-w-5xl">
|
<div className="mx-auto block sm:flex md:max-w-5xl">
|
||||||
<div className="w-full ltr:mr-2 rtl:ml-2 sm:w-9/12">
|
<div className="w-full ltr:mr-2 rtl:ml-2 sm:w-9/12">
|
||||||
<div className="-mx-4 rounded-sm border border-neutral-200 bg-white p-4 py-6 sm:mx-0 sm:px-8">
|
<div className="border-neutral-200 -mx-4 rounded-sm border bg-white p-4 py-6 sm:mx-0 sm:px-8">
|
||||||
<Form
|
<Form
|
||||||
form={formMethods}
|
form={formMethods}
|
||||||
handleSubmit={async (values) => {
|
handleSubmit={async (values) => {
|
||||||
|
@ -713,8 +787,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="block items-center sm:flex">
|
<div className="block items-center sm:flex">
|
||||||
<div className="min-w-48 mb-4 sm:mb-0">
|
<div className="min-w-48 mb-4 sm:mb-0">
|
||||||
<label htmlFor="slug" className="flex text-sm font-medium text-neutral-700">
|
<label htmlFor="slug" className="text-neutral-700 flex text-sm font-medium">
|
||||||
<LinkIcon className="mt-0.5 h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2" />
|
<LinkIcon className="text-neutral-500 mt-0.5 h-4 w-4 ltr:mr-2 rtl:ml-2" />
|
||||||
{t("url")}
|
{t("url")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -744,7 +818,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<MinutesField
|
<MinutesField
|
||||||
label={
|
label={
|
||||||
<>
|
<>
|
||||||
<ClockIcon className="h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2" />{" "}
|
<ClockIcon className="text-neutral-500 h-4 w-4 ltr:mr-2 rtl:ml-2" />{" "}
|
||||||
{t("duration")}
|
{t("duration")}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
@ -766,8 +840,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<div className="min-w-48 sm:mb-0">
|
<div className="min-w-48 sm:mb-0">
|
||||||
<label
|
<label
|
||||||
htmlFor="location"
|
htmlFor="location"
|
||||||
className="mt-2.5 flex text-sm font-medium text-neutral-700">
|
className="text-neutral-700 mt-2.5 flex text-sm font-medium">
|
||||||
<LocationMarkerIcon className="mt-0.5 mb-4 h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2" />
|
<LocationMarkerIcon className="text-neutral-500 mt-0.5 mb-4 h-4 w-4 ltr:mr-2 rtl:ml-2" />
|
||||||
{t("location")}
|
{t("location")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -785,8 +859,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<div className="min-w-48 mb-4 mt-2.5 sm:mb-0">
|
<div className="min-w-48 mb-4 mt-2.5 sm:mb-0">
|
||||||
<label
|
<label
|
||||||
htmlFor="description"
|
htmlFor="description"
|
||||||
className="mt-0 flex text-sm font-medium text-neutral-700">
|
className="text-neutral-700 mt-0 flex text-sm font-medium">
|
||||||
<DocumentIcon className="mt-0.5 h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2" />
|
<DocumentIcon className="text-neutral-500 mt-0.5 h-4 w-4 ltr:mr-2 rtl:ml-2" />
|
||||||
{t("description")}
|
{t("description")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -807,8 +881,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<div className="min-w-48 mb-4 sm:mb-0">
|
<div className="min-w-48 mb-4 sm:mb-0">
|
||||||
<label
|
<label
|
||||||
htmlFor="schedulingType"
|
htmlFor="schedulingType"
|
||||||
className="mt-2 flex text-sm font-medium text-neutral-700">
|
className="text-neutral-700 mt-2 flex text-sm font-medium">
|
||||||
<UsersIcon className="h-5 w-5 text-neutral-500 ltr:mr-2 rtl:ml-2" />{" "}
|
<UsersIcon className="text-neutral-500 h-5 w-5 ltr:mr-2 rtl:ml-2" />{" "}
|
||||||
{t("scheduling_type")}
|
{t("scheduling_type")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -831,8 +905,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
|
|
||||||
<div className="block sm:flex">
|
<div className="block sm:flex">
|
||||||
<div className="min-w-48 mb-4 sm:mb-0">
|
<div className="min-w-48 mb-4 sm:mb-0">
|
||||||
<label htmlFor="users" className="flex text-sm font-medium text-neutral-700">
|
<label htmlFor="users" className="text-neutral-700 flex text-sm font-medium">
|
||||||
<UserAddIcon className="h-5 w-5 text-neutral-500 ltr:mr-2 rtl:ml-2" />{" "}
|
<UserAddIcon className="text-neutral-500 h-5 w-5 ltr:mr-2 rtl:ml-2" />{" "}
|
||||||
{t("attendees")}
|
{t("attendees")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -868,9 +942,9 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<ChevronRightIcon
|
<ChevronRightIcon
|
||||||
className={`${
|
className={`${
|
||||||
advancedSettingsVisible ? "rotate-90 transform" : ""
|
advancedSettingsVisible ? "rotate-90 transform" : ""
|
||||||
} ml-auto h-5 w-5 text-neutral-500`}
|
} text-neutral-500 ml-auto h-5 w-5`}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm font-medium text-neutral-700">
|
<span className="text-neutral-700 text-sm font-medium">
|
||||||
{t("show_advanced_settings")}
|
{t("show_advanced_settings")}
|
||||||
</span>
|
</span>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
|
@ -885,7 +959,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<div className="min-w-48 mb-4 sm:mb-0">
|
<div className="min-w-48 mb-4 sm:mb-0">
|
||||||
<label
|
<label
|
||||||
htmlFor="createEventsOn"
|
htmlFor="createEventsOn"
|
||||||
className="flex text-sm font-medium text-neutral-700">
|
className="text-neutral-700 flex text-sm font-medium">
|
||||||
{t("create_events_on")}
|
{t("create_events_on")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -909,7 +983,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
)}
|
)}
|
||||||
<div className="block items-center sm:flex">
|
<div className="block items-center sm:flex">
|
||||||
<div className="min-w-48 mb-4 sm:mb-0">
|
<div className="min-w-48 mb-4 sm:mb-0">
|
||||||
<label htmlFor="eventName" className="flex text-sm font-medium text-neutral-700">
|
<label htmlFor="eventName" className="text-neutral-700 flex text-sm font-medium">
|
||||||
{t("event_name")} <InfoBadge content={t("event_name_tooltip")} />
|
{t("event_name")} <InfoBadge content={t("event_name_tooltip")} />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -930,7 +1004,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<div className="min-w-48 mb-4 sm:mb-0">
|
<div className="min-w-48 mb-4 sm:mb-0">
|
||||||
<label
|
<label
|
||||||
htmlFor="smartContractAddress"
|
htmlFor="smartContractAddress"
|
||||||
className="flex text-sm font-medium text-neutral-700">
|
className="text-neutral-700 flex text-sm font-medium">
|
||||||
{t("Smart Contract Address")}
|
{t("Smart Contract Address")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -953,7 +1027,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<div className="min-w-48 mb-4 sm:mb-0">
|
<div className="min-w-48 mb-4 sm:mb-0">
|
||||||
<label
|
<label
|
||||||
htmlFor="additionalFields"
|
htmlFor="additionalFields"
|
||||||
className="flexflex mt-2 text-sm font-medium text-neutral-700">
|
className="flexflex text-neutral-700 mt-2 text-sm font-medium">
|
||||||
{t("additional_inputs")}
|
{t("additional_inputs")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1059,7 +1133,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<hr className="my-2 border-neutral-200" />
|
<hr className="border-neutral-200 my-2" />
|
||||||
<Controller
|
<Controller
|
||||||
name="minimumBookingNotice"
|
name="minimumBookingNotice"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
|
@ -1080,7 +1154,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
|
|
||||||
<div className="block items-center sm:flex">
|
<div className="block items-center sm:flex">
|
||||||
<div className="min-w-48 mb-4 sm:mb-0">
|
<div className="min-w-48 mb-4 sm:mb-0">
|
||||||
<label htmlFor="eventName" className="flex text-sm font-medium text-neutral-700">
|
<label htmlFor="eventName" className="text-neutral-700 flex text-sm font-medium">
|
||||||
{t("slot_interval")}
|
{t("slot_interval")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1129,7 +1203,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<div className="min-w-48 mb-4 sm:mb-0">
|
<div className="min-w-48 mb-4 sm:mb-0">
|
||||||
<label
|
<label
|
||||||
htmlFor="inviteesCanSchedule"
|
htmlFor="inviteesCanSchedule"
|
||||||
className="mt-2.5 flex text-sm font-medium text-neutral-700">
|
className="text-neutral-700 mt-2.5 flex text-sm font-medium">
|
||||||
{t("invitees_can_schedule")}
|
{t("invitees_can_schedule")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1149,7 +1223,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<RadioGroup.Item
|
<RadioGroup.Item
|
||||||
id={period.type}
|
id={period.type}
|
||||||
value={period.type}
|
value={period.type}
|
||||||
className="flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
|
className="focus:outline-none flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 ltr:mr-2 rtl:ml-2">
|
||||||
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
|
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
|
||||||
</RadioGroup.Item>
|
</RadioGroup.Item>
|
||||||
{period.prefix ? <span>{period.prefix} </span> : null}
|
{period.prefix ? <span>{period.prefix} </span> : null}
|
||||||
|
@ -1164,7 +1238,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
/>
|
/>
|
||||||
<select
|
<select
|
||||||
id=""
|
id=""
|
||||||
className="focus:border-primary-500 focus:ring-primary-500 block w-full rounded-sm border-gray-300 py-2 pl-3 pr-10 text-base focus:outline-none sm:text-sm"
|
className="focus:border-primary-500 focus:ring-primary-500 focus:outline-none block w-full rounded-sm border-gray-300 py-2 pl-3 pr-10 text-base sm:text-sm"
|
||||||
{...formMethods.register("periodCountCalendarDays")}
|
{...formMethods.register("periodCountCalendarDays")}
|
||||||
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
|
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
|
||||||
<option value="1">{t("calendar_days")}</option>
|
<option value="1">{t("calendar_days")}</option>
|
||||||
|
@ -1205,7 +1279,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<div className="min-w-48 mb-4 sm:mb-0">
|
<div className="min-w-48 mb-4 sm:mb-0">
|
||||||
<label
|
<label
|
||||||
htmlFor="bufferTime"
|
htmlFor="bufferTime"
|
||||||
className="mt-2.5 flex text-sm font-medium text-neutral-700">
|
className="text-neutral-700 mt-2.5 flex text-sm font-medium">
|
||||||
{t("buffer_time")}
|
{t("buffer_time")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1214,7 +1288,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<label
|
<label
|
||||||
htmlFor="beforeBufferTime"
|
htmlFor="beforeBufferTime"
|
||||||
className="mb-2 flex text-sm font-medium text-neutral-700">
|
className="text-neutral-700 mb-2 flex text-sm font-medium">
|
||||||
{t("before_event")}
|
{t("before_event")}
|
||||||
</label>
|
</label>
|
||||||
<Controller
|
<Controller
|
||||||
|
@ -1253,7 +1327,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<label
|
<label
|
||||||
htmlFor="afterBufferTime"
|
htmlFor="afterBufferTime"
|
||||||
className="mb-2 flex text-sm font-medium text-neutral-700">
|
className="text-neutral-700 mb-2 flex text-sm font-medium">
|
||||||
{t("after_event")}
|
{t("after_event")}
|
||||||
</label>
|
</label>
|
||||||
<Controller
|
<Controller
|
||||||
|
@ -1298,7 +1372,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<div className="min-w-48 mb-4 sm:mb-0">
|
<div className="min-w-48 mb-4 sm:mb-0">
|
||||||
<label
|
<label
|
||||||
htmlFor="availability"
|
htmlFor="availability"
|
||||||
className="flex text-sm font-medium text-neutral-700">
|
className="text-neutral-700 flex text-sm font-medium">
|
||||||
{t("availability")}
|
{t("availability")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1341,7 +1415,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<div className="min-w-48 mb-4 sm:mb-0">
|
<div className="min-w-48 mb-4 sm:mb-0">
|
||||||
<label
|
<label
|
||||||
htmlFor="payment"
|
htmlFor="payment"
|
||||||
className="mt-2 flex text-sm font-medium text-neutral-700">
|
className="text-neutral-700 mt-2 flex text-sm font-medium">
|
||||||
{t("payment")}
|
{t("payment")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1466,9 +1540,9 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
href={permalink}
|
href={permalink}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-md inline-flex items-center rounded-sm px-2 py-1 text-sm font-medium text-neutral-700 hover:bg-gray-200 hover:text-gray-900">
|
className="text-md text-neutral-700 inline-flex items-center rounded-sm px-2 py-1 text-sm font-medium hover:bg-gray-200 hover:text-gray-900">
|
||||||
<ExternalLinkIcon
|
<ExternalLinkIcon
|
||||||
className="h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2"
|
className="text-neutral-500 h-4 w-4 ltr:mr-2 rtl:ml-2"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
{t("preview")}
|
{t("preview")}
|
||||||
|
@ -1480,12 +1554,12 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
className="text-md flex items-center rounded-sm px-2 py-1 text-sm font-medium text-gray-700 hover:bg-gray-200 hover:text-gray-900">
|
className="text-md flex items-center rounded-sm px-2 py-1 text-sm font-medium text-gray-700 hover:bg-gray-200 hover:text-gray-900">
|
||||||
<LinkIcon className="h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2" />
|
<LinkIcon className="text-neutral-500 h-4 w-4 ltr:mr-2 rtl:ml-2" />
|
||||||
{t("copy_link")}
|
{t("copy_link")}
|
||||||
</button>
|
</button>
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger className="text-md flex items-center rounded-sm px-2 py-1 text-sm font-medium text-neutral-700 hover:bg-gray-200 hover:text-gray-900">
|
<DialogTrigger className="text-md text-neutral-700 flex items-center rounded-sm px-2 py-1 text-sm font-medium hover:bg-gray-200 hover:text-gray-900">
|
||||||
<TrashIcon className="h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2" />
|
<TrashIcon className="text-neutral-500 h-4 w-4 ltr:mr-2 rtl:ml-2" />
|
||||||
{t("delete")}
|
{t("delete")}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<ConfirmationDialogContent
|
<ConfirmationDialogContent
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2228.833 2073.333">
|
||||||
|
<path fill="#5059C9" d="M1554.637,777.5h575.713c54.391,0,98.483,44.092,98.483,98.483c0,0,0,0,0,0v524.398 c0,199.901-162.051,361.952-361.952,361.952h0h-1.711c-199.901,0.028-361.975-162-362.004-361.901c0-0.017,0-0.034,0-0.052V828.971 C1503.167,800.544,1526.211,777.5,1554.637,777.5L1554.637,777.5z"/>
|
||||||
|
<circle fill="#5059C9" cx="1943.75" cy="440.583" r="233.25"/>
|
||||||
|
<circle fill="#7B83EB" cx="1218.083" cy="336.917" r="336.917"/>
|
||||||
|
<path fill="#7B83EB" d="M1667.323,777.5H717.01c-53.743,1.33-96.257,45.931-95.01,99.676v598.105 c-7.505,322.519,247.657,590.16,570.167,598.053c322.51-7.893,577.671-275.534,570.167-598.053V877.176 C1763.579,823.431,1721.066,778.83,1667.323,777.5z"/>
|
||||||
|
<path opacity=".1" d="M1244,777.5v838.145c-0.258,38.435-23.549,72.964-59.09,87.598 c-11.316,4.787-23.478,7.254-35.765,7.257H667.613c-6.738-17.105-12.958-34.21-18.142-51.833 c-18.144-59.477-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1244z"/>
|
||||||
|
<path opacity=".2" d="M1192.167,777.5v889.978c-0.002,12.287-2.47,24.449-7.257,35.765 c-14.634,35.541-49.163,58.833-87.598,59.09H691.975c-8.812-17.105-17.105-34.21-24.362-51.833 c-7.257-17.623-12.958-34.21-18.142-51.833c-18.144-59.476-27.402-121.307-27.472-183.49V877.02 c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z"/>
|
||||||
|
<path opacity=".2" d="M1192.167,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855h-447.84 c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z"/>
|
||||||
|
<path opacity=".2" d="M1140.333,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855H649.472 c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1140.333z"/>
|
||||||
|
<path opacity=".1" d="M1244,509.522v163.275c-8.812,0.518-17.105,1.037-25.917,1.037 c-8.812,0-17.105-0.518-25.917-1.037c-17.496-1.161-34.848-3.937-51.833-8.293c-104.963-24.857-191.679-98.469-233.25-198.003 c-7.153-16.715-12.706-34.071-16.587-51.833h258.648C1201.449,414.866,1243.801,457.217,1244,509.522z"/>
|
||||||
|
<path opacity=".2" d="M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293 c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z"/>
|
||||||
|
<path opacity=".2" d="M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293 c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z"/>
|
||||||
|
<path opacity=".2" d="M1140.333,561.355v103.148c-104.963-24.857-191.679-98.469-233.25-198.003 h138.395C1097.783,466.699,1140.134,509.051,1140.333,561.355z"/>
|
||||||
|
<linearGradient id="a" gradientUnits="userSpaceOnUse" x1="198.099" y1="1683.0726" x2="942.2344" y2="394.2607" gradientTransform="matrix(1 0 0 -1 0 2075.3333)">
|
||||||
|
<stop offset="0" stop-color="#5a62c3"/>
|
||||||
|
<stop offset=".5" stop-color="#4d55bd"/>
|
||||||
|
<stop offset="1" stop-color="#3940ab"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path fill="url(#a)" d="M95.01,466.5h950.312c52.473,0,95.01,42.538,95.01,95.01v950.312c0,52.473-42.538,95.01-95.01,95.01 H95.01c-52.473,0-95.01-42.538-95.01-95.01V561.51C0,509.038,42.538,466.5,95.01,466.5z"/>
|
||||||
|
<path fill="#FFF" d="M820.211,828.193H630.241v517.297H509.211V828.193H320.123V727.844h500.088V828.193z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.2 KiB |
|
@ -581,6 +581,7 @@
|
||||||
"cal_provide_video_meeting_url": "Cal will provide a Daily video meeting URL.",
|
"cal_provide_video_meeting_url": "Cal will provide a Daily video meeting URL.",
|
||||||
"cal_provide_jitsi_meeting_url": "We will generate a Jitsi Meet URL for you.",
|
"cal_provide_jitsi_meeting_url": "We will generate a Jitsi Meet URL for you.",
|
||||||
"cal_provide_huddle01_meeting_url": "Cal will provide a Huddle01 web3 video meeting URL.",
|
"cal_provide_huddle01_meeting_url": "Cal will provide a Huddle01 web3 video meeting URL.",
|
||||||
|
"cal_provide_teams_meeting_url": "Cal will provide a MS Teams meeting URL. NOTE: MUST HAVE A WORK OR SCHOOL ACCOUNT",
|
||||||
"require_payment": "Require Payment",
|
"require_payment": "Require Payment",
|
||||||
"commission_per_transaction": "commission per transaction",
|
"commission_per_transaction": "commission per transaction",
|
||||||
"event_type_updated_successfully_description": "Your event type has been updated successfully.",
|
"event_type_updated_successfully_description": "Your event type has been updated successfully.",
|
||||||
|
|
|
@ -2,6 +2,7 @@ import * as example from "./_example";
|
||||||
import * as dailyvideo from "./dailyvideo";
|
import * as dailyvideo from "./dailyvideo";
|
||||||
import * as huddle01video from "./huddle01video";
|
import * as huddle01video from "./huddle01video";
|
||||||
import * as jitsivideo from "./jitsivideo";
|
import * as jitsivideo from "./jitsivideo";
|
||||||
|
import * as office365video from "./office365video";
|
||||||
import * as zoomvideo from "./zoomvideo";
|
import * as zoomvideo from "./zoomvideo";
|
||||||
|
|
||||||
const appStore = {
|
const appStore = {
|
||||||
|
@ -10,6 +11,7 @@ const appStore = {
|
||||||
huddle01video,
|
huddle01video,
|
||||||
jitsivideo,
|
jitsivideo,
|
||||||
zoomvideo,
|
zoomvideo,
|
||||||
|
office365video,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default appStore;
|
export default appStore;
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Place these on `apps/web/.env`
|
||||||
|
# @see https://github.com/calcom/cal.com/#Obtaining-Microsoft-Graph-Client-ID-and-Secret
|
||||||
|
MS_GRAPH_CLIENT_CLIENT_ID=
|
||||||
|
MS_GRAPH_CLIENT_CLIENT_SECRET=
|
|
@ -0,0 +1,35 @@
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { stringify } from "querystring";
|
||||||
|
|
||||||
|
import { getSession } from "@calcom/lib/auth";
|
||||||
|
import { BASE_URL } from "@calcom/lib/constants";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
import { encodeOAuthState } from "../utils";
|
||||||
|
|
||||||
|
const scopes = ["OnlineMeetings.ReadWrite"];
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
if (req.method === "GET") {
|
||||||
|
// Check that user is authenticated
|
||||||
|
const session = await getSession({ req });
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
res.status(401).json({ message: "You must be logged in to do this" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = encodeOAuthState(req);
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
response_type: "code",
|
||||||
|
scope: scopes.join(" "),
|
||||||
|
client_id: process.env.MS_GRAPH_CLIENT_ID,
|
||||||
|
redirect_uri: BASE_URL + "/api/integrations/office365video/callback",
|
||||||
|
state,
|
||||||
|
};
|
||||||
|
const query = stringify(params);
|
||||||
|
const url = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?${query}`;
|
||||||
|
res.status(200).json({ url });
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
import { getSession } from "@calcom/lib/auth";
|
||||||
|
import { BASE_URL } from "@calcom/lib/constants";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
import { decodeOAuthState } from "../utils";
|
||||||
|
|
||||||
|
const scopes = ["OnlineMeetings.ReadWrite"];
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const { code } = req.query;
|
||||||
|
console.log("🚀 ~ file: callback.ts ~ line 14 ~ handler ~ code", req.query);
|
||||||
|
|
||||||
|
// Check that user is authenticated
|
||||||
|
const session = await getSession({ req: req });
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
res.status(401).json({ message: "You must be logged in to do this" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof code !== "string") {
|
||||||
|
res.status(400).json({ message: "No code returned" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toUrlEncoded = (payload: Record<string, string>) =>
|
||||||
|
Object.keys(payload)
|
||||||
|
.map((key) => key + "=" + encodeURIComponent(payload[key]))
|
||||||
|
.join("&");
|
||||||
|
|
||||||
|
const body = toUrlEncoded({
|
||||||
|
client_id: process.env.MS_GRAPH_CLIENT_ID!,
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code,
|
||||||
|
scope: scopes.join(" "),
|
||||||
|
redirect_uri: BASE_URL + "/api/integrations/office365video/callback",
|
||||||
|
client_secret: process.env.MS_GRAPH_CLIENT_SECRET!,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
||||||
|
},
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseBody = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return res.redirect("/integrations?error=" + JSON.stringify(responseBody));
|
||||||
|
}
|
||||||
|
|
||||||
|
const whoami = await fetch("https://graph.microsoft.com/v1.0/me", {
|
||||||
|
headers: { Authorization: "Bearer " + responseBody.access_token },
|
||||||
|
});
|
||||||
|
const graphUser = await whoami.json();
|
||||||
|
|
||||||
|
// In some cases, graphUser.mail is null. Then graphUser.userPrincipalName most likely contains the email address.
|
||||||
|
responseBody.email = graphUser.mail ?? graphUser.userPrincipalName;
|
||||||
|
responseBody.expiry_date = Math.round(+new Date() / 1000 + responseBody.expires_in); // set expiry date in seconds
|
||||||
|
delete responseBody.expires_in;
|
||||||
|
|
||||||
|
await prisma.credential.create({
|
||||||
|
data: {
|
||||||
|
type: "office365_video",
|
||||||
|
key: responseBody,
|
||||||
|
userId: session.user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = decodeOAuthState(req);
|
||||||
|
return res.redirect(state?.returnTo ?? "/integrations");
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as add } from "./add";
|
||||||
|
export { default as callback } from "./callback";
|
|
@ -0,0 +1,26 @@
|
||||||
|
import type { App } from "@calcom/types/App";
|
||||||
|
|
||||||
|
import _package from "./package.json";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
name: "Microsoft 365/Teams",
|
||||||
|
description: _package.description,
|
||||||
|
installed: !!(process.env.MS_GRAPH_CLIENT_ID && process.env.MS_GRAPH_CLIENT_SECRET),
|
||||||
|
type: "office365_video",
|
||||||
|
imageSrc: "apps/msteams.svg",
|
||||||
|
variant: "conferencing",
|
||||||
|
logo: "/apps/msteams.svg",
|
||||||
|
publisher: "Cal.com",
|
||||||
|
url: "https://www.microsoft.com/en-ca/microsoft-teams/group-chat-software",
|
||||||
|
verified: true,
|
||||||
|
rating: 4.3, // TODO: placeholder for now, pull this from TrustPilot or G2
|
||||||
|
reviews: 69, // TODO: placeholder for now, pull this from TrustPilot or G2
|
||||||
|
category: "video",
|
||||||
|
label: "MS Teams",
|
||||||
|
slug: "msteams",
|
||||||
|
title: "MS Teams",
|
||||||
|
trending: true,
|
||||||
|
} as App;
|
||||||
|
|
||||||
|
export * as api from "./api";
|
||||||
|
export * as lib from "./lib";
|
|
@ -0,0 +1,142 @@
|
||||||
|
import { Credential } from "@prisma/client";
|
||||||
|
|
||||||
|
import { handleErrorsJson, handleErrorsRaw } from "@calcom/lib/errors";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
import type { CalendarEvent } from "@calcom/types/CalendarEvent";
|
||||||
|
import type { PartialReference } from "@calcom/types/EventManager";
|
||||||
|
import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter";
|
||||||
|
|
||||||
|
const MS_GRAPH_CLIENT_ID = process.env.MS_GRAPH_CLIENT_ID || "";
|
||||||
|
const MS_GRAPH_CLIENT_SECRET = process.env.MS_GRAPH_CLIENT_SECRET || "";
|
||||||
|
|
||||||
|
/** @link https://docs.microsoft.com/en-us/graph/api/application-post-onlinemeetings?view=graph-rest-1.0&tabs=http#response */
|
||||||
|
export interface TeamsEventResult {
|
||||||
|
creationDateTime: string;
|
||||||
|
startDateTime: string;
|
||||||
|
endDateTime: string;
|
||||||
|
id: string;
|
||||||
|
joinWebUrl: string;
|
||||||
|
subject: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface O365AuthCredentials {
|
||||||
|
email: string;
|
||||||
|
scope: string;
|
||||||
|
token_type: string;
|
||||||
|
expiry_date: number;
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
ext_expires_in: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks to see if our O365 user token is valid or if we need to refresh
|
||||||
|
const o365Auth = (credential: Credential) => {
|
||||||
|
const isExpired = (expiryDate: number) => expiryDate < Math.round(+new Date() / 1000);
|
||||||
|
|
||||||
|
const o365AuthCredentials = credential.key as unknown as O365AuthCredentials;
|
||||||
|
|
||||||
|
const refreshAccessToken = (refreshToken: string) => {
|
||||||
|
return fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
|
body: new URLSearchParams({
|
||||||
|
scope: "User.Read Calendars.Read Calendars.ReadWrite",
|
||||||
|
client_id: MS_GRAPH_CLIENT_ID,
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
client_secret: MS_GRAPH_CLIENT_SECRET,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then(handleErrorsJson)
|
||||||
|
.then(async (responseBody) => {
|
||||||
|
// set expiry date as offset from current time.
|
||||||
|
responseBody.expiry_date = Math.round(Date.now() + responseBody.expires_in * 1000);
|
||||||
|
delete responseBody.expires_in;
|
||||||
|
// Store new tokens in database.
|
||||||
|
await prisma.credential.update({
|
||||||
|
where: {
|
||||||
|
id: credential.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
key: responseBody,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
o365AuthCredentials.expiry_date = responseBody.expiry_date;
|
||||||
|
o365AuthCredentials.access_token = responseBody.access_token;
|
||||||
|
return o365AuthCredentials.access_token;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getToken: () =>
|
||||||
|
!isExpired(o365AuthCredentials.expiry_date)
|
||||||
|
? Promise.resolve(o365AuthCredentials.access_token)
|
||||||
|
: refreshAccessToken(o365AuthCredentials.refresh_token),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const TeamsVideoApiAdapter = (credential: Credential): VideoApiAdapter => {
|
||||||
|
const auth = o365Auth(credential);
|
||||||
|
|
||||||
|
const translateEvent = (event: CalendarEvent) => {
|
||||||
|
return {
|
||||||
|
startDateTime: event.startTime,
|
||||||
|
endDateTime: event.endTime,
|
||||||
|
subject: event.title,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Since the meeting link is not tied to an event we only need the create and update functions
|
||||||
|
return {
|
||||||
|
getAvailability: () => {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
},
|
||||||
|
updateMeeting: async (bookingRef: PartialReference, event: CalendarEvent) => {
|
||||||
|
const accessToken = await auth.getToken();
|
||||||
|
|
||||||
|
const resultString = await fetch("https://graph.microsoft.com/v1.0/me/onlineMeetings", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer " + accessToken,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(translateEvent(event)),
|
||||||
|
}).then(handleErrorsRaw);
|
||||||
|
|
||||||
|
const resultObject = JSON.parse(resultString);
|
||||||
|
|
||||||
|
return Promise.resolve({
|
||||||
|
type: "office365_video",
|
||||||
|
id: resultObject.id,
|
||||||
|
password: "",
|
||||||
|
url: resultObject.joinUrl,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteMeeting: () => {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
},
|
||||||
|
createMeeting: async (event: CalendarEvent): Promise<VideoCallData> => {
|
||||||
|
const accessToken = await auth.getToken();
|
||||||
|
|
||||||
|
const resultString = await fetch("https://graph.microsoft.com/v1.0/me/onlineMeetings", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer " + accessToken,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(translateEvent(event)),
|
||||||
|
}).then(handleErrorsRaw);
|
||||||
|
|
||||||
|
const resultObject = JSON.parse(resultString);
|
||||||
|
|
||||||
|
return Promise.resolve({
|
||||||
|
type: "office365_video",
|
||||||
|
id: resultObject.id,
|
||||||
|
password: "",
|
||||||
|
url: resultObject.joinUrl,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TeamsVideoApiAdapter;
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as locationOption } from "./locationOption";
|
||||||
|
export { default as VideoApiAdapter } from "./VideoApiAdapter";
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { LocationType } from "@calcom/lib/location";
|
||||||
|
|
||||||
|
const locationOption = {
|
||||||
|
value: LocationType.Teams,
|
||||||
|
label: "MS Teams",
|
||||||
|
disabled: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default locationOption;
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"name": "@calcom/office365video",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"main": "./index.ts",
|
||||||
|
"description": "Use your Office 365 account to book video calls through MS Teams NOTE: MUST HAVE A WORK / SCHOOL ACCOUNT",
|
||||||
|
"dependencies": {
|
||||||
|
"@calcom/prisma": "*",
|
||||||
|
"@calcom/lib": "*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@calcom/types": "*"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { NextApiRequest } from "next";
|
||||||
|
|
||||||
|
import { IntegrationOAuthCallbackState } from "./types";
|
||||||
|
|
||||||
|
export function encodeOAuthState(req: NextApiRequest) {
|
||||||
|
if (typeof req.query.state !== "string") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const state: IntegrationOAuthCallbackState = JSON.parse(req.query.state);
|
||||||
|
|
||||||
|
return JSON.stringify(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeOAuthState(req: NextApiRequest) {
|
||||||
|
if (typeof req.query.state !== "string") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const state: IntegrationOAuthCallbackState = JSON.parse(req.query.state);
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
|
@ -4,6 +4,7 @@
|
||||||
"main": "./index.ts",
|
"main": "./index.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/zoomvideo": "*",
|
"@calcom/zoomvideo": "*",
|
||||||
"@calcom/dailyvideo": "*"
|
"@calcom/dailyvideo": "*",
|
||||||
|
"@calcom/office365video": "*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { compare, hash } from "bcryptjs";
|
import { compare, hash } from "bcryptjs";
|
||||||
|
import { Session } from "next-auth";
|
||||||
|
import { getSession as getSessionInner, GetSessionParams } from "next-auth/react";
|
||||||
|
|
||||||
export async function hashPassword(password: string) {
|
export async function hashPassword(password: string) {
|
||||||
const hashedPassword = await hash(password, 12);
|
const hashedPassword = await hash(password, 12);
|
||||||
|
@ -9,3 +11,10 @@ export async function verifyPassword(password: string, hashedPassword: string) {
|
||||||
const isValid = await compare(password, hashedPassword);
|
const isValid = await compare(password, hashedPassword);
|
||||||
return isValid;
|
return isValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getSession(options: GetSessionParams): Promise<Session | null> {
|
||||||
|
const session = await getSessionInner(options);
|
||||||
|
|
||||||
|
// that these are equal are ensured in `[...nextauth]`'s callback
|
||||||
|
return session as Session | null;
|
||||||
|
}
|
||||||
|
|
|
@ -8,4 +8,5 @@ export enum LocationType {
|
||||||
Jitsi = "integrations:jitsi",
|
Jitsi = "integrations:jitsi",
|
||||||
Huddle01 = "integrations:huddle01",
|
Huddle01 = "integrations:huddle01",
|
||||||
Tandem = "integrations:tandem",
|
Tandem = "integrations:tandem",
|
||||||
|
Teams = "integrations:office365_video",
|
||||||
}
|
}
|
||||||
|
|
44
yarn.lock
44
yarn.lock
|
@ -2020,12 +2020,12 @@
|
||||||
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1"
|
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1"
|
||||||
integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==
|
integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==
|
||||||
|
|
||||||
"@prisma/client@3.10.0":
|
"@prisma/client@3.9.2":
|
||||||
version "3.10.0"
|
version "3.9.2"
|
||||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.10.0.tgz#4782fe6f1b0e43c2a11a75ad4bb1098599d1dfb1"
|
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.9.2.tgz#ad17dcfb702842573fe6ec3b7dc4615eff8d8fc6"
|
||||||
integrity sha512-6P4sV7WFuODSfSoSEzCH1qfmWMrCUBk1LIIuTbQf6m1LI/IOpLN4lnqGDmgiBGprEzuWobnGLfe9YsXLn0inrg==
|
integrity sha512-VlEIYVMyfFZHbVBOlunPl47gmP/Z0zzPjPj8I7uKEIaABqrUy50ru3XS0aZd8GFvevVwt7p91xxkUjNjrWhKAQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@prisma/engines-version" "3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86"
|
"@prisma/engines-version" "3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009"
|
||||||
|
|
||||||
"@prisma/debug@3.8.1":
|
"@prisma/debug@3.8.1":
|
||||||
version "3.8.1"
|
version "3.8.1"
|
||||||
|
@ -2036,15 +2036,15 @@
|
||||||
ms "2.1.3"
|
ms "2.1.3"
|
||||||
strip-ansi "6.0.1"
|
strip-ansi "6.0.1"
|
||||||
|
|
||||||
"@prisma/engines-version@3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86":
|
"@prisma/engines-version@3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009":
|
||||||
version "3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86"
|
version "3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009"
|
||||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86.tgz#82750856fa637dd89b8f095d2dcc6ac0631231c6"
|
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009.tgz#ea03ffa723382a526dc6625ce6eae9b6ad984400"
|
||||||
integrity sha512-cVYs5gyQH/qyut24hUvDznCfPrWiNMKNfPb9WmEoiU6ihlkscIbCfkmuKTtspVLWRdl0LqjYEC7vfnPv17HWhw==
|
integrity sha512-5Dh+qTDhpPR66w6NNAnPs+/W/Qt4r1DSd+qhfPFcDThUK4uxoZKGlPb2IYQn5LL+18aIGnmteDf7BnVMmvBNSQ==
|
||||||
|
|
||||||
"@prisma/engines@3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86":
|
"@prisma/engines@3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009":
|
||||||
version "3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86"
|
version "3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009"
|
||||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86.tgz#2964113729a78b8b21e186b5592affd1fde73c16"
|
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009.tgz#e5c345cdedb7be83d11c1e0c5ab61d866b411256"
|
||||||
integrity sha512-LjRssaWu9w2SrXitofnutRIyURI7l0veQYIALz7uY4shygM9nMcK3omXcObRm7TAcw3Z+9ytfK1B+ySOsOesxQ==
|
integrity sha512-qM+uJbkelB21bnK44gYE049YTHIjHysOuj0mj5U2gDGyNLfmiazlggzFPCgEjgme4U5YB2tYs6Z5Hq08Kl8pjA==
|
||||||
|
|
||||||
"@prisma/generator-helper@~3.8.1":
|
"@prisma/generator-helper@~3.8.1":
|
||||||
version "3.8.1"
|
version "3.8.1"
|
||||||
|
@ -11676,12 +11676,12 @@ prism-react-renderer@^1.1.1:
|
||||||
resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.3.1.tgz#88fc9d0df6bed06ca2b9097421349f8c2f24e30d"
|
resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.3.1.tgz#88fc9d0df6bed06ca2b9097421349f8c2f24e30d"
|
||||||
integrity sha512-xUeDMEz074d0zc5y6rxiMp/dlC7C+5IDDlaEUlcBOFE2wddz7hz5PNupb087mPwTt7T9BrFmewObfCBuf/LKwQ==
|
integrity sha512-xUeDMEz074d0zc5y6rxiMp/dlC7C+5IDDlaEUlcBOFE2wddz7hz5PNupb087mPwTt7T9BrFmewObfCBuf/LKwQ==
|
||||||
|
|
||||||
prisma@3.10.0:
|
prisma@3.9.2:
|
||||||
version "3.10.0"
|
version "3.9.2"
|
||||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.10.0.tgz#872d87afbeb1cbcaa77c3d6a63c125e0d704b04d"
|
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.9.2.tgz#cc2da4e8db91231dea7465adf9db6e19f11032a9"
|
||||||
integrity sha512-dAld12vtwdz9Rz01nOjmnXe+vHana5PSog8t0XGgLemKsUVsaupYpr74AHaS3s78SaTS5s2HOghnJF+jn91ZrA==
|
integrity sha512-i9eK6cexV74OgeWaH3+e6S07kvC9jEZTl6BqtBH398nlCU0tck7mE9dicY6YQd+euvMjjCtY89q4NgmaPnUsSg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@prisma/engines" "3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86"
|
"@prisma/engines" "3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009"
|
||||||
|
|
||||||
process-es6@^0.11.2:
|
process-es6@^0.11.2:
|
||||||
version "0.11.6"
|
version "0.11.6"
|
||||||
|
@ -13929,10 +13929,10 @@ ts-morph@^13.0.2:
|
||||||
"@ts-morph/common" "~0.12.3"
|
"@ts-morph/common" "~0.12.3"
|
||||||
code-block-writer "^11.0.0"
|
code-block-writer "^11.0.0"
|
||||||
|
|
||||||
ts-node@^10.6.0:
|
ts-node@^10.2.1:
|
||||||
version "10.6.0"
|
version "10.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.6.0.tgz#c3f4195d5173ce3affdc8f2fd2e9a7ac8de5376a"
|
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.5.0.tgz#618bef5854c1fbbedf5e31465cbb224a1d524ef9"
|
||||||
integrity sha512-CJen6+dfOXolxudBQXnVjRVvYTmTWbyz7cn+xq2XTsvnaXbHqr4gXSCNbS2Jj8yTZMuGwUoBESLaOkLascVVvg==
|
integrity sha512-6kEJKwVxAJ35W4akuiysfKwKmjkbYxwQMTBaAxo9KKAx/Yd26mPUyhGz3ji+EsJoAgrLqVsYHNuuYwQe22lbtw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@cspotcode/source-map-support" "0.7.0"
|
"@cspotcode/source-map-support" "0.7.0"
|
||||||
"@tsconfig/node10" "^1.0.7"
|
"@tsconfig/node10" "^1.0.7"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user