Compare commits
26
Commits
v3.7.16
...
v3.8.0-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bd0428d18 | ||
|
|
c7089b9db6 | ||
|
|
81ad00a816 | ||
|
|
b5730da56d | ||
|
|
9455d14ad5 | ||
|
|
a056160404 | ||
|
|
6d53cd84f8 | ||
|
|
30cb8f745d | ||
|
|
bafa3c97ac | ||
|
|
bb48ccbad9 | ||
|
|
0a68c6d048 | ||
|
|
bb9f8e5d38 | ||
|
|
c99bb190f1 | ||
|
|
f0d2a45fdf | ||
|
|
e5f9315ee6 | ||
|
|
f7782b2080 | ||
|
|
e1f2dbab84 | ||
|
|
c2edbbcea7 | ||
|
|
c7e78cc181 | ||
|
|
7ba1557d1d | ||
|
|
d5b45b757a | ||
|
|
bb3b262bd7 | ||
|
|
d4f41e803a | ||
|
|
74eafa59e1 | ||
|
|
e6cd016cad | ||
|
|
7b0d5959cb |
@@ -12,7 +12,7 @@
|
||||
"mailparser": "^3.6.5",
|
||||
"next": "^13.5.4",
|
||||
"supports-color": "8.1.1",
|
||||
"zod": "^3.22.2"
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mailparser": "^3.4.0"
|
||||
|
||||
@@ -41,6 +41,6 @@
|
||||
"typescript": "^4.9.4",
|
||||
"tzdata": "^1.0.30",
|
||||
"uuid": "^8.3.2",
|
||||
"zod": "^3.22.2"
|
||||
"zod": "^3.22.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
|
||||
export default function getCalLink(eventType: {
|
||||
team?: { slug: string | null } | null;
|
||||
@@ -6,7 +6,7 @@ export default function getCalLink(eventType: {
|
||||
users?: { username: string | null }[];
|
||||
slug: string;
|
||||
}) {
|
||||
return `${CAL_URL}/${
|
||||
return `${WEBSITE_URL}/${
|
||||
eventType?.team
|
||||
? `team/${eventType?.team?.slug}`
|
||||
: eventType?.owner
|
||||
|
||||
@@ -18,7 +18,8 @@ export const generateMetadata = async ({
|
||||
}) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const props = await getData(legacyCtx as unknown as GetServerSidePropsContext);
|
||||
const { entity, user, slug, booking } = props;
|
||||
const { eventData, user, slug, booking } = props;
|
||||
const entity = eventData.entity;
|
||||
const { trpc } = await import("@calcom/trpc");
|
||||
const { data: event } = trpc.viewer.public.event.useQuery(
|
||||
{ username: user, eventSlug: slug, isTeamEvent: false, org: entity.orgSlug ?? null },
|
||||
|
||||
@@ -16,7 +16,8 @@ export const generateMetadata = async ({
|
||||
}) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const props = await getData(legacyCtx as unknown as GetServerSidePropsContext);
|
||||
const { entity, user, slug, booking } = props;
|
||||
const { eventData, user, slug, booking } = props;
|
||||
const entity = eventData.entity;
|
||||
const { trpc } = await import("@calcom/trpc");
|
||||
const { data: event } = trpc.viewer.public.event.useQuery(
|
||||
{ username: user, eventSlug: slug, isTeamEvent: false, org: entity.orgSlug ?? null },
|
||||
|
||||
@@ -2,7 +2,7 @@ import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||
import { doesAppSupportTeamInstall } from "@calcom/app-store/utils";
|
||||
import { Spinner } from "@calcom/features/calendars/weeklyview/components/spinner/Spinner";
|
||||
import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import type { AppFrontendPayload } from "@calcom/types/App";
|
||||
@@ -125,7 +125,7 @@ export const InstallAppButtonChild = ({
|
||||
StartIcon={(props) => (
|
||||
<Avatar
|
||||
alt={team.logo || ""}
|
||||
imageSrc={team.logo || `${CAL_URL}/${team.logo}/avatar.png`} // if no image, use default avatar
|
||||
imageSrc={team.logo || `${WEBAPP_URL}/${team.logo}/avatar.png`} // if no image, use default avatar
|
||||
size="sm"
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -18,8 +18,7 @@ import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
|
||||
import type { EditableSchema } from "@calcom/features/form-builder/schema";
|
||||
import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
||||
import { IS_VISUAL_REGRESSION_TESTING } from "@calcom/lib/constants";
|
||||
import { APP_NAME, IS_VISUAL_REGRESSION_TESTING, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { generateHashedLink } from "@calcom/lib/generateHashedLink";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
@@ -31,6 +30,7 @@ import {
|
||||
Label,
|
||||
SettingsToggle,
|
||||
showToast,
|
||||
Switch,
|
||||
TextField,
|
||||
Tooltip,
|
||||
} from "@calcom/ui";
|
||||
@@ -45,10 +45,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
const { data: user } = trpc.viewer.me.useQuery();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const { t } = useLocale();
|
||||
|
||||
const [showEventNameTip, setShowEventNameTip] = useState(false);
|
||||
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!formMethods.getValues("hashedLink"));
|
||||
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!formMethods.getValues("successRedirectUrl"));
|
||||
const [useEventTypeDestinationCalendarEmail, setUseEventTypeDestinationCalendarEmail] = useState(
|
||||
formMethods.getValues("useEventTypeDestinationCalendarEmail")
|
||||
);
|
||||
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
|
||||
|
||||
const bookingFields: Prisma.JsonObject = {};
|
||||
@@ -74,7 +76,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
const [requiresConfirmation, setRequiresConfirmation] = useState(
|
||||
formMethods.getValues("requiresConfirmation")
|
||||
);
|
||||
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${formMethods.getValues("slug")}`;
|
||||
const placeholderHashedLink = `${WEBSITE_URL}/d/${hashedUrl}/${formMethods.getValues("slug")}`;
|
||||
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
||||
const noShowFeeEnabled =
|
||||
formMethods.getValues("metadata")?.apps?.stripe?.enabled === true &&
|
||||
@@ -117,6 +119,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
const seatsLocked = shouldLockDisableProps("seatsPerTimeSlotEnabled");
|
||||
|
||||
const closeEventNameTip = () => setShowEventNameTip(false);
|
||||
const displayDestinationCalendarSelector =
|
||||
!!connectedCalendarsQuery.data?.connectedCalendars.length && !team;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-4">
|
||||
@@ -126,7 +130,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
* This will fallback to each user selected destination calendar.
|
||||
*/}
|
||||
<div className="border-subtle space-y-6 rounded-lg border p-6">
|
||||
{!!connectedCalendarsQuery.data?.connectedCalendars.length && !team && (
|
||||
{displayDestinationCalendarSelector && (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
@@ -172,6 +176,18 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{displayDestinationCalendarSelector && (
|
||||
<div className="w-full">
|
||||
<Switch
|
||||
label={t("display_add_to_calendar_organizer")}
|
||||
checked={useEventTypeDestinationCalendarEmail}
|
||||
onCheckedChange={(val) => {
|
||||
setUseEventTypeDestinationCalendarEmail(val);
|
||||
formMethods.setValue("useEventTypeDestinationCalendarEmail", val);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<BookerLayoutSelector fallbackToUserSettings isDark={selectedThemeIsDark} isOuterBorder={true} />
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { EventLocationType } from "@calcom/app-store/locations";
|
||||
import { getEventLocationType, MeetLocationType } from "@calcom/app-store/locations";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
import { slugify } from "@calcom/lib/slugify";
|
||||
@@ -401,7 +401,7 @@ export const EventSetupTab = (
|
||||
The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work.
|
||||
Change it{" "}
|
||||
<Link
|
||||
href={`${CAL_URL}/event-types/${formMethods.getValues("id")}?tabName=advanced`}
|
||||
href={`${WEBAPP_URL}/event-types/${formMethods.getValues("id")}?tabName=advanced`}
|
||||
className="underline">
|
||||
here.
|
||||
</Link>{" "}
|
||||
@@ -412,7 +412,7 @@ export const EventSetupTab = (
|
||||
{isChildrenManagedEventType && !locationAvailable && locationDetails && (
|
||||
<p className="pl-1 text-sm leading-none text-red-600">
|
||||
{t("app_not_connected", { appName: locationDetails.name })}{" "}
|
||||
<a className="underline" href={`${CAL_URL}/apps/${locationDetails.slug}`}>
|
||||
<a className="underline" href={`${WEBAPP_URL}/apps/${locationDetails.slug}`}>
|
||||
{t("connect_now")}
|
||||
</a>
|
||||
</p>
|
||||
@@ -444,9 +444,10 @@ export const EventSetupTab = (
|
||||
|
||||
const lengthLockedProps = shouldLockDisableProps("length");
|
||||
const descriptionLockedProps = shouldLockDisableProps("description");
|
||||
|
||||
const urlPrefix = orgBranding
|
||||
? orgBranding?.fullDomain.replace(/^(https?:|)\/\//, "")
|
||||
: `${CAL_URL?.replace(/^(https?:|)\/\//, "")}`;
|
||||
: `${WEBSITE_URL?.replace(/^(https?:|)\/\//, "")}`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import { CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||
import { WEBSITE_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
@@ -45,7 +45,7 @@ export const UsernameAvailabilityField = ({
|
||||
|
||||
const usernamePrefix = orgBranding
|
||||
? orgBranding?.fullDomain.replace(/^(https?:|)\/\//, "")
|
||||
: `${CAL_URL?.replace(/^(https?:|)\/\//, "")}`;
|
||||
: `${WEBSITE_URL?.replace(/^(https?:|)\/\//, "")}`;
|
||||
|
||||
return (
|
||||
<Controller
|
||||
|
||||
@@ -11,10 +11,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
if (!session) {
|
||||
return {
|
||||
redirect: {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/auth/login",
|
||||
},
|
||||
permanent: false,
|
||||
destination: "/auth/login",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { GetServerSidePropsContext } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { getBookingForReschedule, getMultipleDurationValue } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { getBookingForReschedule } from "@calcom/features/bookings/lib/get-booking";
|
||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
@@ -82,12 +82,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
|
||||
return {
|
||||
props: {
|
||||
entity: eventData.entity,
|
||||
duration: getMultipleDurationValue(
|
||||
eventData.metadata?.multipleDuration,
|
||||
queryDuration,
|
||||
eventData.length
|
||||
),
|
||||
eventData: {
|
||||
entity: eventData.entity,
|
||||
length: eventData.length,
|
||||
metadata: eventData.metadata,
|
||||
},
|
||||
booking,
|
||||
away: false,
|
||||
user: teamSlug,
|
||||
|
||||
@@ -82,6 +82,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
cancellationReason: true,
|
||||
responses: true,
|
||||
rejectionReason: true,
|
||||
userPrimaryEmail: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@@ -203,7 +203,7 @@ export default function Success(props: PageProps) {
|
||||
duration: calculatedDuration,
|
||||
organizer: {
|
||||
name: users[0].name || "Nameless",
|
||||
email: users[0].email || "Email-less",
|
||||
email: bookingInfo?.userPrimaryEmail || users[0].email || "Email-less",
|
||||
timeZone: users[0].timeZone,
|
||||
},
|
||||
confirmed: !needsConfirmation,
|
||||
@@ -450,7 +450,9 @@ export default function Success(props: PageProps) {
|
||||
</span>
|
||||
<Badge variant="blue">{t("Host")}</Badge>
|
||||
</div>
|
||||
<p className="text-default">{bookingInfo.user.email}</p>
|
||||
<p className="text-default">
|
||||
{bookingInfo?.userPrimaryEmail ?? bookingInfo.user.email}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{bookingInfo?.attendees.map((attendee) => (
|
||||
|
||||
@@ -18,7 +18,7 @@ import { TeamsFilter } from "@calcom/features/filters/components/TeamsFilter";
|
||||
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||
@@ -407,7 +407,7 @@ export const EventTypeList = ({
|
||||
const embedLink = `${group.profile.slug}/${type.slug}`;
|
||||
const calLink = `${bookerUrl}/${embedLink}`;
|
||||
const isPrivateURLEnabled = type.hashedLink?.link;
|
||||
const placeholderHashedLink = `${CAL_URL}/d/${type.hashedLink?.link}/${type.slug}`;
|
||||
const placeholderHashedLink = `${WEBSITE_URL}/d/${type.hashedLink?.link}/${type.slug}`;
|
||||
const isManagedEventType = type.schedulingType === SchedulingType.MANAGED;
|
||||
const isChildrenManagedEventType =
|
||||
type.metadata?.managedEventConfig !== undefined && type.schedulingType !== SchedulingType.MANAGED;
|
||||
|
||||
@@ -15,7 +15,7 @@ import { validateCustomEventName } from "@calcom/core/event";
|
||||
import type { EventLocationType } from "@calcom/core/location";
|
||||
import type { ChildrenEventType } from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
|
||||
import { validateIntervalLimitOrder } from "@calcom/lib";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
@@ -148,6 +148,7 @@ export type FormValues = {
|
||||
multipleDurationEnabled: boolean;
|
||||
users: EventTypeSetup["users"];
|
||||
assignAllTeamMembers: boolean;
|
||||
useEventTypeDestinationCalendarEmail: boolean;
|
||||
};
|
||||
|
||||
export type CustomInputParsed = typeof customInputSchema._output;
|
||||
@@ -298,6 +299,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
hosts: eventType.hosts,
|
||||
successRedirectUrl: eventType.successRedirectUrl || "",
|
||||
users: eventType.users,
|
||||
useEventTypeDestinationCalendarEmail: eventType.useEventTypeDestinationCalendarEmail,
|
||||
children: eventType.children.map((ch) => ({
|
||||
...ch,
|
||||
created: true,
|
||||
@@ -423,7 +425,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
).length;
|
||||
}
|
||||
|
||||
const permalink = `${CAL_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
|
||||
const permalink = `${WEBSITE_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
|
||||
eventType.slug
|
||||
}`;
|
||||
const tabMap = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.7.16",
|
||||
"version": "3.8.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -130,7 +130,7 @@
|
||||
"tailwindcss-radix": "^2.6.0",
|
||||
"turndown": "^7.1.1",
|
||||
"uuid": "^8.3.2",
|
||||
"zod": "^3.22.2"
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.19.6",
|
||||
|
||||
@@ -38,7 +38,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const oldPassword = req.body.oldPassword;
|
||||
const newPassword = req.body.newPassword;
|
||||
|
||||
const currentPassword = user.password;
|
||||
const currentPassword = user.password?.hash;
|
||||
if (!currentPassword) {
|
||||
return res.status(400).json({ error: ErrorCode.UserMissingPassword });
|
||||
}
|
||||
@@ -53,12 +53,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(newPassword);
|
||||
await prisma.user.update({
|
||||
await prisma.userPassword.upsert({
|
||||
where: {
|
||||
id: user.id,
|
||||
userId: user.id,
|
||||
},
|
||||
data: {
|
||||
password: hashedPassword,
|
||||
create: {
|
||||
hash: hashedPassword,
|
||||
userId: user.id,
|
||||
},
|
||||
update: {
|
||||
hash: hashedPassword,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -41,7 +41,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
email: maybeRequest.email,
|
||||
},
|
||||
data: {
|
||||
password: hashedPassword,
|
||||
password: {
|
||||
upsert: {
|
||||
create: { hash: hashedPassword },
|
||||
update: { hash: hashedPassword },
|
||||
},
|
||||
},
|
||||
emailVerified: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@ async function handler(req: NextApiRequest) {
|
||||
data: {
|
||||
username,
|
||||
email: userEmail,
|
||||
password: hashedPassword,
|
||||
password: { create: { hash: hashedPassword } },
|
||||
role: "ADMIN",
|
||||
name: parsedQuery.data.full_name,
|
||||
emailVerified: new Date(),
|
||||
|
||||
@@ -23,13 +23,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(500).json({ error: ErrorCode.InternalServerError });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id }, include: { password: true } });
|
||||
if (!user) {
|
||||
console.error(`Session references user that no longer exists.`);
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
|
||||
if (!user.password && user.identityProvider === IdentityProvider.CAL) {
|
||||
if (!user.password?.hash && user.identityProvider === IdentityProvider.CAL) {
|
||||
return res.status(400).json({ error: ErrorCode.UserMissingPassword });
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.json({ message: "Two factor disabled" });
|
||||
}
|
||||
|
||||
if (user.password && user.identityProvider === IdentityProvider.CAL) {
|
||||
const isCorrectPassword = await verifyPassword(req.body.password, user.password);
|
||||
if (user.password?.hash && user.identityProvider === IdentityProvider.CAL) {
|
||||
const isCorrectPassword = await verifyPassword(req.body.password, user.password.hash);
|
||||
if (!isCorrectPassword) {
|
||||
return res.status(400).json({ error: ErrorCode.IncorrectPassword });
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(500).json({ error: ErrorCode.InternalServerError });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id }, include: { password: true } });
|
||||
if (!user) {
|
||||
console.error(`Session references user that no longer exists.`);
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
@@ -35,7 +35,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(400).json({ error: ErrorCode.ThirdPartyIdentityProviderEnabled });
|
||||
}
|
||||
|
||||
if (!user.password) {
|
||||
if (!user.password?.hash) {
|
||||
return res.status(400).json({ error: ErrorCode.UserMissingPassword });
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(500).json({ error: ErrorCode.InternalServerError });
|
||||
}
|
||||
|
||||
const isCorrectPassword = await verifyPassword(req.body.password, user.password);
|
||||
const isCorrectPassword = await verifyPassword(req.body.password, user.password.hash);
|
||||
if (!isCorrectPassword) {
|
||||
return res.status(400).json({ error: ErrorCode.IncorrectPassword });
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
endTime: booking.endTime.toISOString(),
|
||||
organizer: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
email: booking?.userPrimaryEmail ?? user.email,
|
||||
name,
|
||||
timeZone: user.timeZone,
|
||||
language: { translate: tOrganizer, locale: user.locale ?? "en" },
|
||||
|
||||
@@ -230,7 +230,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
startTime: booking.startTime.toISOString(),
|
||||
endTime: booking.endTime.toISOString(),
|
||||
organizer: {
|
||||
email: booking.user?.email || "Email-less",
|
||||
email: booking?.userPrimaryEmail || booking.user?.email || "Email-less",
|
||||
name: booking.user?.name || "Nameless",
|
||||
timeZone: booking.user?.timeZone || "Europe/London",
|
||||
language: { translate: t, locale: booking?.user?.locale ?? "en" },
|
||||
|
||||
@@ -47,7 +47,7 @@ export default function Apps({ apps }: PageProps) {
|
||||
<div className="mb-16">
|
||||
<div className="grid-col-1 grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
{apps
|
||||
.sort((a, b) => (b.installCount || 0) - (a.installCount || 0))
|
||||
?.sort((a, b) => (b.installCount || 0) - (a.installCount || 0))
|
||||
.map((app) => {
|
||||
return <AppCard key={app.slug} app={app} />;
|
||||
})}
|
||||
|
||||
@@ -139,6 +139,9 @@ const AppearanceView = ({
|
||||
showToast(t("error_updating_settings"), "error");
|
||||
}
|
||||
},
|
||||
onSettled: async () => {
|
||||
await utils.viewer.me.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -184,6 +187,7 @@ const AppearanceView = ({
|
||||
</div>
|
||||
<SectionBottomActions className="mb-6" align="end">
|
||||
<Button
|
||||
loading={mutation.isPending}
|
||||
disabled={isUserThemeSubmitting || !isUserThemeDirty}
|
||||
type="submit"
|
||||
data-testid="update-theme-btn"
|
||||
@@ -210,6 +214,7 @@ const AppearanceView = ({
|
||||
title={t("bookerlayout_user_settings_title")}
|
||||
description={t("bookerlayout_user_settings_description")}
|
||||
isDisabled={isBookerLayoutFormSubmitting || !isBookerLayoutFormDirty}
|
||||
isLoading={mutation.isPending}
|
||||
/>
|
||||
</Form>
|
||||
|
||||
@@ -295,6 +300,7 @@ const AppearanceView = ({
|
||||
</div>
|
||||
<SectionBottomActions align="end">
|
||||
<Button
|
||||
loading={mutation.isPending}
|
||||
disabled={isBrandColorsFormSubmitting || !isBrandColorsFormDirty}
|
||||
color="primary"
|
||||
type="submit">
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
import { Booker } from "@calcom/atoms";
|
||||
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
||||
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||
@@ -14,6 +16,15 @@ export type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
|
||||
|
||||
export { getServerSideProps };
|
||||
|
||||
export const getMultipleDurationValue = (
|
||||
multipleDurationConfig: number[] | undefined,
|
||||
queryDuration: string | string[] | null | undefined,
|
||||
defaultValue: number
|
||||
) => {
|
||||
if (!multipleDurationConfig) return null;
|
||||
if (multipleDurationConfig.includes(Number(queryDuration))) return Number(queryDuration);
|
||||
return defaultValue;
|
||||
};
|
||||
export default function Type({
|
||||
slug,
|
||||
user,
|
||||
@@ -21,10 +32,11 @@ export default function Type({
|
||||
away,
|
||||
isEmbed,
|
||||
isBrandingHidden,
|
||||
entity,
|
||||
duration,
|
||||
eventData,
|
||||
isInstantMeeting,
|
||||
}: PageProps) {
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
return (
|
||||
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
|
||||
<BookerSeo
|
||||
@@ -33,7 +45,7 @@ export default function Type({
|
||||
rescheduleUid={booking?.uid}
|
||||
hideBranding={isBrandingHidden}
|
||||
isTeamEvent
|
||||
entity={entity}
|
||||
entity={eventData.entity}
|
||||
bookingData={booking}
|
||||
/>
|
||||
<Booker
|
||||
@@ -44,8 +56,16 @@ export default function Type({
|
||||
isInstantMeeting={isInstantMeeting}
|
||||
hideBranding={isBrandingHidden}
|
||||
isTeamEvent
|
||||
entity={entity}
|
||||
duration={duration}
|
||||
entity={eventData.entity}
|
||||
durationConfig={eventData.metadata?.multipleDuration}
|
||||
/* TODO: Currently unused, evaluate it is needed-
|
||||
* Possible alternative approach is to have onDurationChange.
|
||||
*/
|
||||
duration={getMultipleDurationValue(
|
||||
eventData.metadata?.multipleDuration,
|
||||
searchParams?.get("duration"),
|
||||
eventData.length
|
||||
)}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -86,7 +86,8 @@ testBothFutureAndLegacyRoutes.describe("Forgot password", async () => {
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
expect(await verifyPassword(newPassword, updatedUser.password!)).toBeTruthy();
|
||||
const updatedPassword = updatedUser.password!.hash;
|
||||
expect(await verifyPassword(newPassword, updatedPassword)).toBeTruthy();
|
||||
|
||||
// finally, make sure the same URL cannot be used to reset the password again, as it should be expired.
|
||||
await page.goto(`/auth/forgot-password/${id}`);
|
||||
|
||||
@@ -652,7 +652,6 @@ type SupportedTestWorkflows = PrismaType.WorkflowCreateInput;
|
||||
|
||||
type CustomUserOptsKeys =
|
||||
| "username"
|
||||
| "password"
|
||||
| "completedOnboarding"
|
||||
| "locale"
|
||||
| "name"
|
||||
@@ -669,6 +668,7 @@ type CustomUserOpts = Partial<Pick<Prisma.User, CustomUserOptsKeys>> & {
|
||||
useExactUsername?: boolean;
|
||||
roleInOrganization?: MembershipRole;
|
||||
schedule?: Schedule;
|
||||
password?: string | null;
|
||||
};
|
||||
|
||||
// creates the actual user in the db.
|
||||
@@ -690,7 +690,11 @@ const createUser = (
|
||||
username: uname,
|
||||
name: opts?.name,
|
||||
email: opts?.email ?? `${uname}@example.com`,
|
||||
password: hashPassword(uname),
|
||||
password: {
|
||||
create: {
|
||||
hash: hashPassword(uname),
|
||||
},
|
||||
},
|
||||
emailVerified: new Date(),
|
||||
completedOnboarding: opts?.completedOnboarding ?? true,
|
||||
timeZone: opts?.timeZone ?? TimeZoneEnum.UK,
|
||||
@@ -791,7 +795,7 @@ async function confirmPendingPayment(page: Page) {
|
||||
|
||||
// login using a replay of an E2E routine.
|
||||
export async function login(
|
||||
user: Pick<Prisma.User, "username"> & Partial<Pick<Prisma.User, "password" | "email">>,
|
||||
user: Pick<Prisma.User, "username"> & Partial<Pick<Prisma.User, "email">> & { password?: string | null },
|
||||
page: Page
|
||||
) {
|
||||
// get locators
|
||||
@@ -812,7 +816,7 @@ export async function login(
|
||||
}
|
||||
|
||||
export async function apiLogin(
|
||||
user: Pick<Prisma.User, "username"> & Partial<Pick<Prisma.User, "password" | "email">>,
|
||||
user: Pick<Prisma.User, "username"> & Partial<Pick<Prisma.User, "email">> & { password: string | null },
|
||||
page: Page
|
||||
) {
|
||||
const csrfToken = await page
|
||||
|
||||
@@ -807,6 +807,7 @@
|
||||
"additional_input_description": "Require scheduler to input additional inputs prior the booking is confirmed",
|
||||
"label": "Label",
|
||||
"placeholder": "Placeholder",
|
||||
"display_add_to_calendar_organizer":"Display 'Add to calendar' email as the organizer",
|
||||
"type": "Type",
|
||||
"edit": "Edit",
|
||||
"add_input": "Add an Input",
|
||||
@@ -2194,7 +2195,7 @@
|
||||
"create_your_org_description": "Upgrade to Enterprise and receive a subdomain, unified billing, Insights, extensive whitelabeling and more",
|
||||
"other_payment_app_enabled": "You can only enable one payment app per event type",
|
||||
"admin_delete_organization_description": "<ul><li>Teams that are member of this organization will also be deleted along with their event-types</li><li>Users that were part of the organization will not be deleted and their event-types will also remain intact.</li><li>Usernames would be changed to allow them to exist outside the organization</li></ul>",
|
||||
"admin_delete_organization_title": "Delete organization?",
|
||||
"admin_delete_organization_title": "Delete {{organizationName}}?",
|
||||
"published": "Published",
|
||||
"unpublished": "Unpublished",
|
||||
"publish": "Publish",
|
||||
|
||||
@@ -108,6 +108,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
timeZone,
|
||||
requiresBookerEmailVerification,
|
||||
lockTimeZoneToggleOnBookingPage,
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
id: 123,
|
||||
@@ -156,6 +157,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
scheduleId,
|
||||
requiresBookerEmailVerification,
|
||||
lockTimeZoneToggleOnBookingPage,
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
@@ -250,6 +252,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
timeZone,
|
||||
requiresBookerEmailVerification,
|
||||
lockTimeZoneToggleOnBookingPage,
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
id: 123,
|
||||
@@ -298,6 +301,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
userId,
|
||||
requiresBookerEmailVerification,
|
||||
lockTimeZoneToggleOnBookingPage,
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
@@ -350,6 +354,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
requiresBookerEmailVerification,
|
||||
lockTimeZoneToggleOnBookingPage,
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
|
||||
@@ -8,7 +8,7 @@ import { expect, vi } from "vitest";
|
||||
import "vitest-fetch-mock";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
@@ -307,16 +307,18 @@ export function expectWebhookToHaveBeenCalledWith(
|
||||
export function expectWorkflowToBeTriggered({
|
||||
emails,
|
||||
organizer,
|
||||
destinationEmail,
|
||||
}: {
|
||||
emails: Fixtures["emails"];
|
||||
organizer: { email: string; name: string; timeZone: string };
|
||||
destinationEmail?: string;
|
||||
}) {
|
||||
const subjectPattern = /^Reminder: /i;
|
||||
expect(emails.get()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
subject: expect.stringMatching(subjectPattern),
|
||||
to: organizer.email,
|
||||
to: destinationEmail ?? organizer.email,
|
||||
}),
|
||||
])
|
||||
);
|
||||
@@ -370,6 +372,7 @@ export function expectSuccessfulBookingCreationEmails({
|
||||
recurrence,
|
||||
bookingTimeRange,
|
||||
booking,
|
||||
destinationEmail,
|
||||
}: {
|
||||
emails: Fixtures["emails"];
|
||||
organizer: { email: string; name: string; timeZone: string };
|
||||
@@ -381,8 +384,9 @@ export function expectSuccessfulBookingCreationEmails({
|
||||
eventDomain?: string;
|
||||
bookingTimeRange?: { start: Date; end: Date };
|
||||
booking: { uid: string; urlOrigin?: string };
|
||||
destinationEmail?: string;
|
||||
}) {
|
||||
const bookingUrlOrigin = booking.urlOrigin || CAL_URL;
|
||||
const bookingUrlOrigin = booking.urlOrigin || WEBSITE_URL;
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
titleTag: "confirmed_event_type_subject",
|
||||
@@ -409,7 +413,7 @@ export function expectSuccessfulBookingCreationEmails({
|
||||
},
|
||||
}
|
||||
: null),
|
||||
to: `${organizer.email}`,
|
||||
to: `${destinationEmail ?? organizer.email}`,
|
||||
ics: {
|
||||
filename: "event.ics",
|
||||
iCalUID: `${iCalUID}`,
|
||||
@@ -417,7 +421,7 @@ export function expectSuccessfulBookingCreationEmails({
|
||||
method: "REQUEST",
|
||||
},
|
||||
},
|
||||
`${organizer.email}`
|
||||
`${destinationEmail ?? organizer.email}`
|
||||
);
|
||||
|
||||
expect(emails).toHaveEmail(
|
||||
@@ -742,7 +746,7 @@ export function expectBookingRequestRescheduledEmails({
|
||||
booking: { uid: string; urlOrigin?: string };
|
||||
bookNewTimePath: string;
|
||||
}) {
|
||||
const bookingUrlOrigin = booking.urlOrigin || CAL_URL;
|
||||
const bookingUrlOrigin = booking.urlOrigin || WEBSITE_URL;
|
||||
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
|
||||
@@ -58,7 +58,6 @@ import sendgrid_config_json from "./sendgrid/config.json";
|
||||
import shimmervideo_config_json from "./shimmervideo/config.json";
|
||||
import signal_config_json from "./signal/config.json";
|
||||
import sirius_video_config_json from "./sirius_video/config.json";
|
||||
import skiff_config_json from "./skiff/config.json";
|
||||
import { metadata as stripepayment__metadata_ts } from "./stripepayment/_metadata";
|
||||
import sylapsvideo_config_json from "./sylapsvideo/config.json";
|
||||
import { metadata as tandemvideo__metadata_ts } from "./tandemvideo/_metadata";
|
||||
@@ -141,7 +140,6 @@ export const appStoreMetadata = {
|
||||
shimmervideo: shimmervideo_config_json,
|
||||
signal: signal_config_json,
|
||||
sirius_video: sirius_video_config_json,
|
||||
skiff: skiff_config_json,
|
||||
stripepayment: stripepayment__metadata_ts,
|
||||
sylapsvideo: sylapsvideo_config_json,
|
||||
tandemvideo: tandemvideo__metadata_ts,
|
||||
|
||||
@@ -58,7 +58,6 @@ export const apiHandlers = {
|
||||
shimmervideo: import("./shimmervideo/api"),
|
||||
signal: import("./signal/api"),
|
||||
sirius_video: import("./sirius_video/api"),
|
||||
skiff: import("./skiff/api"),
|
||||
stripepayment: import("./stripepayment/api"),
|
||||
sylapsvideo: import("./sylapsvideo/api"),
|
||||
tandemvideo: import("./tandemvideo/api"),
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useEffect, useRef } from "react";
|
||||
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
|
||||
import { useHasTeamPlan } from "@calcom/lib/hooks/useHasPaidPlan";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -156,7 +155,7 @@ export const AppDependencyComponent = ({
|
||||
<div>
|
||||
<>
|
||||
<Link
|
||||
href={`${CAL_URL}/apps/${dependency.slug}`}
|
||||
href={`${WEBAPP_URL}/apps/${dependency.slug}`}
|
||||
className="text-info flex items-center underline">
|
||||
<span className="mr-1">
|
||||
{t("connect_app", { dependencyName: dependency.name })}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
|
||||
export const scopes = [
|
||||
"https://www.googleapis.com/auth/calendar.readonly",
|
||||
"https://www.googleapis.com/auth/calendar.events",
|
||||
"https://www.googleapis.com/auth/userinfo.profile",
|
||||
];
|
||||
|
||||
let client_id = "";
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { Auth } from "googleapis";
|
||||
import { google } from "googleapis";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { WEBAPP_URL_FOR_OAUTH, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { Prisma } from "@calcom/prisma/client";
|
||||
@@ -91,6 +93,10 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
throw new HttpError({ message: "Internal Error", statusCode: 500 });
|
||||
}
|
||||
|
||||
// Update the user's profile photo with google profile photo if it's null
|
||||
// Since we don't want to block the user from using the app if this fails, we don't await this
|
||||
updateProfilePhoto(oAuth2Client, req.session.user.id);
|
||||
|
||||
const credential = await prisma.credential.create({
|
||||
data: {
|
||||
type: "google_calendar",
|
||||
@@ -156,6 +162,23 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
);
|
||||
}
|
||||
|
||||
async function updateProfilePhoto(oAuth2Client: Auth.OAuth2Client, userId: number) {
|
||||
try {
|
||||
const oauth2 = google.oauth2({ version: "v2", auth: oAuth2Client });
|
||||
const userDetails = await oauth2.userinfo.get();
|
||||
if (userDetails.data?.picture) {
|
||||
await prisma.user.update({
|
||||
where: { id: userId, avatarUrl: null, avatar: null },
|
||||
data: {
|
||||
avatarUrl: userDetails.data.picture,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error updating avatarUrl from google calendar connect", error);
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { AlertCircle, ArrowRight, Check } from "@calcom/ui/components/icon";
|
||||
|
||||
@@ -38,7 +38,7 @@ const ExistingGoogleCal = ({ gCalInstalled, appName }: { gCalInstalled?: boolean
|
||||
<div>
|
||||
<>
|
||||
<Link
|
||||
href={`${CAL_URL}/apps/google-calendar`}
|
||||
href={`${WEBAPP_URL}/apps/google-calendar`}
|
||||
className="text-info flex items-center underline">
|
||||
<span className="mr-1">{t("connect_google_calendar")}</span>
|
||||
<ArrowRight />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -86,7 +86,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
);
|
||||
|
||||
res.redirect(
|
||||
getSafeRedirectUrl(`${CAL_URL}/apps/installed/automation?hl=intercom`) ??
|
||||
getSafeRedirectUrl(`${WEBAPP_URL}/apps/installed/automation?hl=intercom`) ??
|
||||
getInstalledAppPath({ variant: "automation", slug: "intercom" })
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import type {
|
||||
@@ -94,7 +94,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
|
||||
return {
|
||||
id: `${CAL_URL}/${slug}/${eventType.slug}`,
|
||||
id: `${WEBSITE_URL}/${slug}/${eventType.slug}`,
|
||||
type: "item",
|
||||
title: eventType.title,
|
||||
subtitle: `${slug}/${eventType.slug}`,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import type { TextComponent } from "../lib";
|
||||
@@ -9,11 +9,11 @@ import type { TextComponent } from "../lib";
|
||||
* @returns boolean
|
||||
*/
|
||||
export async function isValidCalURL(url: string) {
|
||||
const regex = new RegExp(`^${CAL_URL}/`, `i`);
|
||||
const regex = new RegExp(`^${WEBSITE_URL}/`, `i`);
|
||||
|
||||
const error: TextComponent = {
|
||||
type: "text",
|
||||
text: `This is not a valid ${CAL_URL.replace("https://", "")} link`,
|
||||
text: `This is not a valid ${WEBSITE_URL.replace("https://", "")} link`,
|
||||
style: "error",
|
||||
align: "left",
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import { z } from "zod";
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import { RoutingFormEmbedButton, RoutingFormEmbedDialog } from "@calcom/features/embed/RoutingFormEmbed";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
@@ -405,8 +405,8 @@ export const FormAction = forwardRef(function FormAction<T extends typeof Button
|
||||
const embedLink = `forms/${routingForm?.id}`;
|
||||
const orgBranding = useOrgBranding();
|
||||
|
||||
const formLink = `${orgBranding?.fullDomain ?? CAL_URL}/${embedLink}`;
|
||||
let redirectUrl = `${orgBranding?.fullDomain ?? CAL_URL}/router?form=${routingForm?.id}`;
|
||||
const formLink = `${orgBranding?.fullDomain ?? WEBSITE_URL}/${embedLink}`;
|
||||
let redirectUrl = `${orgBranding?.fullDomain ?? WEBSITE_URL}/router?form=${routingForm?.id}`;
|
||||
|
||||
routingForm?.fields?.forEach((field) => {
|
||||
redirectUrl += `&${getFieldIdentifier(field)}={Recalled_Response_For_This_Field}`;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import Link from "next/link";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import React, { useCallback, useState, useEffect } from "react";
|
||||
import { Query, Builder, Utils as QbUtils } from "react-awesome-query-builder";
|
||||
// types
|
||||
import type { JsonTree, ImmutableTree, BuilderProps } from "react-awesome-query-builder";
|
||||
@@ -98,7 +98,7 @@ const Route = ({
|
||||
|
||||
const index = routes.indexOf(route);
|
||||
|
||||
const { data: eventTypesByGroup } = trpc.viewer.eventTypes.getByViewer.useQuery({
|
||||
const { data: eventTypesByGroup, isLoading } = trpc.viewer.eventTypes.getByViewer.useQuery({
|
||||
forRoutingForms: true,
|
||||
});
|
||||
|
||||
@@ -138,9 +138,15 @@ const Route = ({
|
||||
? eventOptions[0].value.substring(0, eventOptions[0].value.lastIndexOf("/") + 1)
|
||||
: "";
|
||||
|
||||
const [customEventTypeSlug, setCustomEventTypeSlug] = useState<string>(
|
||||
!isRouter(route) ? route.action.value.split("/").pop() ?? "" : ""
|
||||
);
|
||||
const [customEventTypeSlug, setCustomEventTypeSlug] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
const isCustom =
|
||||
!isRouter(route) && !eventOptions.find((eventOption) => eventOption.value === route.action.value);
|
||||
setCustomEventTypeSlug(isCustom && !isRouter(route) ? route.action.value.split("/").pop() ?? "" : "");
|
||||
}
|
||||
}, [isLoading]);
|
||||
|
||||
const onChange = (route: Route, immutableTree: ImmutableTree, config: QueryBuilderUpdatedConfig) => {
|
||||
const jsonTree = QbUtils.getTree(immutableTree);
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
items:
|
||||
- 1.png
|
||||
- 2.png
|
||||
- 3.png
|
||||
- 4.png
|
||||
- 5.png
|
||||
---
|
||||
|
||||
{DESCRIPTION}
|
||||
|
||||
Skiff is a privacy-first, end-to-end encrypted workspace with Mail, Pages, Drive, and Calendar applications. Skiff Calendar integrates with Cal.com, enabling you to schedule appointments, organize events, and use your preferred video conferencing product.
|
||||
|
||||
## Features
|
||||
|
||||
- **Full workspace** — beyond email and calendar, Skiff enables writing and collaborating on end-to-end encrypted notes or wikis. You can also upload, preview, and share files.
|
||||
- **Native applications** — Skiff has native macOS, Windows, iOS, and Android applications, ensuring you can stay on top of your schedule from any device.
|
||||
- **Multiple accounts** — all Skiff products support multiple accounts, or sharing your entire team to collaborate inside one workspace.
|
||||
- **Custom domains** - you can use your own domain name for free on Skiff to send and receive mail or calendar invites.
|
||||
- **End-to-end encryption** — all data is encrypted on your device so that only you can access it. Skiff Mail is also open-source to ensure the highest level of transparency.
|
||||
- **Complete privacy** - no trackers or ads, and your data is never sold.
|
||||
@@ -1,20 +0,0 @@
|
||||
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
|
||||
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
|
||||
|
||||
import appConfig from "../config.json";
|
||||
|
||||
const handler: AppDeclarativeHandler = {
|
||||
appType: appConfig.type,
|
||||
variant: appConfig.variant,
|
||||
slug: appConfig.slug,
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
redirect: {
|
||||
newTab: true,
|
||||
url: "https://skiff.com/blog/skiff-cal-com-launch",
|
||||
},
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
@@ -1 +0,0 @@
|
||||
export { default as add } from "./add";
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"/*": "Don't modify slug - If required, do it using cli edit command",
|
||||
"name": "Skiff",
|
||||
"slug": "skiff",
|
||||
"type": "skiff_calendar",
|
||||
"logo": "icon.svg",
|
||||
"url": "https://skiff.com/blog/skiff-cal-com-launch",
|
||||
"variant": "calendar",
|
||||
"categories": ["calendar"],
|
||||
"publisher": "Skiff World, Inc.",
|
||||
"email": "hello@skiff.org",
|
||||
"description": "End-to-end encrypted, private calendar, email, notes, and drive.",
|
||||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "link-as-an-app",
|
||||
"dirName": "skiff"
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * as api from "./api";
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"name": "@calcom/skiff",
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
},
|
||||
"description": "End-to-end encrypted, private calendar, email, notes, and drive."
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 400 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 294 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 346 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 207 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 283 KiB |
@@ -1,13 +0,0 @@
|
||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 399.586C0 258.244 0 187.573 27.9428 133.781C51.49 88.4505 88.4505 51.49 133.781 27.9428C187.573 0 258.244 0 399.586 0H624.414C765.756 0 836.427 0 890.219 27.9428C935.549 51.49 972.51 88.4505 996.057 133.781C1024 187.573 1024 258.244 1024 399.586V624.414C1024 765.756 1024 836.427 996.057 890.219C972.51 935.549 935.549 972.51 890.219 996.057C836.427 1024 765.756 1024 624.414 1024H399.586C258.244 1024 187.573 1024 133.781 996.057C88.4505 972.51 51.49 935.549 27.9428 890.219C0 836.427 0 765.756 0 624.414V399.586Z" fill="#EF5A3C"/>
|
||||
<g clip-path="url(#clip0_1851_83221)">
|
||||
<path d="M634.311 128.949C671.289 128.001 708.267 135.586 742.4 150.756C716.8 134.638 689.304 121.364 660.859 110.934C518.637 58.7861 358.4 85.3342 241.778 181.097C237.985 183.942 228.504 192.475 224.711 196.268C220.918 200.06 212.385 207.645 208.592 211.438C203.852 216.179 198.163 222.816 193.422 227.556C189.629 232.297 182.044 240.831 179.2 244.623C175.407 250.312 170.666 256.001 167.822 260.742C164.978 264.534 158.341 274.964 154.548 279.705C153.6 281.601 149.807 287.29 148.859 289.186C147.911 290.134 145.066 295.823 144.118 296.771C142.222 299.616 135.585 311.942 133.689 314.786C131.792 319.527 126.104 329.956 124.207 334.697C122.311 339.438 118.518 347.023 116.622 351.764C115.674 353.66 110.933 366.934 109.985 368.831C108.089 372.623 106.192 379.26 105.244 383.053C102.4 390.638 100.504 400.119 97.6591 408.653C96.7109 413.393 93.8665 425.719 92.9183 430.46L91.9702 434.253C73.0072 533.808 91.9702 639.053 143.17 727.23C148.859 735.764 155.496 747.142 161.185 755.675C164.029 759.467 167.822 765.156 170.666 768.001L180.148 780.327C182.992 784.119 191.526 793.601 194.37 797.393C199.111 802.134 204.8 808.771 209.541 813.512C212.385 816.356 217.126 821.097 219.97 823.942L225.659 828.682L226.607 829.63L228.504 830.579L231.348 832.475C235.141 835.319 239.881 838.164 243.674 841.008L246.518 842.905C247.467 843.853 248.415 843.853 249.363 844.801C253.155 846.697 258.844 850.49 261.689 852.386C263.585 853.334 266.429 855.23 268.326 856.179C270.222 857.127 275.911 859.971 278.755 860.919C281.6 862.816 286.341 863.764 289.185 865.66C296.77 868.505 304.355 871.349 311.941 874.193C315.733 875.142 320.474 877.038 324.267 877.986C327.111 878.934 332.8 879.882 335.644 880.83C338.489 881.779 343.229 881.779 347.022 882.727C360.296 884.623 372.622 885.571 385.896 885.571C417.185 884.623 448.474 877.986 476.918 865.66C537.6 840.06 587.852 788.86 611.555 726.282L614.4 719.645C615.348 717.749 617.244 711.112 618.193 709.216C620.089 703.527 621.985 696.89 622.933 691.201C622.933 689.305 623.881 685.512 624.83 683.616C626.726 673.186 628.622 660.86 628.622 650.43C628.622 647.586 628.622 641.897 628.622 639.053C628.622 639.053 628.622 638.105 628.622 637.156C620.089 637.156 612.504 636.208 603.97 635.26C474.074 621.986 375.467 509.156 379.259 377.364C382.104 237.986 497.778 128.001 634.311 128.949ZM310.044 792.653C293.926 783.171 277.807 771.793 264.533 758.519C205.748 701.63 165.926 625.779 152.652 544.238C128 400.119 187.733 252.208 306.252 165.927C372.622 117.571 456.059 91.0231 538.548 93.8676C440.889 98.6083 345.126 139.379 275.911 209.542C189.629 295.823 148.859 420.979 168.77 541.393C182.044 624.83 223.763 704.475 286.341 761.364C350.815 818.253 447.526 831.527 525.274 794.549C459.852 832.475 375.467 830.579 310.044 792.653ZM322.37 689.305C169.718 536.653 197.215 277.808 379.259 160.238C402.963 145.068 428.563 132.742 456.059 123.26C498.726 109.038 544.237 103.349 588.8 107.142C499.674 106.193 411.496 140.327 347.97 201.008C214.281 326.164 204.8 536.653 327.111 674.134C329.955 676.979 334.696 682.668 338.489 686.46C396.326 746.193 492.089 763.26 565.096 721.542C489.244 772.742 384 754.727 322.37 689.305ZM364.089 576.475L371.674 587.853L380.207 599.23L389.689 610.608C399.17 621.038 408.652 631.468 420.03 640.001C464.592 675.082 527.17 689.305 582.163 670.342C546.133 686.46 504.415 687.408 466.489 676.03C432.355 665.601 402.963 645.69 380.207 620.09C335.644 572.682 308.148 511.053 302.459 446.579C283.496 256.949 447.526 95.7639 637.155 124.208C593.541 122.312 549.926 128.001 510.104 143.171C499.674 146.964 487.348 152.653 477.867 158.342C475.97 159.29 470.281 162.134 468.385 163.082C466.489 164.031 460.8 167.823 458.904 168.771C455.111 170.668 450.37 174.46 446.578 176.356C442.785 179.201 438.044 182.045 435.2 184.89C311.941 274.964 280.652 447.527 364.089 576.475Z" fill="black"/>
|
||||
<path d="M777.481 841.956C781.274 839.111 790.755 830.578 794.548 826.785C798.34 822.993 806.874 815.407 810.666 811.615C815.407 806.874 821.096 800.237 825.836 795.496C829.629 790.756 837.214 782.222 840.059 778.43C843.851 773.689 848.592 768 851.437 763.259C854.281 759.467 860.918 749.037 864.711 744.296C865.659 742.4 869.451 736.711 870.399 734.815C871.348 733.867 874.192 728.178 875.14 727.23C877.037 724.385 883.674 712.059 885.57 709.215C887.466 704.474 893.155 694.044 895.051 689.304C896.948 684.563 900.74 676.978 902.636 672.237C903.585 670.341 908.325 657.067 909.274 655.17C911.17 651.378 913.066 644.741 914.014 640.948C916.859 633.363 918.755 623.882 921.599 615.348C922.548 610.607 925.392 598.281 926.34 593.541L927.288 589.748C946.251 490.193 927.288 384.948 876.088 296.77C870.399 288.237 863.762 276.859 858.074 268.326C856.177 264.533 851.436 259.793 849.54 256L840.059 243.674C837.214 239.881 828.681 230.4 825.836 226.607C821.096 221.867 815.407 215.23 810.666 210.489C807.822 207.644 803.081 202.904 800.236 200.059L794.548 195.318L793.599 194.37L791.703 193.422L786.962 190.578C783.17 187.733 778.429 184.889 774.636 182.044L771.792 180.148C770.844 179.2 769.896 179.2 768.948 178.252C765.155 176.356 759.466 172.563 756.622 170.667C754.725 169.718 751.881 167.822 749.985 166.874C748.088 165.926 742.399 163.081 739.555 162.133C736.711 160.237 731.97 159.289 729.125 157.393C721.54 154.548 713.955 151.704 706.37 148.859C702.577 147.911 697.836 146.015 694.044 145.067C691.199 144.118 685.511 143.17 682.666 142.222C679.822 141.274 675.081 141.274 671.288 140.326C658.014 138.43 645.688 137.481 632.414 137.481C601.125 138.43 569.836 145.067 541.392 157.393C480.711 182.993 430.459 234.193 406.755 296.77L403.911 303.407C402.962 305.304 401.066 311.941 400.118 313.837C398.222 319.526 396.325 326.163 395.377 331.852C395.377 333.748 394.429 337.541 393.481 339.437C391.585 349.867 389.688 362.193 389.688 372.622C389.688 375.467 389.688 381.156 389.688 384C389.688 384 389.688 384.948 389.688 386.844C530.014 387.793 641.896 505.363 638.103 645.689C634.311 785.067 519.585 894.104 383.051 894.104C346.074 895.052 309.096 887.467 274.962 872.296C300.562 888.415 328.059 901.689 356.503 912.119C500.622 964.267 659.911 937.719 777.481 841.956ZM709.214 231.348C725.333 240.83 741.451 252.207 754.725 265.481C813.511 322.37 853.333 398.222 866.607 479.763C891.259 623.881 830.577 772.741 712.059 858.074C645.688 906.43 562.251 932.978 479.762 930.133C578.37 926.341 674.133 885.57 742.399 815.407C829.629 729.126 869.451 603.97 849.54 482.607C836.266 399.17 794.548 319.526 731.97 262.637C667.496 205.748 570.785 192.474 493.036 229.452C559.407 191.526 643.792 193.422 709.214 231.348ZM696.888 334.696C849.54 487.348 822.044 746.193 639.999 863.763C616.296 878.933 590.696 891.259 563.199 900.741C520.533 914.963 475.022 920.652 430.459 916.859C519.585 917.807 607.762 883.674 671.288 822.993C805.925 696.889 814.459 486.4 693.096 349.867C690.251 347.022 685.511 341.333 681.718 337.541C623.881 277.807 528.118 260.741 455.111 302.459C530.014 251.259 635.259 269.274 696.888 334.696ZM511.051 882.726C521.481 878.933 533.807 873.244 543.288 867.556C545.185 866.607 550.874 863.763 552.77 862.815C554.666 861.867 560.355 858.074 562.251 857.126C566.044 855.23 570.785 851.437 574.577 849.541C578.37 846.696 583.111 843.852 585.955 841.007C708.266 749.037 738.607 576.474 656.118 447.526L647.585 436.148L639.051 424.77L629.57 413.393C620.088 402.963 610.607 392.533 599.229 384C554.666 348.919 492.088 334.696 437.096 353.659C473.125 337.541 514.844 336.593 552.77 347.97C586.903 358.4 616.296 378.311 639.051 403.911C683.614 451.319 711.111 512.948 716.799 577.422C735.762 767.052 571.733 928.237 382.103 899.793C426.666 904.533 470.281 897.896 511.051 882.726Z" fill="black"/>
|
||||
<path d="M401.066 388.741C398.221 388.741 395.377 387.793 392.532 387.793C392.532 387.793 392.532 387.793 392.532 388.741C399.169 388.741 405.806 389.689 412.443 390.637C408.651 389.689 404.858 389.689 401.066 388.741Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1851_83221">
|
||||
<rect width="853.333" height="853.333" fill="white" transform="translate(85.3333 85.3333)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 8.5 KiB |
@@ -21,7 +21,7 @@
|
||||
"@stripe/stripe-js": "^1.35.0",
|
||||
"stripe": "^9.16.0",
|
||||
"uuid": "^8.3.2",
|
||||
"zod": "^3.22.2"
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*",
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
addForm as addRoutingForm,
|
||||
addOneFieldAndDescriptionAndSaveForm,
|
||||
} from "@calcom/app-store/routing-forms/playwright/tests/testUtils";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import type { Fixtures } from "@calcom/web/playwright/lib/fixtures";
|
||||
import { test } from "@calcom/web/playwright/lib/fixtures";
|
||||
|
||||
@@ -46,7 +46,7 @@ test.describe("Typeform App", () => {
|
||||
const text = await page.evaluate(async () => {
|
||||
return navigator.clipboard.readText();
|
||||
});
|
||||
expect(text).toBe(`${CAL_URL}/router?form=${formId}&test={Recalled_Response_For_This_Field}`);
|
||||
expect(text).toBe(`${WEBAPP_URL}/router?form=${formId}&test={Recalled_Response_For_This_Field}`);
|
||||
});
|
||||
|
||||
test("should copy link in RoutingForms list", async ({ page, context, users }) => {
|
||||
@@ -66,7 +66,7 @@ test.describe("Typeform App", () => {
|
||||
const text = await page.evaluate(async () => {
|
||||
return navigator.clipboard.readText();
|
||||
});
|
||||
expect(text).toBe(`${CAL_URL}/router?form=${formId}&test={Recalled_Response_For_This_Field}`);
|
||||
expect(text).toBe(`${WEBAPP_URL}/router?form=${formId}&test={Recalled_Response_For_This_Field}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import { RRule } from "rrule";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getRichDescription } from "@calcom/lib/CalEventParser";
|
||||
import { getWhen } from "@calcom/lib/CalEventParser";
|
||||
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
|
||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
|
||||
export enum BookingAction {
|
||||
@@ -33,6 +34,8 @@ const generateIcsString = ({
|
||||
isRequestReschedule?: boolean;
|
||||
t?: TFunction;
|
||||
}) => {
|
||||
const location = getVideoCallUrlFromCalEvent(event) || event.location;
|
||||
|
||||
// Taking care of recurrence rule
|
||||
let recurrenceRule: string | undefined = undefined;
|
||||
const partstat: ParticipationStatus = "ACCEPTED";
|
||||
@@ -93,6 +96,7 @@ const generateIcsString = ({
|
||||
}))
|
||||
: []),
|
||||
],
|
||||
location: location ?? undefined,
|
||||
method: "REQUEST",
|
||||
status,
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
import { test } from "@calcom/web/test/fixtures/fixtures";
|
||||
|
||||
import { buildCalendarEvent, buildPerson } from "../../../lib/test/builder";
|
||||
import { buildVideoCallData } from "../../../lib/test/builder";
|
||||
import generateIcsString from "../generateIcsString";
|
||||
|
||||
const assertHasIcsString = (icsString: string | undefined) => {
|
||||
@@ -49,89 +50,145 @@ const testIcsStringContains = ({
|
||||
};
|
||||
|
||||
describe("generateIcsString", () => {
|
||||
test("when bookingAction is Create", () => {
|
||||
const event = buildCalendarEvent({
|
||||
iCalSequence: 0,
|
||||
attendees: [buildPerson()],
|
||||
describe("booking actions", () => {
|
||||
test("when bookingAction is Create", () => {
|
||||
const event = buildCalendarEvent({
|
||||
iCalSequence: 0,
|
||||
attendees: [buildPerson()],
|
||||
});
|
||||
|
||||
const title = "new_event_scheduled_recurring";
|
||||
const subtitle = "emailed_you_and_any_other_attendees";
|
||||
const status = "CONFIRMED";
|
||||
|
||||
const icsString = generateIcsString({
|
||||
event: event,
|
||||
title,
|
||||
subtitle,
|
||||
role: "organizer",
|
||||
status,
|
||||
});
|
||||
|
||||
const assertedIcsString = assertHasIcsString(icsString);
|
||||
|
||||
testIcsStringContains({ icsString: assertedIcsString, event, status });
|
||||
});
|
||||
test("when bookingAction is Cancel", () => {
|
||||
const event = buildCalendarEvent({
|
||||
iCalSequence: 0,
|
||||
attendees: [buildPerson()],
|
||||
});
|
||||
const title = "event_request_cancelled";
|
||||
const subtitle = "emailed_you_and_any_other_attendees";
|
||||
const status = "CANCELLED";
|
||||
|
||||
const title = "new_event_scheduled_recurring";
|
||||
const subtitle = "emailed_you_and_any_other_attendees";
|
||||
const status = "CONFIRMED";
|
||||
const icsString = generateIcsString({
|
||||
event: event,
|
||||
title,
|
||||
subtitle,
|
||||
role: "organizer",
|
||||
status,
|
||||
});
|
||||
|
||||
const icsString = generateIcsString({
|
||||
event: event,
|
||||
title,
|
||||
subtitle,
|
||||
role: "organizer",
|
||||
status,
|
||||
const assertedIcsString = assertHasIcsString(icsString);
|
||||
|
||||
testIcsStringContains({ icsString: assertedIcsString, event, status });
|
||||
});
|
||||
test("when bookingAction is Reschedule", () => {
|
||||
const event = buildCalendarEvent({
|
||||
iCalSequence: 0,
|
||||
attendees: [buildPerson()],
|
||||
});
|
||||
const title = "event_type_has_been_rescheduled";
|
||||
const subtitle = "emailed_you_and_any_other_attendees";
|
||||
const status = "CONFIRMED";
|
||||
|
||||
const assertedIcsString = assertHasIcsString(icsString);
|
||||
const icsString = generateIcsString({
|
||||
event: event,
|
||||
title,
|
||||
subtitle,
|
||||
role: "organizer",
|
||||
status,
|
||||
});
|
||||
|
||||
testIcsStringContains({ icsString: assertedIcsString, event, status });
|
||||
const assertedIcsString = assertHasIcsString(icsString);
|
||||
|
||||
testIcsStringContains({ icsString: assertedIcsString, event, status });
|
||||
});
|
||||
test("when bookingAction is RequestReschedule", () => {
|
||||
const event = buildCalendarEvent({
|
||||
iCalSequence: 0,
|
||||
attendees: [buildPerson()],
|
||||
});
|
||||
const title = "request_reschedule_title_organizer";
|
||||
const subtitle = "request_reschedule_subtitle_organizer";
|
||||
const status = "CANCELLED";
|
||||
|
||||
const icsString = generateIcsString({
|
||||
event: event,
|
||||
title,
|
||||
subtitle,
|
||||
role: "organizer",
|
||||
status,
|
||||
});
|
||||
|
||||
const assertedIcsString = assertHasIcsString(icsString);
|
||||
|
||||
testIcsStringContains({ icsString: assertedIcsString, event, status });
|
||||
});
|
||||
});
|
||||
test("when bookingAction is Cancel", () => {
|
||||
const event = buildCalendarEvent({
|
||||
iCalSequence: 0,
|
||||
attendees: [buildPerson()],
|
||||
describe("set location", () => {
|
||||
test("Location is a video link", () => {
|
||||
const videoCallData = buildVideoCallData();
|
||||
const event = buildCalendarEvent(
|
||||
{
|
||||
iCalSequence: 0,
|
||||
attendees: [buildPerson()],
|
||||
videoCallData,
|
||||
},
|
||||
true
|
||||
);
|
||||
const title = "request_reschedule_title_organizer";
|
||||
const subtitle = "request_reschedule_subtitle_organizer";
|
||||
const status = "CANCELLED";
|
||||
|
||||
const icsString = generateIcsString({
|
||||
event: event,
|
||||
title,
|
||||
subtitle,
|
||||
role: "organizer",
|
||||
status,
|
||||
});
|
||||
|
||||
const assertedIcsString = assertHasIcsString(icsString);
|
||||
|
||||
expect(icsString).toEqual(expect.stringContaining(`LOCATION:${videoCallData.url}`));
|
||||
});
|
||||
const title = "event_request_cancelled";
|
||||
const subtitle = "emailed_you_and_any_other_attendees";
|
||||
const status = "CANCELLED";
|
||||
// Could be custom link, address, or phone number
|
||||
test("Location is a string", () => {
|
||||
const event = buildCalendarEvent(
|
||||
{
|
||||
iCalSequence: 0,
|
||||
attendees: [buildPerson()],
|
||||
location: "+1234567890",
|
||||
},
|
||||
true
|
||||
);
|
||||
const title = "request_reschedule_title_organizer";
|
||||
const subtitle = "request_reschedule_subtitle_organizer";
|
||||
const status = "CANCELLED";
|
||||
|
||||
const icsString = generateIcsString({
|
||||
event: event,
|
||||
title,
|
||||
subtitle,
|
||||
role: "organizer",
|
||||
status,
|
||||
const icsString = generateIcsString({
|
||||
event: event,
|
||||
title,
|
||||
subtitle,
|
||||
role: "organizer",
|
||||
status,
|
||||
});
|
||||
|
||||
const assertedIcsString = assertHasIcsString(icsString);
|
||||
|
||||
expect(icsString).toEqual(expect.stringContaining(`LOCATION:${event.location}`));
|
||||
});
|
||||
|
||||
const assertedIcsString = assertHasIcsString(icsString);
|
||||
|
||||
testIcsStringContains({ icsString: assertedIcsString, event, status });
|
||||
});
|
||||
test("when bookingAction is Reschedule", () => {
|
||||
const event = buildCalendarEvent({
|
||||
iCalSequence: 0,
|
||||
attendees: [buildPerson()],
|
||||
});
|
||||
const title = "event_type_has_been_rescheduled";
|
||||
const subtitle = "emailed_you_and_any_other_attendees";
|
||||
const status = "CONFIRMED";
|
||||
|
||||
const icsString = generateIcsString({
|
||||
event: event,
|
||||
title,
|
||||
subtitle,
|
||||
role: "organizer",
|
||||
status,
|
||||
});
|
||||
|
||||
const assertedIcsString = assertHasIcsString(icsString);
|
||||
|
||||
testIcsStringContains({ icsString: assertedIcsString, event, status });
|
||||
});
|
||||
test("when bookingAction is RequestReschedule", () => {
|
||||
const event = buildCalendarEvent({
|
||||
iCalSequence: 0,
|
||||
attendees: [buildPerson()],
|
||||
});
|
||||
const title = "request_reschedule_title_organizer";
|
||||
const subtitle = "request_reschedule_subtitle_organizer";
|
||||
const status = "CANCELLED";
|
||||
|
||||
const icsString = generateIcsString({
|
||||
event: event,
|
||||
title,
|
||||
subtitle,
|
||||
role: "organizer",
|
||||
status,
|
||||
});
|
||||
|
||||
const assertedIcsString = assertHasIcsString(icsString);
|
||||
|
||||
testIcsStringContains({ icsString: assertedIcsString, event, status });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TFunction } from "next-i18next";
|
||||
import type { TFunction } from "next-i18next";
|
||||
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
import { BaseEmailHtml, CallToAction } from "../components";
|
||||
|
||||
@@ -30,7 +30,7 @@ export const DisabledAppEmail = (
|
||||
|
||||
<CallToAction
|
||||
label={t("edit_event_type")}
|
||||
href={`${CAL_URL}/event-types/${eventTypeId}?tabName=apps`}
|
||||
href={`${WEBAPP_URL}/event-types/${eventTypeId}?tabName=apps`}
|
||||
/>
|
||||
</>
|
||||
) : title && eventTypeId ? (
|
||||
@@ -43,7 +43,7 @@ export const DisabledAppEmail = (
|
||||
|
||||
<CallToAction
|
||||
label={t("edit_event_type")}
|
||||
href={`${CAL_URL}/event-types/${eventTypeId}?tabName=apps`}
|
||||
href={`${WEBAPP_URL}/event-types/${eventTypeId}?tabName=apps`}
|
||||
/>
|
||||
</>
|
||||
) : appType.some((type) => type === "video") ? (
|
||||
@@ -54,7 +54,7 @@ export const DisabledAppEmail = (
|
||||
|
||||
<hr style={{ marginBottom: "24px" }} />
|
||||
|
||||
<CallToAction label={t("navigate_installed_apps")} href={`${CAL_URL}/apps/installed`} />
|
||||
<CallToAction label={t("navigate_installed_apps")} href={`${WEBAPP_URL}/apps/installed`} />
|
||||
</>
|
||||
) : appType.some((type) => type === "calendar") ? (
|
||||
<>
|
||||
@@ -67,7 +67,7 @@ export const DisabledAppEmail = (
|
||||
|
||||
<hr style={{ marginBottom: "24px" }} />
|
||||
|
||||
<CallToAction label={t("navigate_installed_apps")} href={`${CAL_URL}/apps/installed`} />
|
||||
<CallToAction label={t("navigate_installed_apps")} href={`${WEBAPP_URL}/apps/installed`} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -77,7 +77,7 @@ export const DisabledAppEmail = (
|
||||
|
||||
<hr style={{ marginBottom: "24px" }} />
|
||||
|
||||
<CallToAction label={t("navigate_installed_apps")} href={`${CAL_URL}/apps/installed`} />
|
||||
<CallToAction label={t("navigate_installed_apps")} href={`${WEBAPP_URL}/apps/installed`} />
|
||||
</>
|
||||
)}
|
||||
</BaseEmailHtml>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
import { Trans } from "next-i18next";
|
||||
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
import { BaseEmailHtml, CallToAction } from "../components";
|
||||
|
||||
@@ -49,7 +49,7 @@ export const SlugReplacementEmail = (
|
||||
<td align="center">
|
||||
<CallToAction
|
||||
label={t("review_event_type")}
|
||||
href={`${CAL_URL}/event-types`}
|
||||
href={`${WEBAPP_URL}/event-types`}
|
||||
endIconName="white-arrow-right"
|
||||
/>
|
||||
</td>
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { AuthOptions, Session } from "next-auth";
|
||||
import { getToken } from "next-auth/jwt";
|
||||
|
||||
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
@@ -94,7 +94,7 @@ export async function getServerSession(options: {
|
||||
emailVerified: user.emailVerified,
|
||||
email_verified: user.emailVerified !== null,
|
||||
role: user.role,
|
||||
image: `${CAL_URL}/${user.username}/avatar.png`,
|
||||
image: `${WEBAPP_URL}/${user.username}/avatar.png`,
|
||||
belongsToActiveTeam: token.belongsToActiveTeam,
|
||||
org: token.org,
|
||||
locale: user.locale ?? undefined,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Membership, Team, UserPermissionRole } from "@prisma/client";
|
||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
||||
import type { AuthOptions, Session } from "next-auth";
|
||||
import type { JWT } from "next-auth/jwt";
|
||||
import { encode } from "next-auth/jwt";
|
||||
@@ -42,8 +43,8 @@ const ORGANIZATIONS_AUTOLINK =
|
||||
|
||||
const usernameSlug = (username: string) => `${slugify(username)}-${randomString(6).toLowerCase()}`;
|
||||
|
||||
const loginWithTotp = async (user: { email: string }) =>
|
||||
`/auth/login?totp=${await (await import("./signJwt")).default({ email: user.email })}`;
|
||||
const loginWithTotp = async (email: string) =>
|
||||
`/auth/login?totp=${await (await import("./signJwt")).default({ email })}`;
|
||||
|
||||
type UserTeams = {
|
||||
teams: (Membership & {
|
||||
@@ -126,18 +127,18 @@ const providers: Provider[] = [
|
||||
if (user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) {
|
||||
throw new Error(ErrorCode.ThirdPartyIdentityProviderEnabled);
|
||||
}
|
||||
if (!user.password && user.identityProvider == IdentityProvider.CAL) {
|
||||
if (!user.password?.hash && user.identityProvider == IdentityProvider.CAL) {
|
||||
throw new Error(ErrorCode.IncorrectEmailPassword);
|
||||
}
|
||||
if (!user.password && user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) {
|
||||
if (!user.password?.hash && user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) {
|
||||
throw new Error(ErrorCode.IncorrectEmailPassword);
|
||||
}
|
||||
|
||||
if (user.password && !credentials.totpCode) {
|
||||
if (!user.password) {
|
||||
if (user.password?.hash && !credentials.totpCode) {
|
||||
if (!user.password?.hash) {
|
||||
throw new Error(ErrorCode.IncorrectEmailPassword);
|
||||
}
|
||||
const isCorrectPassword = await verifyPassword(credentials.password, user.password);
|
||||
const isCorrectPassword = await verifyPassword(credentials.password, user.password.hash);
|
||||
if (!isCorrectPassword) {
|
||||
throw new Error(ErrorCode.IncorrectEmailPassword);
|
||||
}
|
||||
@@ -717,7 +718,7 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||
}
|
||||
}
|
||||
if (existingUser.twoFactorEnabled && existingUser.identityProvider === idP) {
|
||||
return loginWithTotp(existingUser);
|
||||
return loginWithTotp(existingUser.email);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
@@ -733,7 +734,7 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||
if (!userWithNewEmail) {
|
||||
await prisma.user.update({ where: { id: existingUser.id }, data: { email: user.email } });
|
||||
if (existingUser.twoFactorEnabled) {
|
||||
return loginWithTotp(existingUser);
|
||||
return loginWithTotp(existingUser.email);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
@@ -752,6 +753,9 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||
mode: "insensitive",
|
||||
},
|
||||
},
|
||||
include: {
|
||||
password: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingUserWithEmail) {
|
||||
@@ -762,7 +766,7 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||
existingUserWithEmail.identityProvider !== IdentityProvider.CAL
|
||||
) {
|
||||
if (existingUserWithEmail.twoFactorEnabled) {
|
||||
return loginWithTotp(existingUserWithEmail);
|
||||
return loginWithTotp(existingUserWithEmail.email);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
@@ -770,7 +774,7 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||
|
||||
// check if user was invited
|
||||
if (
|
||||
!existingUserWithEmail.password &&
|
||||
!existingUserWithEmail.password?.hash &&
|
||||
!existingUserWithEmail.emailVerified &&
|
||||
!existingUserWithEmail.username
|
||||
) {
|
||||
@@ -792,7 +796,7 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||
});
|
||||
|
||||
if (existingUserWithEmail.twoFactorEnabled) {
|
||||
return loginWithTotp(existingUserWithEmail);
|
||||
return loginWithTotp(existingUserWithEmail.email);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
@@ -803,18 +807,34 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||
existingUserWithEmail.identityProvider === IdentityProvider.CAL &&
|
||||
(idP === IdentityProvider.GOOGLE || idP === IdentityProvider.SAML)
|
||||
) {
|
||||
await prisma.user.update({
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { email: existingUserWithEmail.email },
|
||||
// also update email to the IdP email
|
||||
data: {
|
||||
password: null,
|
||||
email: user.email,
|
||||
identityProvider: idP,
|
||||
identityProviderId: account.providerAccountId,
|
||||
},
|
||||
});
|
||||
|
||||
// safely delete password from UserPassword table if it exists
|
||||
try {
|
||||
await prisma.userPassword.delete({
|
||||
where: { userId: updatedUser.id },
|
||||
});
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof PrismaClientKnownRequestError &&
|
||||
(err.code === "P2025" || err.code === "P2016")
|
||||
) {
|
||||
log.warn("UserPassword not found for user", safeStringify(existingUserWithEmail));
|
||||
} else {
|
||||
log.warn("Could not delete UserPassword for user", safeStringify(existingUserWithEmail));
|
||||
}
|
||||
}
|
||||
|
||||
if (existingUserWithEmail.twoFactorEnabled) {
|
||||
return loginWithTotp(existingUserWithEmail);
|
||||
return loginWithTotp(existingUserWithEmail.email);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
@@ -853,7 +873,7 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||
await calcomAdapter.linkAccount(linkAccountNewUserData);
|
||||
|
||||
if (account.twoFactorEnabled) {
|
||||
return loginWithTotp(newUser);
|
||||
return loginWithTotp(newUser.email);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -138,18 +138,22 @@ async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) {
|
||||
where: { email },
|
||||
update: {
|
||||
username,
|
||||
password: hashedPassword,
|
||||
emailVerified: new Date(Date.now()),
|
||||
identityProvider: IdentityProvider.CAL,
|
||||
password: {
|
||||
upsert: {
|
||||
create: { hash: hashedPassword },
|
||||
update: { hash: hashedPassword },
|
||||
},
|
||||
},
|
||||
},
|
||||
create: {
|
||||
username,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
identityProvider: IdentityProvider.CAL,
|
||||
password: { create: { hash: hashedPassword } },
|
||||
},
|
||||
});
|
||||
|
||||
// Wrapping in a transaction as if one fails we want to rollback the whole thing to preventa any data inconsistencies
|
||||
const { membership } = await createOrUpdateMemberships({
|
||||
teamMetadata,
|
||||
@@ -180,7 +184,7 @@ async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) {
|
||||
data: {
|
||||
username,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
password: { create: { hash: hashedPassword } },
|
||||
metadata: {
|
||||
stripeCustomerId: customer.id,
|
||||
checkoutSessionId,
|
||||
|
||||
@@ -79,14 +79,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
where: { email: userEmail },
|
||||
update: {
|
||||
username: correctedUsername,
|
||||
password: hashedPassword,
|
||||
password: {
|
||||
upsert: {
|
||||
create: { hash: hashedPassword },
|
||||
update: { hash: hashedPassword },
|
||||
},
|
||||
},
|
||||
emailVerified: new Date(Date.now()),
|
||||
identityProvider: IdentityProvider.CAL,
|
||||
},
|
||||
create: {
|
||||
username: correctedUsername,
|
||||
email: userEmail,
|
||||
password: hashedPassword,
|
||||
password: { create: { hash: hashedPassword } },
|
||||
identityProvider: IdentityProvider.CAL,
|
||||
},
|
||||
});
|
||||
@@ -128,14 +133,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
where: { email: userEmail },
|
||||
update: {
|
||||
username: correctedUsername,
|
||||
password: hashedPassword,
|
||||
password: {
|
||||
upsert: {
|
||||
create: { hash: hashedPassword },
|
||||
update: { hash: hashedPassword },
|
||||
},
|
||||
},
|
||||
emailVerified: new Date(Date.now()),
|
||||
identityProvider: IdentityProvider.CAL,
|
||||
},
|
||||
create: {
|
||||
username: correctedUsername,
|
||||
email: userEmail,
|
||||
password: hashedPassword,
|
||||
password: { create: { hash: hashedPassword } },
|
||||
identityProvider: IdentityProvider.CAL,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -245,7 +245,7 @@ async function handler(req: CustomRequest) {
|
||||
startTime: bookingToDelete?.startTime ? dayjs(bookingToDelete.startTime).format() : "",
|
||||
endTime: bookingToDelete?.endTime ? dayjs(bookingToDelete.endTime).format() : "",
|
||||
organizer: {
|
||||
email: organizer.email,
|
||||
email: bookingToDelete?.userPrimaryEmail ?? organizer.email,
|
||||
name: organizer.name ?? "Nameless",
|
||||
timeZone: organizer.timeZone,
|
||||
timeFormat: getTimeFormatStringFromUserTimeFormat(organizer.timeFormat),
|
||||
@@ -524,7 +524,7 @@ async function handler(req: CustomRequest) {
|
||||
startTime: bookingToDelete.startTime.toISOString(),
|
||||
endTime: bookingToDelete.endTime.toISOString(),
|
||||
organizer: {
|
||||
email: bookingToDelete.user?.email ?? "dev@calendso.com",
|
||||
email: bookingToDelete?.userPrimaryEmail ?? bookingToDelete.user?.email ?? "dev@calendso.com",
|
||||
name: bookingToDelete.user?.name ?? "no user",
|
||||
timeZone: bookingToDelete.user?.timeZone ?? "",
|
||||
timeFormat: getTimeFormatStringFromUserTimeFormat(organizer.timeFormat),
|
||||
|
||||
@@ -342,6 +342,7 @@ export const getEventTypesFromDB = async (eventTypeId: number) => {
|
||||
durationLimits: true,
|
||||
assignAllTeamMembers: true,
|
||||
parentId: true,
|
||||
useEventTypeDestinationCalendarEmail: true,
|
||||
owner: {
|
||||
select: {
|
||||
hideBranding: true,
|
||||
@@ -741,6 +742,7 @@ async function createBooking({
|
||||
|
||||
const newBookingData: Prisma.BookingCreateInput = {
|
||||
uid,
|
||||
userPrimaryEmail: evt.organizer.email,
|
||||
responses: responses === null || evt.seatsPerTimeSlot ? Prisma.JsonNull : responses,
|
||||
title: evt.title,
|
||||
startTime: dayjs.utc(evt.startTime).toDate(),
|
||||
@@ -1496,6 +1498,7 @@ async function handler(
|
||||
if (isTeamEventType && eventType.schedulingType === "COLLECTIVE" && user.destinationCalendar) {
|
||||
teamDestinationCalendars.push(user.destinationCalendar);
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email ?? "",
|
||||
@@ -1548,6 +1551,12 @@ async function handler(
|
||||
? await getBookerBaseUrl(eventType.team.parentId)
|
||||
: await getBookerBaseUrl(organizerOrganizationId ?? null);
|
||||
|
||||
const destinationCalendar = eventType.destinationCalendar
|
||||
? [eventType.destinationCalendar]
|
||||
: organizerUser.destinationCalendar
|
||||
? [organizerUser.destinationCalendar]
|
||||
: null;
|
||||
|
||||
let evt: CalendarEvent = {
|
||||
bookerUrl,
|
||||
type: eventType.slug,
|
||||
@@ -1560,7 +1569,10 @@ async function handler(
|
||||
organizer: {
|
||||
id: organizerUser.id,
|
||||
name: organizerUser.name || "Nameless",
|
||||
email: organizerUser.email || "Email-less",
|
||||
email:
|
||||
eventType.useEventTypeDestinationCalendarEmail && destinationCalendar?.[0]?.primaryEmail
|
||||
? destinationCalendar[0].primaryEmail
|
||||
: organizerUser.email || "Email-less",
|
||||
username: organizerUser.username || undefined,
|
||||
timeZone: organizerUser.timeZone,
|
||||
language: { translate: tOrganizer, locale: organizerUser.locale ?? "en" },
|
||||
@@ -1571,11 +1583,7 @@ async function handler(
|
||||
attendees: attendeesList,
|
||||
location: bookingLocation, // Will be processed by the EventManager later.
|
||||
conferenceCredentialId,
|
||||
destinationCalendar: eventType.destinationCalendar
|
||||
? [eventType.destinationCalendar]
|
||||
: organizerUser.destinationCalendar
|
||||
? [organizerUser.destinationCalendar]
|
||||
: null,
|
||||
destinationCalendar,
|
||||
hideCalendarNotes: eventType.hideCalendarNotes,
|
||||
requiresConfirmation: !isConfirmedByDefault,
|
||||
eventTypeId: eventType.id,
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { describe, expect } from "vitest";
|
||||
|
||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { WEBSITE_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { resetTestEmails } from "@calcom/lib/testEmails";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
@@ -82,6 +82,9 @@ describe("handleNewBooking", () => {
|
||||
name: "Booker",
|
||||
});
|
||||
|
||||
const organizerOtherEmail = "organizer2@example.com";
|
||||
const organizerDestinationCalendarEmailOnEventType = "organizerEventTypeEmail@example.com";
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
@@ -92,6 +95,7 @@ describe("handleNewBooking", () => {
|
||||
destinationCalendar: {
|
||||
integration: "google_calendar",
|
||||
externalId: "organizer@google-calendar.com",
|
||||
primaryEmail: organizerOtherEmail,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -122,6 +126,7 @@ describe("handleNewBooking", () => {
|
||||
id: 1,
|
||||
slotInterval: 30,
|
||||
length: 30,
|
||||
useEventTypeDestinationCalendarEmail: true,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
@@ -130,6 +135,7 @@ describe("handleNewBooking", () => {
|
||||
destinationCalendar: {
|
||||
integration: "google_calendar",
|
||||
externalId: "event-type-1@google-calendar.com",
|
||||
primaryEmail: organizerDestinationCalendarEmailOnEventType,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -208,7 +214,11 @@ describe("handleNewBooking", () => {
|
||||
iCalUID: createdBooking.iCalUID,
|
||||
});
|
||||
|
||||
expectWorkflowToBeTriggered({ organizer, emails });
|
||||
expectWorkflowToBeTriggered({
|
||||
organizer,
|
||||
emails,
|
||||
destinationEmail: organizerDestinationCalendarEmailOnEventType,
|
||||
});
|
||||
expectSuccessfulCalendarEventCreationInCalendar(calendarMock, {
|
||||
calendarId: "event-type-1@google-calendar.com",
|
||||
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
@@ -219,12 +229,13 @@ describe("handleNewBooking", () => {
|
||||
expectSuccessfulBookingCreationEmails({
|
||||
booking: {
|
||||
uid: createdBooking.uid!,
|
||||
urlOrigin: org ? org.urlOrigin : CAL_URL,
|
||||
urlOrigin: org ? org.urlOrigin : WEBSITE_URL,
|
||||
},
|
||||
booker,
|
||||
organizer,
|
||||
emails,
|
||||
iCalUID,
|
||||
destinationEmail: organizerDestinationCalendarEmailOnEventType,
|
||||
});
|
||||
|
||||
expectBookingCreatedWebhookToHaveBeenFired({
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { describe, expect } from "vitest";
|
||||
|
||||
import { WEBAPP_URL, CAL_URL } from "@calcom/lib/constants";
|
||||
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { BookingStatus, SchedulingType } from "@calcom/prisma/enums";
|
||||
import { test } from "@calcom/web/test/fixtures/fixtures";
|
||||
import {
|
||||
createBookingScenario,
|
||||
getGoogleCalendarCredential,
|
||||
TestData,
|
||||
getOrganizer,
|
||||
getBooker,
|
||||
getScenarioData,
|
||||
mockSuccessfulVideoMeetingCreation,
|
||||
mockCalendarToHaveNoBusySlots,
|
||||
getDate,
|
||||
getGoogleCalendarCredential,
|
||||
getOrganizer,
|
||||
getScenarioData,
|
||||
mockCalendarToHaveNoBusySlots,
|
||||
mockSuccessfulVideoMeetingCreation,
|
||||
TestData,
|
||||
Timezones,
|
||||
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
||||
import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest";
|
||||
import {
|
||||
// expectWorkflowToBeTriggered,
|
||||
expectSuccessfulBookingCreationEmails,
|
||||
expectBookingToBeInDatabase,
|
||||
expectBookingCreatedWebhookToHaveBeenFired,
|
||||
expectBookingToBeInDatabase, // expectWorkflowToBeTriggered,
|
||||
expectSuccessfulBookingCreationEmails,
|
||||
expectSuccessfulCalendarEventCreationInCalendar,
|
||||
} from "@calcom/web/test/utils/bookingScenario/expects";
|
||||
import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking";
|
||||
@@ -211,7 +210,7 @@ describe("handleNewBooking", () => {
|
||||
booking: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
uid: createdBookings[0].uid!,
|
||||
urlOrigin: CAL_URL,
|
||||
urlOrigin: WEBSITE_URL,
|
||||
},
|
||||
organizer,
|
||||
emails,
|
||||
@@ -559,7 +558,7 @@ describe("handleNewBooking", () => {
|
||||
booking: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
uid: createdBookings[0].uid!,
|
||||
urlOrigin: CAL_URL,
|
||||
urlOrigin: WEBSITE_URL,
|
||||
},
|
||||
organizer,
|
||||
emails,
|
||||
@@ -773,7 +772,7 @@ describe("handleNewBooking", () => {
|
||||
booking: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
uid: createdBookings[0].uid!,
|
||||
urlOrigin: CAL_URL,
|
||||
urlOrigin: WEBSITE_URL,
|
||||
},
|
||||
booker,
|
||||
organizer,
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ import { describe, expect } from "vitest";
|
||||
|
||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import { OrganizerDefaultConferencingAppType } from "@calcom/app-store/locations";
|
||||
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
@@ -1486,7 +1486,7 @@ describe("handleNewBooking", () => {
|
||||
booking: {
|
||||
uid: createdBooking.uid!,
|
||||
// All booking links are of WEBAPP_URL and not of the org because the team isn't part of the org
|
||||
urlOrigin: CAL_URL,
|
||||
urlOrigin: WEBSITE_URL,
|
||||
},
|
||||
booker,
|
||||
organizer,
|
||||
|
||||
@@ -73,7 +73,7 @@ function AdminOrgTable() {
|
||||
});
|
||||
};
|
||||
|
||||
const [orgToDelete, setOrgToDelete] = useState<number | null>(null);
|
||||
const [orgToDelete, setOrgToDelete] = useState<(typeof data)[number] | null>(null);
|
||||
return (
|
||||
<div>
|
||||
<Table>
|
||||
@@ -189,7 +189,7 @@ function AdminOrgTable() {
|
||||
id: "delete",
|
||||
label: t("delete"),
|
||||
onClick: () => {
|
||||
setOrgToDelete(org.id);
|
||||
setOrgToDelete(org);
|
||||
},
|
||||
icon: Trash,
|
||||
},
|
||||
@@ -202,12 +202,12 @@ function AdminOrgTable() {
|
||||
</Body>
|
||||
</Table>
|
||||
<DeleteOrgDialog
|
||||
orgId={orgToDelete}
|
||||
org={orgToDelete}
|
||||
onClose={() => setOrgToDelete(null)}
|
||||
onConfirm={() => {
|
||||
if (!orgToDelete) return;
|
||||
deleteMutation.mutate({
|
||||
orgId: orgToDelete,
|
||||
orgId: orgToDelete.id,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@@ -232,28 +232,35 @@ AdminOrgList.getLayout = getLayout;
|
||||
export default AdminOrgList;
|
||||
|
||||
const DeleteOrgDialog = ({
|
||||
orgId,
|
||||
org,
|
||||
onConfirm,
|
||||
onClose,
|
||||
}: {
|
||||
orgId: number | null;
|
||||
org: {
|
||||
id: number;
|
||||
name: string;
|
||||
} | null;
|
||||
onConfirm: () => void;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
if (!org) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function -- noop
|
||||
<Dialog name="delete-user" open={!!orgId} onOpenChange={(open) => (open ? () => {} : onClose())}>
|
||||
<Dialog name="delete-user" open={!!org.id} onOpenChange={(open) => (open ? () => {} : onClose())}>
|
||||
<ConfirmationDialogContent
|
||||
title={t("admin_delete_organization_title")}
|
||||
title={t("admin_delete_organization_title", {
|
||||
organizationName: org.name,
|
||||
})}
|
||||
confirmBtnText={t("delete")}
|
||||
cancelBtnText={t("cancel")}
|
||||
variety="danger"
|
||||
onConfirm={onConfirm}>
|
||||
<Trans
|
||||
i18nKey="admin_delete_organization_description"
|
||||
components={{ li: <li />, ul: <ul className="ml-4 list-disc" /> }}>
|
||||
components={{ li: <li />, ul: <ul className="ml-4 mt-5 list-disc space-y-2" /> }}>
|
||||
<ul>
|
||||
<li>
|
||||
Teams that are member of this organization will also be deleted along with their event-types
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"@sendgrid/mail": "^7.6.2",
|
||||
"libphonenumber-js": "^1.10.51",
|
||||
"twilio": "^3.80.1",
|
||||
"zod": "^3.22.2"
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/tsconfig": "*"
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useEffect, useState } from "react";
|
||||
|
||||
import getStripe from "@calcom/app-store/stripepayment/lib/client";
|
||||
import { getBookingRedirectExtraParams, useBookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, CheckboxField } from "@calcom/ui";
|
||||
@@ -48,7 +48,7 @@ type States =
|
||||
|
||||
const getReturnUrl = (props: Props) => {
|
||||
if (!props.eventType.successRedirectUrl) {
|
||||
return `${CAL_URL}/booking/${props.booking.uid}`;
|
||||
return `${WEBAPP_URL}/booking/${props.booking.uid}`;
|
||||
}
|
||||
|
||||
const returnUrl = new URL(props.eventType.successRedirectUrl);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useIntercom as useIntercomLib } from "react-use-intercom";
|
||||
import { z } from "zod";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useHasTeamPlan, useHasPaidPlan } from "@calcom/lib/hooks/useHasPaidPlan";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
@@ -44,7 +44,7 @@ export const useIntercom = () => {
|
||||
customAttributes: {
|
||||
//keys should be snake cased
|
||||
user_name: data?.username,
|
||||
link: `${CAL_URL}/${data?.username}`,
|
||||
link: `${WEBSITE_URL}/${data?.username}`,
|
||||
identity_provider: data?.identityProvider,
|
||||
timezone: data?.timeZone,
|
||||
locale: data?.locale,
|
||||
|
||||
@@ -67,9 +67,7 @@ const authedAdminProcedureWithRequestedUser = authedAdminProcedure.use(async ({
|
||||
return next({
|
||||
ctx: {
|
||||
user: ctx.user,
|
||||
requestedUser:
|
||||
/** Don't leak the password */
|
||||
exclude(user, ["password"]),
|
||||
requestedUser: user,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -84,8 +82,7 @@ export const userAdminRouter = router({
|
||||
// TODO: Add search, pagination, etc.
|
||||
const users = await prisma.user.findMany();
|
||||
return users.map((user) => ({
|
||||
/** Don't leak the password */
|
||||
...exclude(user, ["password"]),
|
||||
...user,
|
||||
/**
|
||||
* FIXME: This should be either a prisma extension or middleware
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/middleware
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
@@ -24,8 +23,6 @@ import {
|
||||
} from "@calcom/ui";
|
||||
import { Edit2, Link as LinkIcon, MoreHorizontal, Trash2 } from "@calcom/ui/components/icon";
|
||||
|
||||
import { useOrgBranding } from "../../organizations/context/provider";
|
||||
import { subdomainSuffix } from "../../organizations/lib/orgDomains";
|
||||
import { getActionIcon } from "../lib/getActionIcon";
|
||||
import { DeleteDialog } from "./DeleteDialog";
|
||||
|
||||
@@ -61,9 +58,6 @@ export default function WorkflowListPage({ workflows }: Props) {
|
||||
const [parent] = useAutoAnimate<HTMLUListElement>();
|
||||
const router = useRouter();
|
||||
|
||||
const orgBranding = useOrgBranding();
|
||||
const urlPrefix = orgBranding ? `${orgBranding.slug}.${subdomainSuffix()}` : CAL_URL;
|
||||
|
||||
const mutation = trpc.viewer.workflowOrder.useMutation({
|
||||
onError: async (err) => {
|
||||
console.error(err.message);
|
||||
|
||||
@@ -18,7 +18,7 @@ import { useTimePreferences } from "@calcom/features/bookings/lib/timePreference
|
||||
import DatePicker from "@calcom/features/calendars/DatePicker";
|
||||
import { useNonEmptyScheduleDays } from "@calcom/features/schedules";
|
||||
import { useSlotsForDate } from "@calcom/features/schedules/lib/use-schedule/useSlotsForDate";
|
||||
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
||||
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { weekdayToWeekIndex } from "@calcom/lib/date-fns";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -400,7 +400,7 @@ const EmailEmbedPreview = ({
|
||||
<tr style={{ height: "25px" }}>
|
||||
{selectedDateAndTime[key]?.length > 0 &&
|
||||
selectedDateAndTime[key].map((time) => {
|
||||
const bookingURL = `${CAL_URL}/${username}/${eventType.slug}?duration=${eventType.length}&date=${key}&month=${month}&slot=${time}`;
|
||||
const bookingURL = `${WEBSITE_URL}/${username}/${eventType.slug}?duration=${eventType.length}&date=${key}&month=${month}&slot=${time}`;
|
||||
return (
|
||||
<td
|
||||
key={time}
|
||||
@@ -463,7 +463,7 @@ const EmailEmbedPreview = ({
|
||||
<div style={{ marginTop: "13px" }}>
|
||||
<a
|
||||
className="more"
|
||||
href={`${CAL_URL}/${username}/${eventType.slug}`}
|
||||
href={`${WEBSITE_URL}/${username}/${eventType.slug}`}
|
||||
style={{
|
||||
textDecoration: "none",
|
||||
cursor: "pointer",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CAL_URL, IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { WEBSITE_URL, IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
import type { PreviewState } from "../types";
|
||||
import { embedLibUrl } from "./constants";
|
||||
@@ -7,8 +7,8 @@ import { getDimension } from "./getDimension";
|
||||
|
||||
export const doWeNeedCalOriginProp = (embedCalOrigin: string) => {
|
||||
// If we are self hosted, calOrigin won't be app.cal.com so we need to pass it
|
||||
// If we are not self hosted but it's still different from WEBAPP_URL and CAL_URL, we need to pass it -> It happens for organization booking URL at the moment
|
||||
return IS_SELF_HOSTED || (embedCalOrigin !== WEBAPP_URL && embedCalOrigin !== CAL_URL);
|
||||
// If we are not self hosted but it's still different from WEBAPP_URL and WEBSITE_URL, we need to pass it -> It happens for organization booking URL at the moment
|
||||
return IS_SELF_HOSTED || (embedCalOrigin !== WEBAPP_URL && embedCalOrigin !== WEBSITE_URL);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -121,7 +121,7 @@ export const Codes = {
|
||||
calLink: "${calLink}",
|
||||
layout: "${previewState.layout}"
|
||||
});
|
||||
|
||||
|
||||
${uiInstructionCode}`;
|
||||
},
|
||||
|
||||
@@ -155,7 +155,7 @@ export const Codes = {
|
||||
// \`data-cal-config='${JSON.stringify({
|
||||
layout: previewState.layout,
|
||||
})}'\`
|
||||
|
||||
|
||||
${uiInstructionCode}`;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import type { Props } from "react-select";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { Avatar, Badge, Button, ButtonGroup, Select, Switch, Tooltip } from "@calcom/ui";
|
||||
@@ -106,7 +106,7 @@ export const ChildrenEventTypeSelect = ({
|
||||
color="secondary"
|
||||
target="_blank"
|
||||
variant="icon"
|
||||
href={`${CAL_URL}/${children.owner?.username}/${children.slug}`}
|
||||
href={`${WEBSITE_URL}/${children.owner?.username}/${children.slug}`}
|
||||
StartIcon={ExternalLink}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -14,7 +13,7 @@ import { HttpError } from "@calcom/lib/http-error";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import turndown from "@calcom/lib/turndownService";
|
||||
import { SchedulingType, MembershipRole } from "@calcom/prisma/enums";
|
||||
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
||||
import { unlockedManagedEventTypeProps } from "@calcom/prisma/zod-utils";
|
||||
import { createEventTypeInput } from "@calcom/prisma/zod/custom/eventtype";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
@@ -24,12 +23,12 @@ import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
Editor,
|
||||
Form,
|
||||
RadioGroup as RadioArea,
|
||||
showToast,
|
||||
TextField,
|
||||
Editor,
|
||||
DialogFooter,
|
||||
} from "@calcom/ui";
|
||||
|
||||
// this describes the uniform data needed to create a new event type on Profile or Team
|
||||
|
||||
@@ -17,9 +17,10 @@ import { filterQuerySchema } from "../lib/getTeamsFiltersFromQuery";
|
||||
export type IEventTypesFilters = RouterOutputs["viewer"]["eventTypes"]["listWithTeam"];
|
||||
export type IEventTypeFilter = IEventTypesFilters[0];
|
||||
|
||||
export const TEAMS_FILTER_KEY = "TEAMS_FILTER_KEY";
|
||||
function useFilterQuery() {
|
||||
// passthrough allows additional params to not be removed
|
||||
return useTypedQuery(filterQuerySchema.passthrough());
|
||||
return useTypedQuery(filterQuerySchema.passthrough(), TEAMS_FILTER_KEY);
|
||||
}
|
||||
|
||||
export const TeamsFilter = ({
|
||||
|
||||
@@ -31,7 +31,7 @@ type BookerLayoutSelectorProps = {
|
||||
* to this boolean.
|
||||
*/
|
||||
isDark?: boolean;
|
||||
|
||||
isLoading?: boolean;
|
||||
isDisabled?: boolean;
|
||||
isOuterBorder?: boolean;
|
||||
};
|
||||
@@ -46,6 +46,7 @@ export const BookerLayoutSelector = ({
|
||||
isDark,
|
||||
isDisabled = false,
|
||||
isOuterBorder = false,
|
||||
isLoading = false,
|
||||
}: BookerLayoutSelectorProps) => {
|
||||
const { control, getValues } = useFormContext();
|
||||
const { t } = useLocale();
|
||||
@@ -79,7 +80,7 @@ export const BookerLayoutSelector = ({
|
||||
/>
|
||||
{!isOuterBorder && (
|
||||
<SectionBottomActions align="end">
|
||||
<Button type="submit" disabled={isDisabled} color="primary">
|
||||
<Button loading={isLoading} type="submit" disabled={isDisabled} color="primary">
|
||||
{t("update")}
|
||||
</Button>
|
||||
</SectionBottomActions>
|
||||
|
||||
+10
-10
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
import { CAL_URL, LOGO } from "./constants";
|
||||
import { LOGO, WEBAPP_URL } from "./constants";
|
||||
|
||||
// Ensures tw prop is typed.
|
||||
declare module "react" {
|
||||
@@ -62,7 +62,7 @@ export const constructMeetingImage = (
|
||||
`?type=meeting`,
|
||||
`&title=${encodeURIComponent(title)}`,
|
||||
`&meetingProfileName=${encodeURIComponent(profile.name)}`,
|
||||
profile.image && `&meetingImage=${encodeURIComponent(CAL_URL + profile.image)}`,
|
||||
profile.image && `&meetingImage=${encodeURIComponent(WEBAPP_URL + profile.image)}`,
|
||||
`${users.map((user) => `&names=${encodeURIComponent(user.name)}`).join("")}`,
|
||||
`${users.map((user) => `&usernames=${encodeURIComponent(user.username)}`).join("")}`,
|
||||
// Joining a multiline string for readability.
|
||||
@@ -103,7 +103,7 @@ const Wrapper = ({ children, variant = "light", rotateBackground }: WrapperProps
|
||||
<img
|
||||
tw="flex absolute left-0 top-0 w-full h-[110%]"
|
||||
style={rotateBackground ? { transform: "rotate(180deg)" } : undefined}
|
||||
src={`${CAL_URL}/social-bg-${variant}-lines.jpg`}
|
||||
src={`${WEBAPP_URL}/social-bg-${variant}-lines.jpg`}
|
||||
alt="background"
|
||||
width="1200"
|
||||
height="600"
|
||||
@@ -125,7 +125,7 @@ export const Meeting = ({ title, users = [], profile }: MeetingImageProps) => {
|
||||
const avatars = attendees
|
||||
.map((user) => {
|
||||
if ("image" in user && user?.image) return user.image;
|
||||
if ("username" in user && user?.username) return `${CAL_URL}/${user.username}/avatar.png`;
|
||||
if ("username" in user && user?.username) return `${WEBAPP_URL}/${user.username}/avatar.png`;
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean) as string[];
|
||||
@@ -138,7 +138,7 @@ export const Meeting = ({ title, users = [], profile }: MeetingImageProps) => {
|
||||
<Wrapper variant="dark">
|
||||
<div tw="h-full flex flex-col justify-start">
|
||||
<div tw="flex items-center justify-center" style={{ fontFamily: "cal", fontWeight: 300 }}>
|
||||
<img src={`${CAL_URL}/${LOGO}`} width="350" alt="Logo" />
|
||||
<img src={`${WEBAPP_URL}/${LOGO}`} width="350" alt="Logo" />
|
||||
{avatars.length > 0 && (
|
||||
<div style={{ color: "#111827" }} tw="font-bold text-[92px] mx-8 bottom-2">
|
||||
/
|
||||
@@ -191,7 +191,7 @@ const VisualBlur = ({ visualSlug }: { visualSlug: string }) => {
|
||||
style={{
|
||||
filter: "blur(98px)",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.7)",
|
||||
backgroundImage: `url(${CAL_URL}${visualSlug})`,
|
||||
backgroundImage: `url(${WEBAPP_URL}${visualSlug})`,
|
||||
backgroundSize: "400px 400px",
|
||||
}}
|
||||
/>
|
||||
@@ -202,7 +202,7 @@ const VisualBlur = ({ visualSlug }: { visualSlug: string }) => {
|
||||
style={{
|
||||
filter: "blur(150px)",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.7)",
|
||||
backgroundImage: `url(${CAL_URL}${visualSlug})`,
|
||||
backgroundImage: `url(${WEBAPP_URL}${visualSlug})`,
|
||||
backgroundSize: "630px 630px",
|
||||
}}
|
||||
/>
|
||||
@@ -212,13 +212,13 @@ const VisualBlur = ({ visualSlug }: { visualSlug: string }) => {
|
||||
|
||||
export const App = ({ name, description, slug }: AppImageProps) => (
|
||||
<Wrapper>
|
||||
<img src={`${CAL_URL}/${LOGO}`} width="150" alt="Logo" tw="absolute right-[48px] top-[48px]" />
|
||||
<img src={`${WEBAPP_URL}/${LOGO}`} width="150" alt="Logo" tw="absolute right-[48px] top-[48px]" />
|
||||
|
||||
<VisualBlur visualSlug={slug} />
|
||||
|
||||
<div tw="flex items-center w-full">
|
||||
<div tw="flex">
|
||||
<img src={`${CAL_URL}${slug}`} alt="App icon" width="172" />
|
||||
<img src={`${WEBAPP_URL}${slug}`} alt="App icon" width="172" />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ color: "#111827" }} tw="flex mt-auto w-full flex-col">
|
||||
@@ -236,7 +236,7 @@ export const Generic = ({ title, description }: GenericImageProps) => (
|
||||
<Wrapper>
|
||||
<div tw="h-full flex flex-col justify-start">
|
||||
<div tw="flex items-center justify-center" style={{ fontFamily: "cal", fontWeight: 300 }}>
|
||||
<img src={`${CAL_URL}/cal-logo-word-black.svg`} width="350" alt="Logo" />
|
||||
<img src={`${WEBAPP_URL}/cal-logo-word-black.svg`} width="350" alt="Logo" />
|
||||
</div>
|
||||
|
||||
<div style={{ color: "#111827" }} tw="relative flex text-[54px] w-full flex-col mt-auto">
|
||||
|
||||
@@ -78,7 +78,7 @@ export const SEO_IMG_DEFAULT = `${WEBSITE_URL}/og-image.png`;
|
||||
// This results in a 80% smaller image 🤯. It is however important that for the query
|
||||
// parameters you pass to the /api/social/og/image endpoint, you wrap them in encodeURIComponent
|
||||
// as well, otherwise the URL won't be valid.
|
||||
export const SEO_IMG_OGIMG = `${CAL_URL}/_next/image?w=1200&q=100&url=${encodeURIComponent(
|
||||
export const SEO_IMG_OGIMG = `${WEBAPP_URL}/_next/image?w=1200&q=100&url=${encodeURIComponent(
|
||||
"/api/social/og/image"
|
||||
)}`;
|
||||
export const SEO_IMG_OGIMG_VIDEO = `${WEBSITE_URL}/video-og-image.png`;
|
||||
|
||||
@@ -101,6 +101,7 @@ const commons = {
|
||||
metadata: EventTypeMetaDataSchema.parse({}),
|
||||
bookingFields: [],
|
||||
assignAllTeamMembers: false,
|
||||
useEventTypeDestinationCalendarEmail: false,
|
||||
};
|
||||
|
||||
const dynamicEvent = {
|
||||
|
||||
@@ -80,9 +80,13 @@ export const deleteDnsRecord = async (domain: string) => {
|
||||
assertCloudflareEnvVars(process.env);
|
||||
|
||||
const dnsRecordToDelete = await getDnsRecordToDelete();
|
||||
await deleteDnsRecord(dnsRecordToDelete);
|
||||
if (dnsRecordToDelete) {
|
||||
await deleteDnsRecord(dnsRecordToDelete);
|
||||
log.info(`Deleted dns-record in Cloudflare: ${domain}`);
|
||||
} else {
|
||||
log.info(`CNAME not found in Cloudflare: ${domain}. Nothing to delete`);
|
||||
}
|
||||
|
||||
log.info(`Deleted dns-record in Cloudflare: ${domain}`);
|
||||
return true;
|
||||
|
||||
async function getDnsRecordToDelete() {
|
||||
@@ -134,7 +138,7 @@ export const deleteDnsRecord = async (domain: string) => {
|
||||
});
|
||||
}
|
||||
|
||||
return searchResult.result[0];
|
||||
return searchResult.result[0] as (typeof searchResult.result)[0] | null;
|
||||
}
|
||||
|
||||
async function deleteDnsRecord(dnsRecordToDelete: { id: string }) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { subdomainSuffix } from "@calcom/ee/organizations/lib/orgDomains";
|
||||
import logger from "@calcom/lib/logger";
|
||||
|
||||
import { deleteDnsRecord, addDnsRecord } from "./deploymentServices/cloudflare";
|
||||
import {
|
||||
@@ -6,6 +7,7 @@ import {
|
||||
createDomain as createVercelDomain,
|
||||
} from "./deploymentServices/vercel";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["domainManager/organization"] });
|
||||
export const deleteDomain = async (slug: string) => {
|
||||
const domain = `${slug}.${subdomainSuffix()}`;
|
||||
// We must have some domain deleted
|
||||
@@ -22,6 +24,7 @@ export const deleteDomain = async (slug: string) => {
|
||||
isDnsRecordDeleted = await deleteDnsRecord(domain);
|
||||
}
|
||||
return isDomainDeleted && isDnsRecordDeleted;
|
||||
return false;
|
||||
};
|
||||
|
||||
export const createDomain = async (slug: string) => {
|
||||
@@ -48,6 +51,10 @@ export const renameDomain = async (oldSlug: string | null, newSlug: string) => {
|
||||
// First create new domain so that if it fails we still have the old domain
|
||||
await createDomain(newSlug);
|
||||
if (oldSlug) {
|
||||
await deleteDomain(oldSlug);
|
||||
try {
|
||||
await deleteDomain(oldSlug);
|
||||
} catch (e) {
|
||||
log.error(`renameDomain: Failed to delete old domain ${oldSlug}. Do a manual deletion if needed`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { CAL_URL } from "../constants";
|
||||
import { WEBSITE_URL } from "../constants";
|
||||
import { getBrand } from "../server/getBrand";
|
||||
|
||||
export const getBookerBaseUrl = async (organizationId: number | null) => {
|
||||
const orgBrand = await getBrand(organizationId);
|
||||
return orgBrand?.fullDomain ?? CAL_URL;
|
||||
return orgBrand?.fullDomain ?? WEBSITE_URL;
|
||||
};
|
||||
|
||||
export const getTeamBookerUrl = async (team: { organizationId: number | null }) => {
|
||||
const orgBrand = await getBrand(team.organizationId);
|
||||
return orgBrand?.fullDomain ?? CAL_URL;
|
||||
return orgBrand?.fullDomain ?? WEBSITE_URL;
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-u
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { CAL_URL } from "./constants";
|
||||
import { WEBSITE_URL } from "./constants";
|
||||
import { getBookerBaseUrl } from "./getBookerUrl/server";
|
||||
|
||||
interface getEventTypeByIdProps {
|
||||
@@ -112,6 +112,7 @@ export default async function getEventTypeById({
|
||||
successRedirectUrl: true,
|
||||
currency: true,
|
||||
bookingFields: true,
|
||||
useEventTypeDestinationCalendarEmail: true,
|
||||
owner: {
|
||||
select: {
|
||||
id: true,
|
||||
@@ -304,7 +305,7 @@ export default async function getEventTypeById({
|
||||
? await getBookerBaseUrl(restEventType.team.parentId)
|
||||
: restEventType.owner
|
||||
? await getBookerBaseUrl(currentOrganizationId)
|
||||
: CAL_URL,
|
||||
: WEBSITE_URL,
|
||||
children: childrenWithUserProfile.flatMap((ch) =>
|
||||
ch.owner !== null
|
||||
? {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
|
||||
export const useBookerUrl = () => {
|
||||
const orgBranding = useOrgBranding();
|
||||
return orgBranding?.fullDomain ?? CAL_URL ?? WEBAPP_URL;
|
||||
return orgBranding?.fullDomain ?? WEBSITE_URL ?? WEBAPP_URL;
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { usePathname, useRouter } from "next/navigation";
|
||||
import { useCallback, useMemo, useEffect } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
import { localStorage } from "../webstorage";
|
||||
import { useRouterQuery } from "./useRouterQuery";
|
||||
|
||||
type OptionalKeys<T> = {
|
||||
@@ -32,7 +33,7 @@ export const queryStringArray = z
|
||||
.preprocess((a) => z.string().parse(a).split(","), z.string().array())
|
||||
.or(z.string().array());
|
||||
|
||||
export function useTypedQuery<T extends z.AnyZodObject>(schema: T) {
|
||||
export function useTypedQuery<T extends z.AnyZodObject>(schema: T, localStorageKey: string | null = null) {
|
||||
type Output = z.infer<typeof schema>;
|
||||
type FullOutput = Required<Output>;
|
||||
type OutputKeys = Required<keyof FullOutput>;
|
||||
@@ -43,7 +44,7 @@ export function useTypedQuery<T extends z.AnyZodObject>(schema: T) {
|
||||
const unparsedQuery = useRouterQuery();
|
||||
const pathname = usePathname();
|
||||
const parsedQuerySchema = schema.safeParse(unparsedQuery);
|
||||
|
||||
const useLocalStorage = !!localStorageKey;
|
||||
let parsedQuery: Output = useMemo(() => {
|
||||
return {} as Output;
|
||||
}, []);
|
||||
@@ -73,6 +74,39 @@ export function useTypedQuery<T extends z.AnyZodObject>(schema: T) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[parsedQuery, router]
|
||||
);
|
||||
//If the value for {localStorageKey} exist in localStorage set it to queryParams
|
||||
useEffect(() => {
|
||||
if (!useLocalStorage) return;
|
||||
const storedValueString = localStorage.getItem(localStorageKey);
|
||||
if (!storedValueString || storedValueString == "{}") return;
|
||||
const storedValue: Output = JSON.parse(storedValueString);
|
||||
if (storedValue) {
|
||||
const searchParams = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(storedValue)) {
|
||||
searchParams.set(String(key), String(value));
|
||||
}
|
||||
router.replace(`${pathname}?${searchParams.toString()}`);
|
||||
}
|
||||
}, []);
|
||||
|
||||
function setLocalStorage<J extends OutputKeys>(key: J, value: Output[J]) {
|
||||
if (!useLocalStorage) return;
|
||||
const storedValue: Output = JSON.parse(localStorage.getItem(localStorageKey) || "{}");
|
||||
const newValue = { ...storedValue, [key]: value };
|
||||
localStorage.setItem(localStorageKey, JSON.stringify(newValue));
|
||||
}
|
||||
function removeByKeyFromLocalStorage(key: OutputOptionalKeys) {
|
||||
if (!useLocalStorage) return;
|
||||
const storedValue: Output = JSON.parse(localStorage.getItem(localStorageKey) || "{}");
|
||||
const newValue = storedValue;
|
||||
delete newValue[key];
|
||||
localStorage.setItem(localStorageKey, JSON.stringify(newValue));
|
||||
}
|
||||
//Remove {localStorageKey} from the localStorage
|
||||
function removeAllValuesFromLocalStorage() {
|
||||
if (!useLocalStorage) return;
|
||||
localStorage.removeItem(localStorageKey);
|
||||
}
|
||||
|
||||
// Delete a key from the query
|
||||
function removeByKey(key: OutputOptionalKeys) {
|
||||
@@ -88,9 +122,13 @@ export function useTypedQuery<T extends z.AnyZodObject>(schema: T) {
|
||||
if (existingValue.includes(value)) return; // prevent adding the same value to the array
|
||||
// @ts-expect-error this is too much for TS it seems
|
||||
setQuery(key, [...existingValue, value]);
|
||||
// @ts-expect-error this is too much for TS it seems
|
||||
setLocalStorage(key, [...existingValue, value]);
|
||||
} else {
|
||||
// @ts-expect-error this is too much for TS it seems
|
||||
setQuery(key, [value]);
|
||||
// @ts-expect-error this is too much for TS it seems
|
||||
setLocalStorage(key, [value]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,9 +139,12 @@ export function useTypedQuery<T extends z.AnyZodObject>(schema: T) {
|
||||
// @ts-expect-error this is too much for TS it seems
|
||||
const newValue = existingValue.filter((item) => item !== value);
|
||||
setQuery(key, newValue);
|
||||
setLocalStorage(key, newValue);
|
||||
} else {
|
||||
// @ts-expect-error this is too much for TS it seems
|
||||
removeByKey(key);
|
||||
//@ts-expect-error this is too much for TS it seems
|
||||
removeByKeyFromLocalStorage(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +152,7 @@ export function useTypedQuery<T extends z.AnyZodObject>(schema: T) {
|
||||
function removeAllQueryParams() {
|
||||
if (pathname !== null) {
|
||||
router.replace(pathname);
|
||||
removeAllValuesFromLocalStorage();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ export async function getBooking(bookingId: number) {
|
||||
bookingFields: booking.eventType?.bookingFields || null,
|
||||
}),
|
||||
organizer: {
|
||||
email: user.email,
|
||||
email: booking?.userPrimaryEmail ?? user.email,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
name: user.name!,
|
||||
timeZone: user.timeZone,
|
||||
|
||||
@@ -38,6 +38,7 @@ export const buildBooking = (booking?: Partial<Booking>): Booking => {
|
||||
uid,
|
||||
userId: null,
|
||||
eventTypeId: null,
|
||||
userPrimaryEmail: null,
|
||||
title: faker.lorem.sentence(),
|
||||
description: faker.lorem.paragraph(),
|
||||
customInputs: null,
|
||||
@@ -82,6 +83,7 @@ export const buildEventType = (eventType?: Partial<EventType>): EventType => {
|
||||
userId: null,
|
||||
teamId: null,
|
||||
requiresBookerEmailVerification: false,
|
||||
useEventTypeDestinationCalendarEmail: false,
|
||||
eventName: faker.lorem.words(),
|
||||
timeZone: null,
|
||||
periodType: "UNLIMITED",
|
||||
@@ -161,7 +163,10 @@ export const buildSubscriberEvent = (booking?: Partial<Booking>) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const buildCalendarEvent = (event?: Partial<CalendarEvent>): CalendarEvent => {
|
||||
export const buildCalendarEvent = (
|
||||
event?: Partial<CalendarEvent>,
|
||||
omitVideoCallData?: boolean
|
||||
): CalendarEvent => {
|
||||
const uid = faker.datatype.uuid();
|
||||
return {
|
||||
uid,
|
||||
@@ -176,7 +181,7 @@ export const buildCalendarEvent = (event?: Partial<CalendarEvent>): CalendarEven
|
||||
customInputs: {},
|
||||
additionalNotes: faker.lorem.paragraph(),
|
||||
organizer: buildPerson(),
|
||||
videoCallData: buildVideoCallData(),
|
||||
...(!omitVideoCallData && { videoCallData: buildVideoCallData() }),
|
||||
...event,
|
||||
};
|
||||
};
|
||||
@@ -224,7 +229,6 @@ export const buildUser = <T extends Partial<UserPayload>>(
|
||||
invitedTo: null,
|
||||
locale: "en",
|
||||
metadata: null,
|
||||
password: null,
|
||||
role: "USER",
|
||||
schedules: [],
|
||||
selectedCalendars: [],
|
||||
|
||||
@@ -56,9 +56,7 @@ export const validateAndGetCorrectedUsernameAndEmail = async ({
|
||||
{
|
||||
OR: [
|
||||
{ emailVerified: { not: null } },
|
||||
{
|
||||
AND: [{ password: { not: null } }, { username: { not: null } }],
|
||||
},
|
||||
{ AND: [{ password: { isNot: null } }, { username: { not: null } }] },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "DestinationCalendar" ADD COLUMN "primaryEmail" TEXT;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Booking" ADD COLUMN "userPrimaryEmail" TEXT;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "EventType" ADD COLUMN "useEventTypeDestinationCalendarEmail" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `password` on the `users` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- CreateTable
|
||||
CREATE TABLE
|
||||
"UserPassword" (
|
||||
"hash" TEXT NOT NULL,
|
||||
"userId" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "UserPassword_userId_key" ON "UserPassword" ("userId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UserPassword" ADD CONSTRAINT "UserPassword_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- Inserts current user password into new table
|
||||
INSERT INTO
|
||||
"UserPassword" ("hash", "userId")
|
||||
SELECT
|
||||
u."password",
|
||||
u.id
|
||||
FROM
|
||||
users u
|
||||
WHERE
|
||||
u."password" IS NOT NULL;
|
||||
|
||||
-- TODO for next migration.
|
||||
-- ALTER TABLE "users" DROP COLUMN "password";
|
||||
@@ -30,7 +30,7 @@
|
||||
"@prisma/generator-helper": "^5.4.2",
|
||||
"prisma": "^5.4.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"zod": "^3.22.2",
|
||||
"zod": "^3.22.4",
|
||||
"zod-prisma": "^0.5.4"
|
||||
},
|
||||
"main": "index.ts",
|
||||
|
||||
@@ -70,62 +70,63 @@ model EventType {
|
||||
profileId Int?
|
||||
profile Profile? @relation(fields: [profileId], references: [id], onDelete: Cascade)
|
||||
|
||||
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
teamId Int?
|
||||
hashedLink HashedLink?
|
||||
bookings Booking[]
|
||||
availability Availability[]
|
||||
webhooks Webhook[]
|
||||
destinationCalendar DestinationCalendar?
|
||||
eventName String?
|
||||
customInputs EventTypeCustomInput[]
|
||||
parentId Int?
|
||||
parent EventType? @relation("managed_eventtype", fields: [parentId], references: [id], onDelete: Cascade)
|
||||
children EventType[] @relation("managed_eventtype")
|
||||
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
teamId Int?
|
||||
hashedLink HashedLink?
|
||||
bookings Booking[]
|
||||
availability Availability[]
|
||||
webhooks Webhook[]
|
||||
destinationCalendar DestinationCalendar?
|
||||
eventName String?
|
||||
customInputs EventTypeCustomInput[]
|
||||
parentId Int?
|
||||
parent EventType? @relation("managed_eventtype", fields: [parentId], references: [id], onDelete: Cascade)
|
||||
children EventType[] @relation("managed_eventtype")
|
||||
/// @zod.custom(imports.eventTypeBookingFields)
|
||||
bookingFields Json?
|
||||
timeZone String?
|
||||
periodType PeriodType @default(UNLIMITED)
|
||||
bookingFields Json?
|
||||
timeZone String?
|
||||
periodType PeriodType @default(UNLIMITED)
|
||||
/// @zod.custom(imports.coerceToDate)
|
||||
periodStartDate DateTime?
|
||||
periodStartDate DateTime?
|
||||
/// @zod.custom(imports.coerceToDate)
|
||||
periodEndDate DateTime?
|
||||
periodDays Int?
|
||||
periodCountCalendarDays Boolean?
|
||||
lockTimeZoneToggleOnBookingPage Boolean @default(false)
|
||||
requiresConfirmation Boolean @default(false)
|
||||
requiresBookerEmailVerification Boolean @default(false)
|
||||
periodEndDate DateTime?
|
||||
periodDays Int?
|
||||
periodCountCalendarDays Boolean?
|
||||
lockTimeZoneToggleOnBookingPage Boolean @default(false)
|
||||
requiresConfirmation Boolean @default(false)
|
||||
requiresBookerEmailVerification Boolean @default(false)
|
||||
/// @zod.custom(imports.recurringEventType)
|
||||
recurringEvent Json?
|
||||
disableGuests Boolean @default(false)
|
||||
hideCalendarNotes Boolean @default(false)
|
||||
recurringEvent Json?
|
||||
disableGuests Boolean @default(false)
|
||||
hideCalendarNotes Boolean @default(false)
|
||||
/// @zod.min(0)
|
||||
minimumBookingNotice Int @default(120)
|
||||
beforeEventBuffer Int @default(0)
|
||||
afterEventBuffer Int @default(0)
|
||||
seatsPerTimeSlot Int?
|
||||
onlyShowFirstAvailableSlot Boolean @default(false)
|
||||
seatsShowAttendees Boolean? @default(false)
|
||||
seatsShowAvailabilityCount Boolean? @default(true)
|
||||
schedulingType SchedulingType?
|
||||
schedule Schedule? @relation(fields: [scheduleId], references: [id])
|
||||
scheduleId Int?
|
||||
minimumBookingNotice Int @default(120)
|
||||
beforeEventBuffer Int @default(0)
|
||||
afterEventBuffer Int @default(0)
|
||||
seatsPerTimeSlot Int?
|
||||
onlyShowFirstAvailableSlot Boolean @default(false)
|
||||
seatsShowAttendees Boolean? @default(false)
|
||||
seatsShowAvailabilityCount Boolean? @default(true)
|
||||
schedulingType SchedulingType?
|
||||
schedule Schedule? @relation(fields: [scheduleId], references: [id])
|
||||
scheduleId Int?
|
||||
// price is deprecated. It has now moved to metadata.apps.stripe.price. Plan to drop this column.
|
||||
price Int @default(0)
|
||||
price Int @default(0)
|
||||
// currency is deprecated. It has now moved to metadata.apps.stripe.currency. Plan to drop this column.
|
||||
currency String @default("usd")
|
||||
slotInterval Int?
|
||||
currency String @default("usd")
|
||||
slotInterval Int?
|
||||
/// @zod.custom(imports.EventTypeMetaDataSchema)
|
||||
metadata Json?
|
||||
metadata Json?
|
||||
/// @zod.custom(imports.successRedirectUrl)
|
||||
successRedirectUrl String?
|
||||
workflows WorkflowsOnEventTypes[]
|
||||
successRedirectUrl String?
|
||||
workflows WorkflowsOnEventTypes[]
|
||||
/// @zod.custom(imports.intervalLimitsType)
|
||||
bookingLimits Json?
|
||||
bookingLimits Json?
|
||||
/// @zod.custom(imports.intervalLimitsType)
|
||||
durationLimits Json?
|
||||
isInstantEvent Boolean @default(false)
|
||||
assignAllTeamMembers Boolean @default(false)
|
||||
durationLimits Json?
|
||||
isInstantEvent Boolean @default(false)
|
||||
assignAllTeamMembers Boolean @default(false)
|
||||
useEventTypeDestinationCalendarEmail Boolean @default(false)
|
||||
|
||||
@@unique([userId, slug])
|
||||
@@unique([teamId, slug])
|
||||
@@ -173,6 +174,8 @@ model DestinationCalendar {
|
||||
id Int @id @default(autoincrement())
|
||||
integration String
|
||||
externalId String
|
||||
/// @zod.email()
|
||||
primaryEmail String?
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId Int? @unique
|
||||
booking Booking[]
|
||||
@@ -191,6 +194,13 @@ enum UserPermissionRole {
|
||||
ADMIN
|
||||
}
|
||||
|
||||
// It holds the password of a User, separate from the User model to avoid leaking the password hash
|
||||
model UserPassword {
|
||||
hash String
|
||||
userId Int @unique
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
// It holds Personal Profiles of a User plus it has email, password and other core things
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
@@ -199,7 +209,7 @@ model User {
|
||||
/// @zod.email()
|
||||
email String
|
||||
emailVerified DateTime?
|
||||
password String?
|
||||
password UserPassword?
|
||||
bio String?
|
||||
avatar String?
|
||||
avatarUrl String?
|
||||
@@ -452,6 +462,9 @@ model Booking {
|
||||
uid String @unique
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId Int?
|
||||
// User's email at the time of booking
|
||||
/// @zod.email()
|
||||
userPrimaryEmail String?
|
||||
references BookingReference[]
|
||||
eventType EventType? @relation(fields: [eventTypeId], references: [id])
|
||||
eventTypeId Int?
|
||||
|
||||
@@ -78,7 +78,11 @@ async function main() {
|
||||
insightsAdmin = await prisma.user.create({
|
||||
data: {
|
||||
email: "insights@example.com",
|
||||
password: await hashPassword("insightsinsightsinsights...!"),
|
||||
password: {
|
||||
create: {
|
||||
hash: await hashPassword("insightsinsightsinsights...!"),
|
||||
},
|
||||
},
|
||||
name: "Insights Admin",
|
||||
role: "ADMIN",
|
||||
username: "insights-admin",
|
||||
@@ -102,7 +106,11 @@ async function main() {
|
||||
insightsUser = await prisma.user.create({
|
||||
data: {
|
||||
email: "insightsuser@example.com",
|
||||
password: await hashPassword("insightsuser"),
|
||||
password: {
|
||||
create: {
|
||||
hash: await hashPassword("insightsuser"),
|
||||
},
|
||||
},
|
||||
name: "Insights User",
|
||||
role: "USER",
|
||||
username: "insights-user",
|
||||
@@ -415,7 +423,11 @@ async function createPerformanceData() {
|
||||
const email = `insightsuser${timestamp}@example.com`;
|
||||
const insightsUser = {
|
||||
email,
|
||||
password: await hashPassword("insightsuser"),
|
||||
password: {
|
||||
create: {
|
||||
hash: await hashPassword("insightsuser"),
|
||||
},
|
||||
},
|
||||
name: `Insights User ${timestamp}`,
|
||||
username: `insights-user-${timestamp}`,
|
||||
completedOnboarding: true,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user