Merge branch 'main' into feature/saml-login
# Conflicts: # package.jsonfeature/saml-login
commit
4680222f5e
14
.env.example
14
.env.example
|
@ -8,7 +8,7 @@
|
||||||
NEXT_PUBLIC_LICENSE_CONSENT=''
|
NEXT_PUBLIC_LICENSE_CONSENT=''
|
||||||
|
|
||||||
# DATABASE_URL='postgresql://<user>:<pass>@<db-host>:<db-port>/<db-name>'
|
# DATABASE_URL='postgresql://<user>:<pass>@<db-host>:<db-port>/<db-name>'
|
||||||
DATABASE_URL="postgresql://postgres:@localhost:5432/calendso?schema=public"
|
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
|
||||||
|
|
||||||
GOOGLE_API_CREDENTIALS='secret'
|
GOOGLE_API_CREDENTIALS='secret'
|
||||||
|
|
||||||
|
@ -22,12 +22,12 @@ GOOGLE_CLIENT_ID=
|
||||||
GOOGLE_CLIENT_SECRET=
|
GOOGLE_CLIENT_SECRET=
|
||||||
|
|
||||||
# Enable SAML login using https://github.com/boxyhq/jackson
|
# Enable SAML login using https://github.com/boxyhq/jackson
|
||||||
SAML_LOGIN_URL=
|
SAML_LOGIN_URL='http://localhost:5000'
|
||||||
SAML_API_URL=
|
SAML_API_URL='http://localhost:6000'
|
||||||
SAML_TENANT_ID=
|
SAML_TENANT_ID='Cal.com'
|
||||||
SAML_PRODUCT_ID=
|
SAML_PRODUCT_ID='Cal.com'
|
||||||
JACKSON_API_KEYS=
|
JACKSON_API_KEYS='secret'
|
||||||
SAML_ADMINS=
|
SAML_ADMINS='onboarding@example.com'
|
||||||
|
|
||||||
# @see: https://github.com/calendso/calendso/issues/263
|
# @see: https://github.com/calendso/calendso/issues/263
|
||||||
# Required for Vercel hosting - set NEXTAUTH_URL to equal your BASE_URL
|
# Required for Vercel hosting - set NEXTAUTH_URL to equal your BASE_URL
|
||||||
|
|
|
@ -0,0 +1,257 @@
|
||||||
|
// SPDX-FileCopyrightText: © 2019 EteSync Authors
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
// https://github.com/mozilla-comm/ical.js/issues/367#issuecomment-568493517
|
||||||
|
declare module "ical.js" {
|
||||||
|
function parse(input: string): any[];
|
||||||
|
|
||||||
|
export class helpers {
|
||||||
|
public updateTimezones(vcal: Component): Component;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Component {
|
||||||
|
public fromString(str: string): Component;
|
||||||
|
|
||||||
|
public name: string;
|
||||||
|
|
||||||
|
constructor(jCal: any[] | string, parent?: Component);
|
||||||
|
|
||||||
|
public toJSON(): any[];
|
||||||
|
|
||||||
|
public getFirstSubcomponent(name?: string): Component | null;
|
||||||
|
public getAllSubcomponents(name?: string): Component[];
|
||||||
|
|
||||||
|
public getFirstPropertyValue<T = any>(name?: string): T;
|
||||||
|
|
||||||
|
public getFirstProperty(name?: string): Property;
|
||||||
|
public getAllProperties(name?: string): Property[];
|
||||||
|
|
||||||
|
public addProperty(property: Property): Property;
|
||||||
|
public addPropertyWithValue(name: string, value: string | number | Record<string, unknown>): Property;
|
||||||
|
|
||||||
|
public hasProperty(name?: string): boolean;
|
||||||
|
|
||||||
|
public updatePropertyWithValue(name: string, value: string | number | Record<string, unknown>): Property;
|
||||||
|
|
||||||
|
public removeAllProperties(name?: string): boolean;
|
||||||
|
|
||||||
|
public addSubcomponent(component: Component): Component;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Event {
|
||||||
|
public uid: string;
|
||||||
|
public summary: string;
|
||||||
|
public startDate: Time;
|
||||||
|
public endDate: Time;
|
||||||
|
public description: string;
|
||||||
|
public location: string;
|
||||||
|
public attendees: Property[];
|
||||||
|
/**
|
||||||
|
* The sequence value for this event. Used for scheduling.
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof Event
|
||||||
|
*/
|
||||||
|
public sequence: number;
|
||||||
|
/**
|
||||||
|
* The duration. This can be the result directly from the property, or the
|
||||||
|
* duration calculated from start date and end date. Setting the property
|
||||||
|
* will remove any `dtend` properties.
|
||||||
|
*
|
||||||
|
* @type {Duration}
|
||||||
|
* @memberof Event
|
||||||
|
*/
|
||||||
|
public duration: Duration;
|
||||||
|
/**
|
||||||
|
* The organizer value as an uri. In most cases this is a mailto: uri,
|
||||||
|
* but it can also be something else, like urn:uuid:...
|
||||||
|
*/
|
||||||
|
public organizer: string;
|
||||||
|
/** The sequence value for this event. Used for scheduling */
|
||||||
|
public sequence: number;
|
||||||
|
/** The recurrence id for this event */
|
||||||
|
public recurrenceId: Time;
|
||||||
|
|
||||||
|
public component: Component;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
component?: Component | null,
|
||||||
|
options?: { strictExceptions: boolean; exepctions: Array<Component | Event> }
|
||||||
|
);
|
||||||
|
|
||||||
|
public isRecurring(): boolean;
|
||||||
|
public iterator(startTime?: Time): RecurExpansion;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Property {
|
||||||
|
public name: string;
|
||||||
|
public type: string;
|
||||||
|
|
||||||
|
constructor(jCal: any[] | string, parent?: Component);
|
||||||
|
|
||||||
|
public getFirstValue<T = any>(): T;
|
||||||
|
public getValues<T = any>(): T[];
|
||||||
|
|
||||||
|
public setParameter(name: string, value: string | string[]): void;
|
||||||
|
public setValue(value: string | Record<string, unknown>): void;
|
||||||
|
public setValues(values: (string | Record<string, unknown>)[]): void;
|
||||||
|
public toJSON(): any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TimeJsonData {
|
||||||
|
year?: number;
|
||||||
|
month?: number;
|
||||||
|
day?: number;
|
||||||
|
hour?: number;
|
||||||
|
minute?: number;
|
||||||
|
second?: number;
|
||||||
|
isDate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Time {
|
||||||
|
public fromString(str: string): Time;
|
||||||
|
public fromJSDate(aDate: Date | null, useUTC: boolean): Time;
|
||||||
|
public fromData(aData: TimeJsonData): Time;
|
||||||
|
|
||||||
|
public now(): Time;
|
||||||
|
|
||||||
|
public isDate: boolean;
|
||||||
|
public timezone: string;
|
||||||
|
public zone: Timezone;
|
||||||
|
|
||||||
|
public year: number;
|
||||||
|
public month: number;
|
||||||
|
public day: number;
|
||||||
|
public hour: number;
|
||||||
|
public minute: number;
|
||||||
|
public second: number;
|
||||||
|
|
||||||
|
constructor(data?: TimeJsonData);
|
||||||
|
public compare(aOther: Time): number;
|
||||||
|
|
||||||
|
public clone(): Time;
|
||||||
|
public convertToZone(zone: Timezone): Time;
|
||||||
|
|
||||||
|
public adjust(
|
||||||
|
aExtraDays: number,
|
||||||
|
aExtraHours: number,
|
||||||
|
aExtraMinutes: number,
|
||||||
|
aExtraSeconds: number,
|
||||||
|
aTimeopt?: Time
|
||||||
|
): void;
|
||||||
|
|
||||||
|
public addDuration(aDuration: Duration): void;
|
||||||
|
public subtractDateTz(aDate: Time): Duration;
|
||||||
|
|
||||||
|
public toUnixTime(): number;
|
||||||
|
public toJSDate(): Date;
|
||||||
|
public toJSON(): TimeJsonData;
|
||||||
|
public get icaltype(): "date" | "date-time";
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Duration {
|
||||||
|
public weeks: number;
|
||||||
|
public days: number;
|
||||||
|
public hours: number;
|
||||||
|
public minutes: number;
|
||||||
|
public seconds: number;
|
||||||
|
public isNegative: boolean;
|
||||||
|
public icalclass: string;
|
||||||
|
public icaltype: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RecurExpansion {
|
||||||
|
public complete: boolean;
|
||||||
|
public dtstart: Time;
|
||||||
|
public last: Time;
|
||||||
|
public next(): Time;
|
||||||
|
public fromData(options);
|
||||||
|
public toJSON();
|
||||||
|
constructor(options: {
|
||||||
|
/** Start time of the event */
|
||||||
|
dtstart: Time;
|
||||||
|
/** Component for expansion, required if not resuming. */
|
||||||
|
component?: Component;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Timezone {
|
||||||
|
public utcTimezone: Timezone;
|
||||||
|
public localTimezone: Timezone;
|
||||||
|
public convert_time(tt: Time, fromZone: Timezone, toZone: Timezone): Time;
|
||||||
|
|
||||||
|
public tzid: string;
|
||||||
|
public component: Component;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
data:
|
||||||
|
| Component
|
||||||
|
| {
|
||||||
|
component: string | Component;
|
||||||
|
tzid?: string;
|
||||||
|
location?: string;
|
||||||
|
tznames?: string;
|
||||||
|
latitude?: number;
|
||||||
|
longitude?: number;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TimezoneService {
|
||||||
|
public get(tzid: string): Timezone | null;
|
||||||
|
public has(tzid: string): boolean;
|
||||||
|
public register(tzid: string, zone: Timezone | Component);
|
||||||
|
public remove(tzid: string): Timezone | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FrequencyValues =
|
||||||
|
| "YEARLY"
|
||||||
|
| "MONTHLY"
|
||||||
|
| "WEEKLY"
|
||||||
|
| "DAILY"
|
||||||
|
| "HOURLY"
|
||||||
|
| "MINUTELY"
|
||||||
|
| "SECONDLY";
|
||||||
|
|
||||||
|
export enum WeekDay {
|
||||||
|
SU = 1,
|
||||||
|
MO,
|
||||||
|
TU,
|
||||||
|
WE,
|
||||||
|
TH,
|
||||||
|
FR,
|
||||||
|
SA,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RecurData {
|
||||||
|
public freq?: FrequencyValues;
|
||||||
|
public interval?: number;
|
||||||
|
public wkst?: WeekDay;
|
||||||
|
public until?: Time;
|
||||||
|
public count?: number;
|
||||||
|
public bysecond?: number[] | number;
|
||||||
|
public byminute?: number[] | number;
|
||||||
|
public byhour?: number[] | number;
|
||||||
|
public byday?: string[] | string;
|
||||||
|
public bymonthday?: number[] | number;
|
||||||
|
public byyearday?: number[] | number;
|
||||||
|
public byweekno?: number[] | number;
|
||||||
|
public bymonth?: number[] | number;
|
||||||
|
public bysetpos?: number[] | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RecurIterator {
|
||||||
|
public next(): Time;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Recur {
|
||||||
|
constructor(data?: RecurData);
|
||||||
|
public until: Time | null;
|
||||||
|
public freq: FrequencyValues;
|
||||||
|
public count: number | null;
|
||||||
|
|
||||||
|
public clone(): Recur;
|
||||||
|
public toJSON(): Omit<RecurData, "until"> & { until?: string };
|
||||||
|
public iterator(startTime?: Time): RecurIterator;
|
||||||
|
public isByCount(): boolean;
|
||||||
|
}
|
||||||
|
}
|
|
@ -125,9 +125,9 @@ export default function ImageUploader({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="flex flex-col items-center justify-center p-8 mt-6 cropper bg-gray-50">
|
<div className="flex flex-col items-center justify-center p-8 mt-6 cropper">
|
||||||
{!result && (
|
{!result && (
|
||||||
<div className="flex items-center justify-start w-20 h-20 bg-gray-500 rounded-full max-h-20">
|
<div className="flex items-center justify-start w-20 h-20 bg-gray-50 rounded-full max-h-20">
|
||||||
{!imageSrc && (
|
{!imageSrc && (
|
||||||
<p className="w-full text-sm text-center text-white sm:text-xs">
|
<p className="w-full text-sm text-center text-white sm:text-xs">
|
||||||
{t("no_target", { target })}
|
{t("no_target", { target })}
|
||||||
|
|
|
@ -1,32 +1,26 @@
|
||||||
import dayjs from "dayjs";
|
|
||||||
import short from "short-uuid";
|
import short from "short-uuid";
|
||||||
import { v5 as uuidv5 } from "uuid";
|
import { v5 as uuidv5 } from "uuid";
|
||||||
|
|
||||||
import { getIntegrationName } from "@lib/integrations";
|
import { getIntegrationName } from "@lib/integrations";
|
||||||
|
|
||||||
import { CalendarEvent } from "./calendarClient";
|
import { CalendarEvent, Person } from "./calendarClient";
|
||||||
import { BASE_URL } from "./config/constants";
|
import { BASE_URL } from "./config/constants";
|
||||||
|
|
||||||
const translator = short();
|
const translator = short();
|
||||||
|
|
||||||
|
// The odd indentation in this file is necessary because otherwise the leading tabs will be applied into the event description.
|
||||||
|
|
||||||
export const getWhat = (calEvent: CalendarEvent) => {
|
export const getWhat = (calEvent: CalendarEvent) => {
|
||||||
return `
|
return `
|
||||||
${calEvent.language("what")}
|
${calEvent.language("what")}:
|
||||||
${calEvent.type}
|
${calEvent.type}
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getWhen = (calEvent: CalendarEvent) => {
|
export const getWhen = (calEvent: CalendarEvent) => {
|
||||||
const inviteeStart = dayjs(calEvent.startTime).tz(calEvent.attendees[0].timeZone);
|
|
||||||
const inviteeEnd = dayjs(calEvent.endTime).tz(calEvent.attendees[0].timeZone);
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
${calEvent.language("when")}
|
${calEvent.language("invitee_timezone")}:
|
||||||
${calEvent.language(inviteeStart.format("dddd").toLowerCase())}, ${calEvent.language(
|
${calEvent.attendees[0].timeZone}
|
||||||
inviteeStart.format("MMMM").toLowerCase()
|
|
||||||
)} ${inviteeStart.format("D")}, ${inviteeStart.format("YYYY")} | ${inviteeStart.format(
|
|
||||||
"h:mma"
|
|
||||||
)} - ${inviteeEnd.format("h:mma")} (${calEvent.attendees[0].timeZone})
|
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -46,14 +40,14 @@ ${calEvent.organizer.email}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
${calEvent.language("who")}
|
${calEvent.language("who")}:
|
||||||
${organizer + attendees}
|
${organizer + attendees}
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAdditionalNotes = (calEvent: CalendarEvent) => {
|
export const getAdditionalNotes = (calEvent: CalendarEvent) => {
|
||||||
return `
|
return `
|
||||||
${calEvent.language("additional_notes")}
|
${calEvent.language("additional_notes")}:
|
||||||
${calEvent.description}
|
${calEvent.description}
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
@ -92,13 +86,28 @@ export const getCancelLink = (calEvent: CalendarEvent): string => {
|
||||||
return BASE_URL + "/cancel/" + getUid(calEvent);
|
return BASE_URL + "/cancel/" + getUid(calEvent);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getRichDescription = (calEvent: CalendarEvent) => {
|
export const getRichDescription = (calEvent: CalendarEvent, attendee?: Person) => {
|
||||||
|
// Only the original attendee can make changes to the event
|
||||||
|
// Guests cannot
|
||||||
|
|
||||||
|
if (attendee && attendee === calEvent.attendees[0]) {
|
||||||
|
return `
|
||||||
|
${getWhat(calEvent)}
|
||||||
|
${getWhen(calEvent)}
|
||||||
|
${getWho(calEvent)}
|
||||||
|
${calEvent.language("where")}:
|
||||||
|
${getLocation(calEvent)}
|
||||||
|
${getAdditionalNotes(calEvent)}
|
||||||
|
`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
${getWhat(calEvent)}
|
${getWhat(calEvent)}
|
||||||
${getWhen(calEvent)}
|
${getWhen(calEvent)}
|
||||||
${getWho(calEvent)}
|
${getWho(calEvent)}
|
||||||
${getLocation(calEvent)}
|
${calEvent.language("where")}:
|
||||||
${getAdditionalNotes(calEvent)}
|
${getLocation(calEvent)}
|
||||||
${getManageLink(calEvent)}
|
${getAdditionalNotes(calEvent)}
|
||||||
`;
|
${getManageLink(calEvent)}
|
||||||
|
`.trim();
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,6 +19,8 @@ import {
|
||||||
import logger from "@lib/logger";
|
import logger from "@lib/logger";
|
||||||
import { VideoCallData } from "@lib/videoClient";
|
import { VideoCallData } from "@lib/videoClient";
|
||||||
|
|
||||||
|
import { Ensure } from "./types/utils";
|
||||||
|
|
||||||
const log = logger.getChildLogger({ prefix: ["[lib] calendarClient"] });
|
const log = logger.getChildLogger({ prefix: ["[lib] calendarClient"] });
|
||||||
|
|
||||||
export type Person = { name: string; email: string; timeZone: string };
|
export type Person = { name: string; email: string; timeZone: string };
|
||||||
|
@ -61,7 +63,7 @@ export interface CalendarEvent {
|
||||||
paymentInfo?: PaymentInfo | null;
|
paymentInfo?: PaymentInfo | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IntegrationCalendar extends Partial<SelectedCalendar> {
|
export interface IntegrationCalendar extends Ensure<Partial<SelectedCalendar>, "externalId"> {
|
||||||
primary?: boolean;
|
primary?: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
}
|
}
|
||||||
|
@ -89,11 +91,9 @@ function getCalendarAdapterOrNull(credential: Credential): CalendarApiAdapter |
|
||||||
case "office365_calendar":
|
case "office365_calendar":
|
||||||
return Office365CalendarApiAdapter(credential);
|
return Office365CalendarApiAdapter(credential);
|
||||||
case "caldav_calendar":
|
case "caldav_calendar":
|
||||||
// FIXME types wrong & type casting should not be needed
|
return new CalDavCalendar(credential);
|
||||||
return new CalDavCalendar(credential) as never as CalendarApiAdapter;
|
|
||||||
case "apple_calendar":
|
case "apple_calendar":
|
||||||
// FIXME types wrong & type casting should not be needed
|
return new AppleCalendar(credential);
|
||||||
return new AppleCalendar(credential) as never as CalendarApiAdapter;
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ export async function getCroppedImg(imageSrc: string, pixelCrop: Area): Promise<
|
||||||
// on very low ratios, the quality of the resize becomes awful. For this reason the resizeRatio is limited to 0.75
|
// on very low ratios, the quality of the resize becomes awful. For this reason the resizeRatio is limited to 0.75
|
||||||
if (resizeRatio <= 0.75) {
|
if (resizeRatio <= 0.75) {
|
||||||
// With a smaller image, thus improved ratio. Keep doing this until the resizeRatio > 0.75.
|
// With a smaller image, thus improved ratio. Keep doing this until the resizeRatio > 0.75.
|
||||||
return getCroppedImg(canvas.toDataURL("image/jpeg"), {
|
return getCroppedImg(canvas.toDataURL("image/png"), {
|
||||||
width: canvas.width,
|
width: canvas.width,
|
||||||
height: canvas.height,
|
height: canvas.height,
|
||||||
x: 0,
|
x: 0,
|
||||||
|
@ -53,5 +53,5 @@ export async function getCroppedImg(imageSrc: string, pixelCrop: Area): Promise<
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return canvas.toDataURL("image/jpeg");
|
return canvas.toDataURL("image/png");
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import utc from "dayjs/plugin/utc";
|
||||||
import { createEvent, DateArray } from "ics";
|
import { createEvent, DateArray } from "ics";
|
||||||
import nodemailer from "nodemailer";
|
import nodemailer from "nodemailer";
|
||||||
|
|
||||||
import { getCancelLink } from "@lib/CalEventParser";
|
import { getCancelLink, getRichDescription } from "@lib/CalEventParser";
|
||||||
import { CalendarEvent, Person } from "@lib/calendarClient";
|
import { CalendarEvent, Person } from "@lib/calendarClient";
|
||||||
import { getErrorFromUnknown } from "@lib/errors";
|
import { getErrorFromUnknown } from "@lib/errors";
|
||||||
import { getIntegrationName } from "@lib/integrations";
|
import { getIntegrationName } from "@lib/integrations";
|
||||||
|
@ -113,30 +113,12 @@ export default class AttendeeScheduledEmail {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getTextBody(): string {
|
protected getTextBody(): string {
|
||||||
// Only the original attendee can make changes to the event
|
|
||||||
// Guests cannot
|
|
||||||
if (this.attendee === this.calEvent.attendees[0]) {
|
|
||||||
return `
|
|
||||||
${this.calEvent.language("your_event_has_been_scheduled")}
|
|
||||||
${this.calEvent.language("emailed_you_and_any_other_attendees")}
|
|
||||||
${this.getWhat()}
|
|
||||||
${this.getWhen()}
|
|
||||||
${this.getWho()}
|
|
||||||
${this.getLocation()}
|
|
||||||
${this.getAdditionalNotes()}
|
|
||||||
${this.calEvent.language("need_to_reschedule_or_cancel")}
|
|
||||||
${getCancelLink(this.calEvent)}
|
|
||||||
`.replace(/(<([^>]+)>)/gi, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
${this.calEvent.language("your_event_has_been_scheduled")}
|
${this.calEvent.language("your_event_has_been_scheduled")}
|
||||||
${this.calEvent.language("emailed_you_and_any_other_attendees")}
|
${this.calEvent.language("emailed_you_and_any_other_attendees")}
|
||||||
${this.getWhat()}
|
|
||||||
${this.getWhen()}
|
${getRichDescription(this.calEvent)}
|
||||||
${this.getLocation()}
|
`.trim();
|
||||||
${this.getAdditionalNotes()}
|
|
||||||
`.replace(/(<([^>]+)>)/gi, "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected printNodeMailerError(error: Error): void {
|
protected printNodeMailerError(error: Error): void {
|
||||||
|
@ -364,12 +346,12 @@ ${this.getAdditionalNotes()}
|
||||||
<p style="height: 6px"></p>
|
<p style="height: 6px"></p>
|
||||||
<div style="line-height: 6px;">
|
<div style="line-height: 6px;">
|
||||||
<p style="color: #494949;">${this.calEvent.language("where")}</p>
|
<p style="color: #494949;">${this.calEvent.language("where")}</p>
|
||||||
<p style="color: #494949; font-weight: 400; line-height: 24px;">${
|
<p style="color: #494949; font-weight: 400; line-height: 24px;">${providerName} ${
|
||||||
hangoutLink &&
|
hangoutLink &&
|
||||||
`<a href="${hangoutLink}" target="_blank" alt="${this.calEvent.language(
|
`<a href="${hangoutLink}" target="_blank" alt="${this.calEvent.language(
|
||||||
"meeting_url"
|
"meeting_url"
|
||||||
)}"><img src="${linkIcon()}" width="12px"></img></a>`
|
)}"><img src="${linkIcon()}" width="12px"></img></a>`
|
||||||
}</p>
|
}</p>
|
||||||
<div style="color: #494949; font-weight: 400; line-height: 24px;"><a href="${hangoutLink}" alt="${this.calEvent.language(
|
<div style="color: #494949; font-weight: 400; line-height: 24px;"><a href="${hangoutLink}" alt="${this.calEvent.language(
|
||||||
"meeting_url"
|
"meeting_url"
|
||||||
)}" style="color: #3E3E3E" target="_blank">${hangoutLink}</a></div>
|
)}" style="color: #3E3E3E" target="_blank">${hangoutLink}</a></div>
|
||||||
|
|
|
@ -6,7 +6,7 @@ import utc from "dayjs/plugin/utc";
|
||||||
import { createEvent, DateArray } from "ics";
|
import { createEvent, DateArray } from "ics";
|
||||||
import nodemailer from "nodemailer";
|
import nodemailer from "nodemailer";
|
||||||
|
|
||||||
import { getCancelLink } from "@lib/CalEventParser";
|
import { getCancelLink, getRichDescription } from "@lib/CalEventParser";
|
||||||
import { CalendarEvent, Person } from "@lib/calendarClient";
|
import { CalendarEvent, Person } from "@lib/calendarClient";
|
||||||
import { getErrorFromUnknown } from "@lib/errors";
|
import { getErrorFromUnknown } from "@lib/errors";
|
||||||
import { getIntegrationName } from "@lib/integrations";
|
import { getIntegrationName } from "@lib/integrations";
|
||||||
|
@ -123,14 +123,9 @@ export default class OrganizerScheduledEmail {
|
||||||
return `
|
return `
|
||||||
${this.calEvent.language("new_event_scheduled")}
|
${this.calEvent.language("new_event_scheduled")}
|
||||||
${this.calEvent.language("emailed_you_and_any_other_attendees")}
|
${this.calEvent.language("emailed_you_and_any_other_attendees")}
|
||||||
${this.getWhat()}
|
|
||||||
${this.getWhen()}
|
${getRichDescription(this.calEvent)}
|
||||||
${this.getWho()}
|
`.trim();
|
||||||
${this.getLocation()}
|
|
||||||
${this.getAdditionalNotes()}
|
|
||||||
${this.calEvent.language("need_to_reschedule_or_cancel")}
|
|
||||||
${getCancelLink(this.calEvent)}
|
|
||||||
`.replace(/(<([^>]+)>)/gi, "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected printNodeMailerError(error: Error): void {
|
protected printNodeMailerError(error: Error): void {
|
||||||
|
@ -352,12 +347,12 @@ ${getCancelLink(this.calEvent)}
|
||||||
<p style="height: 6px"></p>
|
<p style="height: 6px"></p>
|
||||||
<div style="line-height: 6px;">
|
<div style="line-height: 6px;">
|
||||||
<p style="color: #494949;">${this.calEvent.language("where")}</p>
|
<p style="color: #494949;">${this.calEvent.language("where")}</p>
|
||||||
<p style="color: #494949; font-weight: 400; line-height: 24px;">${
|
<p style="color: #494949; font-weight: 400; line-height: 24px;">${providerName} ${
|
||||||
hangoutLink &&
|
hangoutLink &&
|
||||||
`<a href="${hangoutLink}" target="_blank" alt="${this.calEvent.language(
|
`<a href="${hangoutLink}" target="_blank" alt="${this.calEvent.language(
|
||||||
"meeting_url"
|
"meeting_url"
|
||||||
)}"><img src="${linkIcon()}" width="12px"></img></a>`
|
)}"><img src="${linkIcon()}" width="12px"></img></a>`
|
||||||
}</p>
|
}</p>
|
||||||
<div style="color: #494949; font-weight: 400; line-height: 24px;"><a href="${hangoutLink}" alt="${this.calEvent.language(
|
<div style="color: #494949; font-weight: 400; line-height: 24px;"><a href="${hangoutLink}" alt="${this.calEvent.language(
|
||||||
"meeting_url"
|
"meeting_url"
|
||||||
)}" style="color: #3E3E3E" target="_blank">${hangoutLink}</a></div>
|
)}" style="color: #3E3E3E" target="_blank">${hangoutLink}</a></div>
|
||||||
|
|
|
@ -24,8 +24,6 @@ dayjs.extend(utc);
|
||||||
|
|
||||||
const log = logger.getChildLogger({ prefix: ["[[lib] apple calendar"] });
|
const log = logger.getChildLogger({ prefix: ["[[lib] apple calendar"] });
|
||||||
|
|
||||||
type EventBusyDate = Record<"start" | "end", Date>;
|
|
||||||
|
|
||||||
export class AppleCalendar implements CalendarApiAdapter {
|
export class AppleCalendar implements CalendarApiAdapter {
|
||||||
private url: string;
|
private url: string;
|
||||||
private credentials: Record<string, string>;
|
private credentials: Record<string, string>;
|
||||||
|
@ -34,7 +32,7 @@ export class AppleCalendar implements CalendarApiAdapter {
|
||||||
|
|
||||||
constructor(credential: Credential) {
|
constructor(credential: Credential) {
|
||||||
const decryptedCredential = JSON.parse(
|
const decryptedCredential = JSON.parse(
|
||||||
symmetricDecrypt(credential.key, process.env.CALENDSO_ENCRYPTION_KEY)
|
symmetricDecrypt(credential.key as string, process.env.CALENDSO_ENCRYPTION_KEY!)
|
||||||
);
|
);
|
||||||
const username = decryptedCredential.username;
|
const username = decryptedCredential.username;
|
||||||
const password = decryptedCredential.password;
|
const password = decryptedCredential.password;
|
||||||
|
@ -52,12 +50,12 @@ export class AppleCalendar implements CalendarApiAdapter {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
convertDate(date: string): number[] {
|
convertDate(date: string): [number, number, number] {
|
||||||
return dayjs(date)
|
return dayjs(date)
|
||||||
.utc()
|
.utc()
|
||||||
.toArray()
|
.toArray()
|
||||||
.slice(0, 6)
|
.slice(0, 6)
|
||||||
.map((v, i) => (i === 1 ? v + 1 : v));
|
.map((v, i) => (i === 1 ? v + 1 : v)) as [number, number, number];
|
||||||
}
|
}
|
||||||
|
|
||||||
getDuration(start: string, end: string): DurationObject {
|
getDuration(start: string, end: string): DurationObject {
|
||||||
|
@ -70,11 +68,11 @@ export class AppleCalendar implements CalendarApiAdapter {
|
||||||
return attendees.map(({ email, name }) => ({ name, email, partstat: "NEEDS-ACTION" }));
|
return attendees.map(({ email, name }) => ({ name, email, partstat: "NEEDS-ACTION" }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async createEvent(event: CalendarEvent): Promise<Record<string, unknown>> {
|
async createEvent(event: CalendarEvent) {
|
||||||
try {
|
try {
|
||||||
const calendars = await this.listCalendars();
|
const calendars = await this.listCalendars();
|
||||||
const uid = uuidv4();
|
const uid = uuidv4();
|
||||||
const { error, value: iCalString } = await createEvent({
|
const { error, value: iCalString } = createEvent({
|
||||||
uid,
|
uid,
|
||||||
startInputType: "utc",
|
startInputType: "utc",
|
||||||
start: this.convertDate(event.startTime),
|
start: this.convertDate(event.startTime),
|
||||||
|
@ -86,15 +84,9 @@ export class AppleCalendar implements CalendarApiAdapter {
|
||||||
attendees: this.getAttendees(event.attendees),
|
attendees: this.getAttendees(event.attendees),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) throw new Error("Error creating iCalString");
|
||||||
log.debug("Error creating iCalString");
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!iCalString) {
|
if (!iCalString) throw new Error("Error creating iCalString");
|
||||||
log.debug("Error creating iCalString");
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
calendars.map((calendar) => {
|
calendars.map((calendar) => {
|
||||||
|
@ -112,6 +104,9 @@ export class AppleCalendar implements CalendarApiAdapter {
|
||||||
return {
|
return {
|
||||||
uid,
|
uid,
|
||||||
id: uid,
|
id: uid,
|
||||||
|
type: "apple_calendar",
|
||||||
|
password: "",
|
||||||
|
url: "",
|
||||||
};
|
};
|
||||||
} catch (reason) {
|
} catch (reason) {
|
||||||
console.error(reason);
|
console.error(reason);
|
||||||
|
@ -132,7 +127,7 @@ export class AppleCalendar implements CalendarApiAdapter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { error, value: iCalString } = await createEvent({
|
const { error, value: iCalString } = createEvent({
|
||||||
uid,
|
uid,
|
||||||
startInputType: "utc",
|
startInputType: "utc",
|
||||||
start: this.convertDate(event.startTime),
|
start: this.convertDate(event.startTime),
|
||||||
|
@ -201,11 +196,7 @@ export class AppleCalendar implements CalendarApiAdapter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAvailability(
|
async getAvailability(dateFrom: string, dateTo: string, selectedCalendars: IntegrationCalendar[]) {
|
||||||
dateFrom: string,
|
|
||||||
dateTo: string,
|
|
||||||
selectedCalendars: IntegrationCalendar[]
|
|
||||||
): Promise<EventBusyDate[]> {
|
|
||||||
try {
|
try {
|
||||||
const selectedCalendarIds = selectedCalendars
|
const selectedCalendarIds = selectedCalendars
|
||||||
.filter((e) => e.integration === this.integrationName)
|
.filter((e) => e.integration === this.integrationName)
|
||||||
|
@ -229,8 +220,8 @@ export class AppleCalendar implements CalendarApiAdapter {
|
||||||
ids.map(async (calId) => {
|
ids.map(async (calId) => {
|
||||||
return (await this.getEvents(calId, dateFrom, dateTo)).map((event) => {
|
return (await this.getEvents(calId, dateFrom, dateTo)).map((event) => {
|
||||||
return {
|
return {
|
||||||
start: event.startDate,
|
start: event.startDate.toISOString(),
|
||||||
end: event.endDate,
|
end: event.endDate.toISOString(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
@ -272,8 +263,8 @@ export class AppleCalendar implements CalendarApiAdapter {
|
||||||
calId: string,
|
calId: string,
|
||||||
dateFrom: string | null,
|
dateFrom: string | null,
|
||||||
dateTo: string | null,
|
dateTo: string | null,
|
||||||
objectUrls: string[] | null
|
objectUrls?: string[] | null
|
||||||
): Promise<unknown[]> {
|
) {
|
||||||
try {
|
try {
|
||||||
const objects = await fetchCalendarObjects({
|
const objects = await fetchCalendarObjects({
|
||||||
calendar: {
|
calendar: {
|
||||||
|
@ -290,54 +281,48 @@ export class AppleCalendar implements CalendarApiAdapter {
|
||||||
headers: this.headers,
|
headers: this.headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
const events =
|
const events = objects
|
||||||
objects &&
|
.filter((e) => !!e.data)
|
||||||
objects?.length > 0 &&
|
.map((object) => {
|
||||||
objects
|
const jcalData = ICAL.parse(object.data);
|
||||||
.map((object) => {
|
const vcalendar = new ICAL.Component(jcalData);
|
||||||
if (object?.data) {
|
const vevent = vcalendar.getFirstSubcomponent("vevent");
|
||||||
const jcalData = ICAL.parse(object.data);
|
const event = new ICAL.Event(vevent);
|
||||||
const vcalendar = new ICAL.Component(jcalData);
|
|
||||||
const vevent = vcalendar.getFirstSubcomponent("vevent");
|
|
||||||
const event = new ICAL.Event(vevent);
|
|
||||||
|
|
||||||
const calendarTimezone = vcalendar.getFirstSubcomponent("vtimezone")
|
const calendarTimezone =
|
||||||
? vcalendar.getFirstSubcomponent("vtimezone").getFirstPropertyValue("tzid")
|
vcalendar.getFirstSubcomponent("vtimezone")?.getFirstPropertyValue("tzid") || "";
|
||||||
: "";
|
|
||||||
|
|
||||||
const startDate = calendarTimezone
|
const startDate = calendarTimezone
|
||||||
? dayjs(event.startDate).tz(calendarTimezone)
|
? dayjs(event.startDate.toJSDate()).tz(calendarTimezone)
|
||||||
: new Date(event.startDate.toUnixTime() * 1000);
|
: new Date(event.startDate.toUnixTime() * 1000);
|
||||||
const endDate = calendarTimezone
|
const endDate = calendarTimezone
|
||||||
? dayjs(event.endDate).tz(calendarTimezone)
|
? dayjs(event.endDate.toJSDate()).tz(calendarTimezone)
|
||||||
: new Date(event.endDate.toUnixTime() * 1000);
|
: new Date(event.endDate.toUnixTime() * 1000);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uid: event.uid,
|
uid: event.uid,
|
||||||
etag: object.etag,
|
etag: object.etag,
|
||||||
url: object.url,
|
url: object.url,
|
||||||
summary: event.summary,
|
summary: event.summary,
|
||||||
description: event.description,
|
description: event.description,
|
||||||
location: event.location,
|
location: event.location,
|
||||||
sequence: event.sequence,
|
sequence: event.sequence,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
duration: {
|
duration: {
|
||||||
weeks: event.duration.weeks,
|
weeks: event.duration.weeks,
|
||||||
days: event.duration.days,
|
days: event.duration.days,
|
||||||
hours: event.duration.hours,
|
hours: event.duration.hours,
|
||||||
minutes: event.duration.minutes,
|
minutes: event.duration.minutes,
|
||||||
seconds: event.duration.seconds,
|
seconds: event.duration.seconds,
|
||||||
isNegative: event.duration.isNegative,
|
isNegative: event.duration.isNegative,
|
||||||
},
|
},
|
||||||
organizer: event.organizer,
|
organizer: event.organizer,
|
||||||
attendees: event.attendees.map((a) => a.getValues()),
|
attendees: event.attendees.map((a) => a.getValues()),
|
||||||
recurrenceId: event.recurrenceId,
|
recurrenceId: event.recurrenceId,
|
||||||
timezone: calendarTimezone,
|
timezone: calendarTimezone,
|
||||||
};
|
};
|
||||||
}
|
});
|
||||||
})
|
|
||||||
.filter((e) => e != null);
|
|
||||||
|
|
||||||
return events;
|
return events;
|
||||||
} catch (reason) {
|
} catch (reason) {
|
||||||
|
|
|
@ -24,8 +24,6 @@ dayjs.extend(utc);
|
||||||
|
|
||||||
const log = logger.getChildLogger({ prefix: ["[lib] caldav"] });
|
const log = logger.getChildLogger({ prefix: ["[lib] caldav"] });
|
||||||
|
|
||||||
type EventBusyDate = Record<"start" | "end", Date>;
|
|
||||||
|
|
||||||
export class CalDavCalendar implements CalendarApiAdapter {
|
export class CalDavCalendar implements CalendarApiAdapter {
|
||||||
private url: string;
|
private url: string;
|
||||||
private credentials: Record<string, string>;
|
private credentials: Record<string, string>;
|
||||||
|
@ -34,7 +32,7 @@ export class CalDavCalendar implements CalendarApiAdapter {
|
||||||
|
|
||||||
constructor(credential: Credential) {
|
constructor(credential: Credential) {
|
||||||
const decryptedCredential = JSON.parse(
|
const decryptedCredential = JSON.parse(
|
||||||
symmetricDecrypt(credential.key, process.env.CALENDSO_ENCRYPTION_KEY)
|
symmetricDecrypt(credential.key as string, process.env.CALENDSO_ENCRYPTION_KEY!)
|
||||||
);
|
);
|
||||||
const username = decryptedCredential.username;
|
const username = decryptedCredential.username;
|
||||||
const url = decryptedCredential.url;
|
const url = decryptedCredential.url;
|
||||||
|
@ -53,12 +51,12 @@ export class CalDavCalendar implements CalendarApiAdapter {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
convertDate(date: string): number[] {
|
convertDate(date: string): [number, number, number] {
|
||||||
return dayjs(date)
|
return dayjs(date)
|
||||||
.utc()
|
.utc()
|
||||||
.toArray()
|
.toArray()
|
||||||
.slice(0, 6)
|
.slice(0, 6)
|
||||||
.map((v, i) => (i === 1 ? v + 1 : v));
|
.map((v, i) => (i === 1 ? v + 1 : v)) as [number, number, number];
|
||||||
}
|
}
|
||||||
|
|
||||||
getDuration(start: string, end: string): DurationObject {
|
getDuration(start: string, end: string): DurationObject {
|
||||||
|
@ -71,15 +69,14 @@ export class CalDavCalendar implements CalendarApiAdapter {
|
||||||
return attendees.map(({ email, name }) => ({ name, email, partstat: "NEEDS-ACTION" }));
|
return attendees.map(({ email, name }) => ({ name, email, partstat: "NEEDS-ACTION" }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async createEvent(event: CalendarEvent): Promise<Record<string, unknown>> {
|
async createEvent(event: CalendarEvent) {
|
||||||
try {
|
try {
|
||||||
const calendars = await this.listCalendars();
|
const calendars = await this.listCalendars();
|
||||||
const uid = uuidv4();
|
const uid = uuidv4();
|
||||||
|
|
||||||
const { error, value: iCalString } = await createEvent({
|
const { error, value: iCalString } = createEvent({
|
||||||
uid,
|
uid,
|
||||||
startInputType: "utc",
|
startInputType: "utc",
|
||||||
// FIXME types
|
|
||||||
start: this.convertDate(event.startTime),
|
start: this.convertDate(event.startTime),
|
||||||
duration: this.getDuration(event.startTime, event.endTime),
|
duration: this.getDuration(event.startTime, event.endTime),
|
||||||
title: event.title,
|
title: event.title,
|
||||||
|
@ -89,15 +86,9 @@ export class CalDavCalendar implements CalendarApiAdapter {
|
||||||
attendees: this.getAttendees(event.attendees),
|
attendees: this.getAttendees(event.attendees),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) throw new Error("Error creating iCalString");
|
||||||
log.debug("Error creating iCalString");
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!iCalString) {
|
if (!iCalString) throw new Error("Error creating iCalString");
|
||||||
log.debug("Error creating iCalString");
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
calendars.map((calendar) => {
|
calendars.map((calendar) => {
|
||||||
|
@ -115,6 +106,9 @@ export class CalDavCalendar implements CalendarApiAdapter {
|
||||||
return {
|
return {
|
||||||
uid,
|
uid,
|
||||||
id: uid,
|
id: uid,
|
||||||
|
type: "caldav_calendar",
|
||||||
|
password: "",
|
||||||
|
url: "",
|
||||||
};
|
};
|
||||||
} catch (reason) {
|
} catch (reason) {
|
||||||
log.error(reason);
|
log.error(reason);
|
||||||
|
@ -138,7 +132,6 @@ export class CalDavCalendar implements CalendarApiAdapter {
|
||||||
const { error, value: iCalString } = await createEvent({
|
const { error, value: iCalString } = await createEvent({
|
||||||
uid,
|
uid,
|
||||||
startInputType: "utc",
|
startInputType: "utc",
|
||||||
// FIXME - types wrong
|
|
||||||
start: this.convertDate(event.startTime),
|
start: this.convertDate(event.startTime),
|
||||||
duration: this.getDuration(event.startTime, event.endTime),
|
duration: this.getDuration(event.startTime, event.endTime),
|
||||||
title: event.title,
|
title: event.title,
|
||||||
|
@ -205,12 +198,7 @@ export class CalDavCalendar implements CalendarApiAdapter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME - types wrong
|
async getAvailability(dateFrom: string, dateTo: string, selectedCalendars: IntegrationCalendar[]) {
|
||||||
async getAvailability(
|
|
||||||
dateFrom: string,
|
|
||||||
dateTo: string,
|
|
||||||
selectedCalendars: IntegrationCalendar[]
|
|
||||||
): Promise<EventBusyDate[]> {
|
|
||||||
try {
|
try {
|
||||||
const selectedCalendarIds = selectedCalendars
|
const selectedCalendarIds = selectedCalendars
|
||||||
.filter((e) => e.integration === this.integrationName)
|
.filter((e) => e.integration === this.integrationName)
|
||||||
|
@ -235,8 +223,8 @@ export class CalDavCalendar implements CalendarApiAdapter {
|
||||||
ids.map(async (calId) => {
|
ids.map(async (calId) => {
|
||||||
return (await this.getEvents(calId, dateFrom, dateTo)).map((event) => {
|
return (await this.getEvents(calId, dateFrom, dateTo)).map((event) => {
|
||||||
return {
|
return {
|
||||||
start: event.startDate,
|
start: event.startDate.toISOString(),
|
||||||
end: event.endDate,
|
end: event.endDate.toISOString(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
@ -274,7 +262,7 @@ export class CalDavCalendar implements CalendarApiAdapter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEvents(calId: string, dateFrom: string | null, dateTo: string | null): Promise<unknown[]> {
|
async getEvents(calId: string, dateFrom: string | null, dateTo: string | null) {
|
||||||
try {
|
try {
|
||||||
const objects = await fetchCalendarObjects({
|
const objects = await fetchCalendarObjects({
|
||||||
calendar: {
|
calendar: {
|
||||||
|
@ -295,50 +283,47 @@ export class CalDavCalendar implements CalendarApiAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
const events = objects
|
const events = objects
|
||||||
|
.filter((e) => !!e.data)
|
||||||
.map((object) => {
|
.map((object) => {
|
||||||
if (object?.data) {
|
const jcalData = ICAL.parse(object.data);
|
||||||
const jcalData = ICAL.parse(object.data);
|
const vcalendar = new ICAL.Component(jcalData);
|
||||||
const vcalendar = new ICAL.Component(jcalData);
|
const vevent = vcalendar.getFirstSubcomponent("vevent");
|
||||||
const vevent = vcalendar.getFirstSubcomponent("vevent");
|
const event = new ICAL.Event(vevent);
|
||||||
const event = new ICAL.Event(vevent);
|
|
||||||
|
|
||||||
const calendarTimezone = vcalendar.getFirstSubcomponent("vtimezone")
|
const calendarTimezone =
|
||||||
? vcalendar.getFirstSubcomponent("vtimezone").getFirstPropertyValue("tzid")
|
vcalendar.getFirstSubcomponent("vtimezone")?.getFirstPropertyValue("tzid") || "";
|
||||||
: "";
|
|
||||||
|
|
||||||
const startDate = calendarTimezone
|
const startDate = calendarTimezone
|
||||||
? dayjs(event.startDate).tz(calendarTimezone)
|
? dayjs(event.startDate.toJSDate()).tz(calendarTimezone)
|
||||||
: new Date(event.startDate.toUnixTime() * 1000);
|
: new Date(event.startDate.toUnixTime() * 1000);
|
||||||
const endDate = calendarTimezone
|
const endDate = calendarTimezone
|
||||||
? dayjs(event.endDate).tz(calendarTimezone)
|
? dayjs(event.endDate.toJSDate()).tz(calendarTimezone)
|
||||||
: new Date(event.endDate.toUnixTime() * 1000);
|
: new Date(event.endDate.toUnixTime() * 1000);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uid: event.uid,
|
uid: event.uid,
|
||||||
etag: object.etag,
|
etag: object.etag,
|
||||||
url: object.url,
|
url: object.url,
|
||||||
summary: event.summary,
|
summary: event.summary,
|
||||||
description: event.description,
|
description: event.description,
|
||||||
location: event.location,
|
location: event.location,
|
||||||
sequence: event.sequence,
|
sequence: event.sequence,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
duration: {
|
duration: {
|
||||||
weeks: event.duration.weeks,
|
weeks: event.duration.weeks,
|
||||||
days: event.duration.days,
|
days: event.duration.days,
|
||||||
hours: event.duration.hours,
|
hours: event.duration.hours,
|
||||||
minutes: event.duration.minutes,
|
minutes: event.duration.minutes,
|
||||||
seconds: event.duration.seconds,
|
seconds: event.duration.seconds,
|
||||||
isNegative: event.duration.isNegative,
|
isNegative: event.duration.isNegative,
|
||||||
},
|
},
|
||||||
organizer: event.organizer,
|
organizer: event.organizer,
|
||||||
attendees: event.attendees.map((a) => a.getValues()),
|
attendees: event.attendees.map((a) => a.getValues()),
|
||||||
recurrenceId: event.recurrenceId,
|
recurrenceId: event.recurrenceId,
|
||||||
timezone: calendarTimezone,
|
timezone: calendarTimezone,
|
||||||
};
|
};
|
||||||
}
|
});
|
||||||
})
|
|
||||||
.filter((e) => e != null);
|
|
||||||
|
|
||||||
return events;
|
return events;
|
||||||
} catch (reason) {
|
} catch (reason) {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { CalendarApiAdapter, CalendarEvent, IntegrationCalendar } from "@lib/cal
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
|
|
||||||
export interface ConferenceData {
|
export interface ConferenceData {
|
||||||
createRequest: calendar_v3.Schema$CreateConferenceRequest;
|
createRequest?: calendar_v3.Schema$CreateConferenceRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
const googleAuth = (credential: Credential) => {
|
const googleAuth = (credential: Credential) => {
|
||||||
|
@ -91,7 +91,19 @@ export const GoogleCalendarApiAdapter = (credential: Credential): CalendarApiAda
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
resolve(Object.values(apires.data.calendars).flatMap((item) => item["busy"]));
|
let result: Prisma.PromiseReturnType<CalendarApiAdapter["getAvailability"]> = [];
|
||||||
|
if (apires?.data.calendars) {
|
||||||
|
result = Object.values(apires.data.calendars).reduce((c, i) => {
|
||||||
|
i.busy?.forEach((busyTime) => {
|
||||||
|
c.push({
|
||||||
|
start: busyTime.start || "",
|
||||||
|
end: busyTime.end || "",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return c;
|
||||||
|
}, [] as typeof result);
|
||||||
|
}
|
||||||
|
resolve(result);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
@ -146,7 +158,14 @@ export const GoogleCalendarApiAdapter = (credential: Credential): CalendarApiAda
|
||||||
console.error("There was an error contacting google calendar service: ", err);
|
console.error("There was an error contacting google calendar service: ", err);
|
||||||
return reject(err);
|
return reject(err);
|
||||||
}
|
}
|
||||||
return resolve(event.data);
|
return resolve({
|
||||||
|
...event.data,
|
||||||
|
id: event.data.id || "",
|
||||||
|
hangoutLink: event.data.hangoutLink || "",
|
||||||
|
type: "google_calendar",
|
||||||
|
password: "",
|
||||||
|
url: "",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
12
package.json
12
package.json
|
@ -13,8 +13,12 @@
|
||||||
"db-seed": "yarn ts-node scripts/seed.ts",
|
"db-seed": "yarn ts-node scripts/seed.ts",
|
||||||
"db-nuke": "docker-compose down --volumes --remove-orphans",
|
"db-nuke": "docker-compose down --volumes --remove-orphans",
|
||||||
"db-saml-nuke": "docker-compose -p cal-saml -f docker-compose-saml.yml down --volumes --remove-orphans",
|
"db-saml-nuke": "docker-compose -p cal-saml -f docker-compose-saml.yml down --volumes --remove-orphans",
|
||||||
"dx": "cross-env BASE_URL=http://localhost:3000 JWT_SECRET=secret DATABASE_URL=postgresql://postgres:@localhost:5450/calendso run-s db-up db-migrate db-seed dev",
|
"db-setup": "run-s db-up db-migrate db-seed",
|
||||||
"dx-saml": "cross-env BASE_URL=http://localhost:3000 SAML_LOGIN_URL=http://localhost:5000 SAML_API_URL=http://localhost:6000 SAML_ADMINS=onboarding@example.com JWT_SECRET=secret DATABASE_URL=postgresql://postgres:@localhost:5450/calendso run-s db-up db-migrate db-seed dev",
|
"db-reset": "run-s db-nuke db-setup",
|
||||||
|
"db-saml-setup": "run-s db-saml-up db-migrate db-seed",
|
||||||
|
"db-saml-reset": "run-s db-nuke db-saml-setup",
|
||||||
|
"dx": "env-cmd run-s db-setup dev",
|
||||||
|
"dx-saml": "env-cmd run-s db-saml-setup dev",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test-playwright": "jest --config jest.playwright.config.js",
|
"test-playwright": "jest --config jest.playwright.config.js",
|
||||||
"test-codegen": "yarn playwright codegen http://localhost:3000",
|
"test-codegen": "yarn playwright codegen http://localhost:3000",
|
||||||
|
@ -95,8 +99,8 @@
|
||||||
"react-use-intercom": "1.4.0",
|
"react-use-intercom": "1.4.0",
|
||||||
"short-uuid": "^4.2.0",
|
"short-uuid": "^4.2.0",
|
||||||
"stripe": "^8.191.0",
|
"stripe": "^8.191.0",
|
||||||
"tsdav": "^1.1.5",
|
|
||||||
"superjson": "1.8.0",
|
"superjson": "1.8.0",
|
||||||
|
"tsdav": "^1.1.5",
|
||||||
"tslog": "^3.2.1",
|
"tslog": "^3.2.1",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"zod": "^3.8.2"
|
"zod": "^3.8.2"
|
||||||
|
@ -121,7 +125,7 @@
|
||||||
"@typescript-eslint/parser": "^4.33.0",
|
"@typescript-eslint/parser": "^4.33.0",
|
||||||
"autoprefixer": "^10.3.1",
|
"autoprefixer": "^10.3.1",
|
||||||
"babel-jest": "^27.3.1",
|
"babel-jest": "^27.3.1",
|
||||||
"cross-env": "^7.0.3",
|
"env-cmd": "10.1.0",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-prettier": "^3.4.0",
|
"eslint-plugin-prettier": "^3.4.0",
|
||||||
|
|
|
@ -23,7 +23,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (nameCollisions > 0) {
|
if (nameCollisions > 0) {
|
||||||
return res.status(409).json({ errorCode: "TeamNameCollision", message: "Team name already take." });
|
return res
|
||||||
|
.status(409)
|
||||||
|
.json({ errorCode: "TeamNameCollision", message: "Team username already taken." });
|
||||||
}
|
}
|
||||||
|
|
||||||
const createTeam = await prisma.team.create({
|
const createTeam = await prisma.team.create({
|
||||||
|
|
|
@ -13,6 +13,7 @@ import Shell from "@components/Shell";
|
||||||
import EditTeam from "@components/team/EditTeam";
|
import EditTeam from "@components/team/EditTeam";
|
||||||
import TeamList from "@components/team/TeamList";
|
import TeamList from "@components/team/TeamList";
|
||||||
import TeamListItem from "@components/team/TeamListItem";
|
import TeamListItem from "@components/team/TeamListItem";
|
||||||
|
import { Alert } from "@components/ui/Alert";
|
||||||
import Button from "@components/ui/Button";
|
import Button from "@components/ui/Button";
|
||||||
|
|
||||||
export default function Teams() {
|
export default function Teams() {
|
||||||
|
@ -24,6 +25,8 @@ export default function Teams() {
|
||||||
const [showCreateTeamModal, setShowCreateTeamModal] = useState(false);
|
const [showCreateTeamModal, setShowCreateTeamModal] = useState(false);
|
||||||
const [editTeamEnabled, setEditTeamEnabled] = useState(false);
|
const [editTeamEnabled, setEditTeamEnabled] = useState(false);
|
||||||
const [teamToEdit, setTeamToEdit] = useState<Team | null>();
|
const [teamToEdit, setTeamToEdit] = useState<Team | null>();
|
||||||
|
const [hasErrors, setHasErrors] = useState(false);
|
||||||
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
const nameRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
|
const nameRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
|
||||||
|
|
||||||
const handleErrors = async (resp: Response) => {
|
const handleErrors = async (resp: Response) => {
|
||||||
|
@ -48,6 +51,11 @@ export default function Teams() {
|
||||||
loadData();
|
loadData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHasErrors(false);
|
||||||
|
setErrorMessage("");
|
||||||
|
}, [showCreateTeamModal]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <Loader />;
|
return <Loader />;
|
||||||
}
|
}
|
||||||
|
@ -60,10 +68,16 @@ export default function Teams() {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
}).then(() => {
|
})
|
||||||
loadData();
|
.then(handleErrors)
|
||||||
setShowCreateTeamModal(false);
|
.then(() => {
|
||||||
});
|
loadData();
|
||||||
|
setShowCreateTeamModal(false);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setHasErrors(true);
|
||||||
|
setErrorMessage(err.message);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const editTeam = (team: Team) => {
|
const editTeam = (team: Team) => {
|
||||||
|
@ -162,6 +176,7 @@ export default function Teams() {
|
||||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
||||||
{t("name")}
|
{t("name")}
|
||||||
</label>
|
</label>
|
||||||
|
{hasErrors && <Alert className="mt-1 mb-2" severity="error" message={errorMessage} />}
|
||||||
<input
|
<input
|
||||||
ref={nameRef}
|
ref={nameRef}
|
||||||
type="text"
|
type="text"
|
||||||
|
|
|
@ -17,7 +17,6 @@ import Team from "@components/team/screens/Team";
|
||||||
import Avatar from "@components/ui/Avatar";
|
import Avatar from "@components/ui/Avatar";
|
||||||
import AvatarGroup from "@components/ui/AvatarGroup";
|
import AvatarGroup from "@components/ui/AvatarGroup";
|
||||||
import Button from "@components/ui/Button";
|
import Button from "@components/ui/Button";
|
||||||
import Text from "@components/ui/Text";
|
|
||||||
|
|
||||||
function TeamPage({ team }: inferSSRProps<typeof getServerSideProps>) {
|
function TeamPage({ team }: inferSSRProps<typeof getServerSideProps>) {
|
||||||
const { isReady } = useTheme();
|
const { isReady } = useTheme();
|
||||||
|
@ -61,38 +60,43 @@ function TeamPage({ team }: inferSSRProps<typeof getServerSideProps>) {
|
||||||
isReady && (
|
isReady && (
|
||||||
<div>
|
<div>
|
||||||
<HeadSeo title={teamName} description={teamName} />
|
<HeadSeo title={teamName} description={teamName} />
|
||||||
<div className="pt-24 pb-12 px-4">
|
<div className="h-screen bg-neutral-50 dark:bg-black">
|
||||||
<div className="mb-8 text-center">
|
<main className="max-w-3xl px-4 py-24 mx-auto">
|
||||||
<Avatar alt={teamName} imageSrc={team.logo} className="mx-auto w-20 h-20 rounded-full mb-4" />
|
<div className="mb-8 text-center">
|
||||||
<Text variant="headline">{teamName}</Text>
|
<Avatar alt={teamName} imageSrc={team.logo} className="mx-auto w-20 h-20 rounded-full mb-4" />
|
||||||
</div>
|
<h1 className="mb-1 text-3xl font-bold font-cal text-neutral-900 dark:text-white">
|
||||||
{(showMembers.isOn || !team.eventTypes.length) && <Team team={team} />}
|
{teamName}
|
||||||
{!showMembers.isOn && team.eventTypes.length > 0 && (
|
</h1>
|
||||||
<div className="mx-auto max-w-3xl">
|
<p className="text-neutral-500 dark:text-white">{team.bio}</p>
|
||||||
{eventTypes}
|
|
||||||
|
|
||||||
<div className="relative mt-12">
|
|
||||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
|
||||||
<div className="w-full border-t border-gray-200 dark:border-gray-900" />
|
|
||||||
</div>
|
|
||||||
<div className="relative flex justify-center">
|
|
||||||
<span className="px-2 bg-gray-100 text-sm text-gray-500 dark:bg-brand dark:text-gray-500">
|
|
||||||
{t("or")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<aside className="text-center dark:text-white mt-8">
|
|
||||||
<Button
|
|
||||||
color="secondary"
|
|
||||||
EndIcon={ArrowRightIcon}
|
|
||||||
href={`/team/${team.slug}?members=1`}
|
|
||||||
shallow={true}>
|
|
||||||
{t("book_a_team_member")}
|
|
||||||
</Button>
|
|
||||||
</aside>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
{(showMembers.isOn || !team.eventTypes.length) && <Team team={team} />}
|
||||||
|
{!showMembers.isOn && team.eventTypes.length > 0 && (
|
||||||
|
<div className="mx-auto max-w-3xl">
|
||||||
|
{eventTypes}
|
||||||
|
|
||||||
|
<div className="relative mt-12">
|
||||||
|
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||||
|
<div className="w-full border-t border-gray-200 dark:border-gray-900" />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center">
|
||||||
|
<span className="px-2 bg-gray-100 text-sm text-gray-500 dark:bg-brand dark:text-gray-500">
|
||||||
|
{t("or")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside className="text-center dark:text-white mt-8">
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
EndIcon={ArrowRightIcon}
|
||||||
|
href={`/team/${team.slug}?members=1`}
|
||||||
|
shallow={true}>
|
||||||
|
{t("book_a_team_member")}
|
||||||
|
</Button>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -116,6 +120,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
name: true,
|
name: true,
|
||||||
slug: true,
|
slug: true,
|
||||||
logo: true,
|
logo: true,
|
||||||
|
bio: true,
|
||||||
members: {
|
members: {
|
||||||
select: {
|
select: {
|
||||||
user: {
|
user: {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { kont } from "kont";
|
import { kont } from "kont";
|
||||||
|
|
||||||
import { loginProvider } from "./lib/loginProvider";
|
import { loginProvider } from "./lib/loginProvider";
|
||||||
|
@ -35,14 +34,11 @@ describe("webhooks", () => {
|
||||||
// page contains the url
|
// page contains the url
|
||||||
await expect(page).toHaveSelector(`text='${webhookReceiver.url}'`);
|
await expect(page).toHaveSelector(`text='${webhookReceiver.url}'`);
|
||||||
|
|
||||||
// --- go to tomorrow in the pro user's "30min"-event
|
// --- Book the first available day next month in the pro user's "30min"-event
|
||||||
const tomorrow = dayjs().add(1, "day");
|
await page.goto(`http://localhost:3000/pro/30min`);
|
||||||
const tomorrowFormatted = tomorrow.format("YYYY-MM-DDZZ");
|
await page.click('[data-testid="incrementMonth"]');
|
||||||
|
await page.click('[data-testid="day"]');
|
||||||
await page.goto(`http://localhost:3000/pro/30min?date=${encodeURIComponent(tomorrowFormatted)}`);
|
await page.click('[data-testid="time"]');
|
||||||
|
|
||||||
// click first time available
|
|
||||||
await page.click("[data-testid=time]");
|
|
||||||
|
|
||||||
// --- fill form
|
// --- fill form
|
||||||
await page.fill('[name="name"]', "Test Testson");
|
await page.fill('[name="name"]', "Test Testson");
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
|
"@types/*.d.ts",
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
"**/*.tsx"
|
"**/*.tsx"
|
||||||
],
|
],
|
||||||
|
|
22
yarn.lock
22
yarn.lock
|
@ -3173,6 +3173,11 @@ commander@^3.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e"
|
||||||
integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==
|
integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==
|
||||||
|
|
||||||
|
commander@^4.0.0:
|
||||||
|
version "4.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
|
||||||
|
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
|
||||||
|
|
||||||
commander@^5.1.0:
|
commander@^5.1.0:
|
||||||
version "5.1.0"
|
version "5.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
|
||||||
|
@ -3294,13 +3299,6 @@ create-require@^1.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
||||||
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
|
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
|
||||||
|
|
||||||
cross-env@^7.0.3:
|
|
||||||
version "7.0.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
|
|
||||||
integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==
|
|
||||||
dependencies:
|
|
||||||
cross-spawn "^7.0.1"
|
|
||||||
|
|
||||||
cross-fetch@3.1.4:
|
cross-fetch@3.1.4:
|
||||||
version "3.1.4"
|
version "3.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.4.tgz#9723f3a3a247bf8b89039f3a380a9244e8fa2f39"
|
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.4.tgz#9723f3a3a247bf8b89039f3a380a9244e8fa2f39"
|
||||||
|
@ -3319,7 +3317,7 @@ cross-spawn@^6.0.5:
|
||||||
shebang-command "^1.2.0"
|
shebang-command "^1.2.0"
|
||||||
which "^1.2.9"
|
which "^1.2.9"
|
||||||
|
|
||||||
cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||||
version "7.0.3"
|
version "7.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
||||||
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
|
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
|
||||||
|
@ -3728,6 +3726,14 @@ enquirer@^2.3.5, enquirer@^2.3.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-colors "^4.1.1"
|
ansi-colors "^4.1.1"
|
||||||
|
|
||||||
|
env-cmd@10.1.0:
|
||||||
|
version "10.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/env-cmd/-/env-cmd-10.1.0.tgz#c7f5d3b550c9519f137fdac4dd8fb6866a8c8c4b"
|
||||||
|
integrity sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA==
|
||||||
|
dependencies:
|
||||||
|
commander "^4.0.0"
|
||||||
|
cross-spawn "^7.0.0"
|
||||||
|
|
||||||
errno@^0.1.1, errno@~0.1.1:
|
errno@^0.1.1, errno@~0.1.1:
|
||||||
version "0.1.8"
|
version "0.1.8"
|
||||||
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f"
|
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f"
|
||||||
|
|
Loading…
Reference in New Issue