Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47e13cd2fd | ||
|
|
74ab6d209e | ||
|
|
46f0b55a45 | ||
|
|
e4893c2218 | ||
|
|
fa17139e8f | ||
|
|
1cdc97dd37 | ||
|
|
f630e8af40 | ||
|
|
fe0745121b | ||
|
|
f6d7568d0a | ||
|
|
520e7fe036 | ||
|
|
1bf50e09cd | ||
|
|
d1ca7b4bdd | ||
|
|
4e2473531f |
@@ -1,5 +1,5 @@
|
||||
import { AdminRequired } from "components/ui/AdminRequired";
|
||||
import noop from "lodash/noop";
|
||||
import { noop } from "lodash";
|
||||
import type { LinkProps } from "next/link";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
@@ -5,6 +5,8 @@ import { useState } from "react";
|
||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||
import { getEventLocationType } from "@calcom/app-store/locations";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
// TODO: Use browser locale, implement Intl in Dayjs maybe?
|
||||
import "@calcom/dayjs/locales";
|
||||
import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { formatTime } from "@calcom/lib/date-fns";
|
||||
|
||||
@@ -287,6 +287,7 @@ const BookingPage = ({
|
||||
return router.push({
|
||||
pathname: `/booking/${uid}`,
|
||||
query: {
|
||||
isSuccessBookingPage: true,
|
||||
allRemainingBookings: true,
|
||||
email: bookingForm.getValues("responses.email"),
|
||||
eventTypeSlug: eventType.slug,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
export type EventTypeDescriptionSafeProps = {
|
||||
eventType: { description: string | null };
|
||||
eventType: { description: string | null; descriptionAsSafeHTML: string | null };
|
||||
};
|
||||
|
||||
export const EventTypeDescriptionSafeHTML = ({ eventType }: EventTypeDescriptionSafeProps) => {
|
||||
const props: JSX.IntrinsicElements["div"] = { suppressHydrationWarning: true };
|
||||
// @ts-expect-error: @see packages/prisma/middleware/eventTypeDescriptionParseAndSanitize.ts
|
||||
if (eventType.description) props.dangerouslySetInnerHTML = { __html: eventType.descriptionAsSafeHTML };
|
||||
if (eventType.description)
|
||||
props.dangerouslySetInnerHTML = { __html: eventType.descriptionAsSafeHTML || "" };
|
||||
return <div {...props} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -75,7 +75,9 @@ export function CalendarSwitch(props: {
|
||||
/>
|
||||
{props.defaultSelected && (
|
||||
<Badge variant="gray">
|
||||
<FiArrowLeft className="mr-1" /> {t("adding_events_to")}
|
||||
<div className="flex">
|
||||
<FiArrowLeft className="mr-1" /> {t("adding_events_to")}
|
||||
</div>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,14 @@ import Link from "next/link";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||
import { Avatar } from "@calcom/ui";
|
||||
|
||||
type TeamType = NonNullable<TeamWithMembers>;
|
||||
type MembersType = TeamType["members"];
|
||||
type MemberType = MembersType[number];
|
||||
type MemberType = MembersType[number] & { safeBio: string | null };
|
||||
|
||||
type TeamTypeWithSafeHtml = Omit<TeamType, "members"> & { members: MemberType[] };
|
||||
|
||||
const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => {
|
||||
const { t } = useLocale();
|
||||
@@ -30,7 +31,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
||||
<>
|
||||
<div
|
||||
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
dangerouslySetInnerHTML={{ __html: md.render(member.bio || "") }}
|
||||
dangerouslySetInnerHTML={{ __html: member.safeBio || "" }}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
@@ -43,7 +44,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
||||
);
|
||||
};
|
||||
|
||||
const Members = ({ members, teamName }: { members: MembersType; teamName: string | null }) => {
|
||||
const Members = ({ members, teamName }: { members: MemberType[]; teamName: string | null }) => {
|
||||
if (!members || members.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -57,7 +58,7 @@ const Members = ({ members, teamName }: { members: MembersType; teamName: string
|
||||
);
|
||||
};
|
||||
|
||||
const Team = ({ team }: { team: TeamType }) => {
|
||||
const Team = ({ team }: { team: TeamTypeWithSafeHtml }) => {
|
||||
return (
|
||||
<div>
|
||||
<Members members={team.members} teamName={team.name} />
|
||||
|
||||
@@ -99,6 +99,9 @@ const nextConfig = {
|
||||
skipDefaultConversion: true,
|
||||
preventFullImport: true,
|
||||
},
|
||||
lodash: {
|
||||
transform: "lodash/{{member}}",
|
||||
},
|
||||
// TODO: We need to have all components in `@calcom/ui/components` in order to use this
|
||||
// "@calcom/ui": {
|
||||
// transform: "@calcom/ui/components/{{member}}",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "2.7.3",
|
||||
"version": "2.7.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -67,6 +67,7 @@
|
||||
"accept-language-parser": "^1.5.0",
|
||||
"async": "^3.2.4",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"canvas": "^2.11.0",
|
||||
"classnames": "^2.3.1",
|
||||
"dotenv-cli": "^6.0.0",
|
||||
"entities": "^4.4.0",
|
||||
@@ -77,6 +78,7 @@
|
||||
"ical.js": "^1.4.0",
|
||||
"ics": "^2.37.0",
|
||||
"jose": "^4.13.1",
|
||||
"jsdom": "^21.1.1",
|
||||
"kbar": "^0.1.0-beta.36",
|
||||
"libphonenumber-js": "^1.10.12",
|
||||
"lodash": "^4.17.21",
|
||||
|
||||
@@ -24,7 +24,7 @@ import defaultEvents, {
|
||||
} from "@calcom/lib/defaultEvents";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
import { markdownAndSanitize } from "@calcom/lib/markdownAndSanitize";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
||||
@@ -147,7 +147,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
||||
<>
|
||||
<div
|
||||
className=" dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
dangerouslySetInnerHTML={{ __html: md.render(user.bio || "") }}
|
||||
dangerouslySetInnerHTML={{ __html: props.safeBio }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -343,6 +343,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
const eventTypes = eventTypesRaw.map((eventType) => ({
|
||||
...eventType,
|
||||
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
|
||||
descriptionAsSafeHTML: markdownAndSanitize(eventType.description),
|
||||
}));
|
||||
|
||||
const isSingleUser = users.length === 1;
|
||||
@@ -352,9 +353,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
})
|
||||
: [];
|
||||
|
||||
const safeBio = markdownAndSanitize(user.bio) || "";
|
||||
|
||||
return {
|
||||
props: {
|
||||
users,
|
||||
safeBio,
|
||||
profile,
|
||||
user: {
|
||||
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { LocationObject } from "@calcom/app-store/locations";
|
||||
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { addListFormatting } from "@calcom/lib/markdownIt";
|
||||
import { markdownAndSanitize } from "@calcom/lib/markdownAndSanitize";
|
||||
import type { User } from "@calcom/prisma/client";
|
||||
|
||||
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
||||
@@ -59,7 +59,6 @@ Type.isThemeSupported = true;
|
||||
const paramsSchema = z.object({ type: z.string(), user: z.string() });
|
||||
async function getUserPageProps(context: GetStaticPropsContext) {
|
||||
// load server side dependencies
|
||||
const MarkdownIt = await import("markdown-it").then((mod) => mod.default);
|
||||
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
||||
const { privacyFilteredLocations } = await import("@calcom/app-store/locations");
|
||||
const { parseRecurringEvent } = await import("@calcom/lib/isRecurringEvent");
|
||||
@@ -125,8 +124,6 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
||||
},
|
||||
});
|
||||
|
||||
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
||||
|
||||
if (!user || !user.eventTypes.length) return { notFound: true };
|
||||
|
||||
const [eventType]: ((typeof user.eventTypes)[number] & {
|
||||
@@ -153,7 +150,7 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
||||
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
|
||||
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
|
||||
locations: privacyFilteredLocations(locations),
|
||||
descriptionAsSafeHTML: eventType.description ? addListFormatting(md.render(eventType.description)) : null,
|
||||
descriptionAsSafeHTML: markdownAndSanitize(eventType.description),
|
||||
});
|
||||
// Check if the user you are logging into has any active teams or premium user name
|
||||
const hasActiveTeam =
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
getUsernameList,
|
||||
} from "@calcom/lib/defaultEvents";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { markdownAndSanitize } from "@calcom/lib/markdownAndSanitize";
|
||||
import prisma, { bookEventTypeSelect } from "@calcom/prisma";
|
||||
import {
|
||||
customInputSchema,
|
||||
@@ -189,6 +190,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
slug: u.username,
|
||||
theme: u.theme,
|
||||
})),
|
||||
descriptionAsSafeHTML: markdownAndSanitize(eventType.description),
|
||||
};
|
||||
})[0];
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ResetPasswordRequest } from "@prisma/client";
|
||||
import debounce from "lodash/debounce";
|
||||
import { debounce } from "lodash";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { getCsrfToken } from "next-auth/react";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import debounce from "lodash/debounce";
|
||||
import { debounce } from "lodash";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { getCsrfToken } from "next-auth/react";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
|
||||
@@ -180,7 +180,7 @@ const querySchema = z.object({
|
||||
cancel: stringToBoolean,
|
||||
changes: stringToBoolean,
|
||||
reschedule: stringToBoolean,
|
||||
isSuccessBookingPage: z.string().optional(),
|
||||
isSuccessBookingPage: stringToBoolean,
|
||||
formerTime: z.string().optional(),
|
||||
email: z.string().optional(),
|
||||
seatReferenceUid: z.string().optional(),
|
||||
@@ -266,7 +266,11 @@ export default function Success(props: SuccessProps) {
|
||||
const giphyImage = giphyAppData?.thankYouPage;
|
||||
|
||||
const eventName = getEventName(eventNameObject, true);
|
||||
const needsConfirmation = eventType.requiresConfirmation && reschedule != true;
|
||||
// Confirmation can be needed in two cases as of now
|
||||
// - Event Type has require confirmation option enabled always
|
||||
// - EventType has conditionally enabled confirmation option based on how far the booking is scheduled.
|
||||
// - It's a paid event and payment is pending.
|
||||
const needsConfirmation = bookingInfo.status === BookingStatus.PENDING;
|
||||
const userIsOwner = !!(session?.user?.id && eventType.owner?.id === session.user.id);
|
||||
|
||||
const isCancelled =
|
||||
@@ -485,7 +489,7 @@ export default function Success(props: SuccessProps) {
|
||||
</>
|
||||
)}
|
||||
<div className="font-medium">{t("what")}</div>
|
||||
<div className="col-span-2 mb-6 last:mb-0">{eventName}</div>
|
||||
<div className="col-span-2 mb-6 last:mb-0">{props.bookingInfo.title}</div>
|
||||
<div className="font-medium">{t("when")}</div>
|
||||
<div className="col-span-2 mb-6 last:mb-0">
|
||||
{reschedule && !!formerTime && (
|
||||
@@ -527,7 +531,7 @@ export default function Success(props: SuccessProps) {
|
||||
</div>
|
||||
)}
|
||||
{bookingInfo?.attendees.map((attendee) => (
|
||||
<div key={attendee.name} className="mb-3 last:mb-0">
|
||||
<div key={attendee.name + attendee.email} className="mb-3 last:mb-0">
|
||||
{attendee.name && <p>{attendee.name}</p>}
|
||||
<p data-testid={`attendee-${attendee.email}`} className="text-bookinglight">
|
||||
{attendee.email}
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { LocationObject } from "@calcom/core/location";
|
||||
import { privacyFilteredLocations } from "@calcom/core/location";
|
||||
import { parseRecurringEvent } from "@calcom/lib";
|
||||
import { getWorkingHours } from "@calcom/lib/availability";
|
||||
import { markdownAndSanitize } from "@calcom/lib/markdownAndSanitize";
|
||||
import { availiblityPageEventTypeSelect } from "@calcom/prisma";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
@@ -119,6 +120,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
hideBranding: u.hideBranding,
|
||||
timeZone: u.timeZone,
|
||||
})),
|
||||
descriptionAsSafeHTML: markdownAndSanitize(hashedLink.eventType.description),
|
||||
});
|
||||
|
||||
const [user] = users;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { parseRecurringEvent } from "@calcom/lib";
|
||||
import { markdownAndSanitize } from "@calcom/lib/markdownAndSanitize";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { bookEventTypeSelect } from "@calcom/prisma/selects";
|
||||
import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
@@ -93,6 +94,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
brandColor: u.brandColor,
|
||||
darkBrandColor: u.darkBrandColor,
|
||||
})),
|
||||
descriptionAsSafeHTML: markdownAndSanitize(eventType.description),
|
||||
};
|
||||
})[0];
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
import { markdownAndSanitize } from "@calcom/lib/markdownAndSanitizeClientSide";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc, TRPCClientError } from "@calcom/trpc/react";
|
||||
import {
|
||||
@@ -141,7 +142,7 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
|
||||
</div>
|
||||
<EventTypeDescription
|
||||
// @ts-expect-error FIXME: We have a type mismatch here @hariombalhara @sean-brydon
|
||||
eventType={type}
|
||||
eventType={{ ...type, descriptionAsSafeHTML: markdownAndSanitize(type.description) }}
|
||||
shortenDescription
|
||||
/>
|
||||
</Link>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { IdentityProvider } from "@prisma/client";
|
||||
import crypto from "crypto";
|
||||
import { signOut } from "next-auth/react";
|
||||
import type { BaseSyntheticEvent } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
@@ -332,11 +331,6 @@ const ProfileForm = ({
|
||||
bio: z.string(),
|
||||
});
|
||||
|
||||
const emailMd5 = crypto
|
||||
.createHash("md5")
|
||||
.update(defaultValues.email || "example@example.com")
|
||||
.digest("hex");
|
||||
|
||||
const formMethods = useForm<FormValues>({
|
||||
defaultValues,
|
||||
resolver: zodResolver(profileFormSchema),
|
||||
@@ -356,7 +350,7 @@ const ProfileForm = ({
|
||||
name="avatar"
|
||||
render={({ field: { value } }) => (
|
||||
<>
|
||||
<Avatar alt="" imageSrc={value} gravatarFallbackMd5={emailMd5} size="lg" />
|
||||
<Avatar alt="" imageSrc={value} gravatarFallbackMd5="fallback" size="lg" />
|
||||
<div className="ltr:ml-4 rtl:mr-4">
|
||||
<ImageUploader
|
||||
target="avatar"
|
||||
|
||||
@@ -10,7 +10,7 @@ import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
import { markdownAndSanitize } from "@calcom/lib/markdownAndSanitize";
|
||||
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -113,7 +113,7 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
|
||||
<>
|
||||
<div
|
||||
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
dangerouslySetInnerHTML={{ __html: md.render(team.bio || "") }}
|
||||
dangerouslySetInnerHTML={{ __html: team.safeBio }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -187,11 +187,18 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
...user,
|
||||
avatar: CAL_URL + "/" + user.username + "/avatar.png",
|
||||
})),
|
||||
descriptionAsSafeHTML: markdownAndSanitize(type.description),
|
||||
}));
|
||||
|
||||
const safeBio = markdownAndSanitize(team.bio) || "";
|
||||
|
||||
const members = team.members.map((member) => {
|
||||
return { ...member, safeBio: markdownAndSanitize(member.bio || "") };
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
team,
|
||||
team: { ...team, safeBio, members },
|
||||
trpcState: ssr.dehydrate(),
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { privacyFilteredLocations } from "@calcom/core/location";
|
||||
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
||||
import { parseRecurringEvent } from "@calcom/lib";
|
||||
import { getWorkingHours } from "@calcom/lib/availability";
|
||||
import { markdownAndSanitize } from "@calcom/lib/markdownAndSanitize";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
@@ -171,6 +172,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
hideBranding,
|
||||
timeZone,
|
||||
})),
|
||||
descriptionAsSafeHTML: markdownAndSanitize(eventType.description),
|
||||
});
|
||||
|
||||
eventTypeObject.availability = [];
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { LocationObject } from "@calcom/app-store/locations";
|
||||
import { privacyFilteredLocations } from "@calcom/app-store/locations";
|
||||
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
||||
import { parseRecurringEvent } from "@calcom/lib";
|
||||
import { markdownAndSanitize } from "@calcom/lib/markdownAndSanitize";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
@@ -121,6 +122,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
image: u.avatar,
|
||||
slug: u.username,
|
||||
})),
|
||||
descriptionAsSafeHTML: markdownAndSanitize(eventType.description),
|
||||
};
|
||||
})[0];
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
import type { IncomingMessage, ServerResponse } from "http";
|
||||
import { createServer } from "http";
|
||||
import noop from "lodash/noop";
|
||||
import { noop } from "lodash";
|
||||
|
||||
import { test } from "./fixtures";
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ export async function ssgInit<TParams extends { locale?: string }>(opts: GetStat
|
||||
ctx: {
|
||||
prisma,
|
||||
session: null,
|
||||
user: null,
|
||||
locale,
|
||||
i18n: _i18n,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SelectedCalendar } from "@prisma/client";
|
||||
import _ from "lodash";
|
||||
import { sortBy } from "lodash";
|
||||
import * as process from "process";
|
||||
|
||||
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
||||
@@ -54,8 +54,8 @@ export const getConnectedCalendars = async (
|
||||
};
|
||||
}
|
||||
const cals = await calendar.listCalendars();
|
||||
const calendars = _(cals)
|
||||
.map((cal) => {
|
||||
const calendars = sortBy(
|
||||
cals.map((cal) => {
|
||||
if (cal.externalId === destinationCalendarExternalId) destinationCalendar = cal;
|
||||
return {
|
||||
...cal,
|
||||
@@ -64,9 +64,9 @@ export const getConnectedCalendars = async (
|
||||
isSelected: selectedCalendars.some((selected) => selected.externalId === cal.externalId),
|
||||
credentialId,
|
||||
};
|
||||
})
|
||||
.sortBy(["primary"])
|
||||
.value();
|
||||
}),
|
||||
["primary"]
|
||||
);
|
||||
const primary = calendars.find((item) => item.primary) ?? calendars.find((cal) => cal !== undefined);
|
||||
if (!primary) {
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { DestinationCalendar, Booking } from "@prisma/client";
|
||||
import { cloneDeep } from "lodash";
|
||||
import merge from "lodash/merge";
|
||||
import { cloneDeep, merge } from "lodash";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
import type { z } from "zod";
|
||||
|
||||
|
||||
@@ -1,31 +1,6 @@
|
||||
/* eslint-disable @calcom/eslint/deprecated-imports */
|
||||
import dayjs from "dayjs";
|
||||
import dayjsBusinessTime from "dayjs-business-days2";
|
||||
import "dayjs/locale/ar";
|
||||
import "dayjs/locale/bg";
|
||||
import "dayjs/locale/cs";
|
||||
import "dayjs/locale/de";
|
||||
import "dayjs/locale/es";
|
||||
import "dayjs/locale/es-mx";
|
||||
import "dayjs/locale/fr";
|
||||
import "dayjs/locale/he";
|
||||
import "dayjs/locale/hu";
|
||||
import "dayjs/locale/it";
|
||||
import "dayjs/locale/ja";
|
||||
import "dayjs/locale/ko";
|
||||
import "dayjs/locale/nl";
|
||||
import "dayjs/locale/pl";
|
||||
import "dayjs/locale/pt";
|
||||
import "dayjs/locale/pt-br";
|
||||
import "dayjs/locale/ro";
|
||||
import "dayjs/locale/ru";
|
||||
import "dayjs/locale/sr";
|
||||
import "dayjs/locale/sv";
|
||||
import "dayjs/locale/tr";
|
||||
import "dayjs/locale/uk";
|
||||
import "dayjs/locale/vi";
|
||||
import "dayjs/locale/zh-cn";
|
||||
import "dayjs/locale/zh-tw";
|
||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||
import isBetween from "dayjs/plugin/isBetween";
|
||||
import isToday from "dayjs/plugin/isToday";
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import "dayjs/locale/ar";
|
||||
import "dayjs/locale/bg";
|
||||
import "dayjs/locale/cs";
|
||||
import "dayjs/locale/de";
|
||||
import "dayjs/locale/es";
|
||||
import "dayjs/locale/es-mx";
|
||||
import "dayjs/locale/fr";
|
||||
import "dayjs/locale/he";
|
||||
import "dayjs/locale/hu";
|
||||
import "dayjs/locale/it";
|
||||
import "dayjs/locale/ja";
|
||||
import "dayjs/locale/ko";
|
||||
import "dayjs/locale/nl";
|
||||
import "dayjs/locale/pl";
|
||||
import "dayjs/locale/pt";
|
||||
import "dayjs/locale/pt-br";
|
||||
import "dayjs/locale/ro";
|
||||
import "dayjs/locale/ru";
|
||||
import "dayjs/locale/sr";
|
||||
import "dayjs/locale/sv";
|
||||
import "dayjs/locale/tr";
|
||||
import "dayjs/locale/uk";
|
||||
import "dayjs/locale/vi";
|
||||
import "dayjs/locale/zh-cn";
|
||||
import "dayjs/locale/zh-tw";
|
||||
@@ -1,3 +1,5 @@
|
||||
import { markdownAndSanitize } from "@calcom/lib/markdownAndSanitize";
|
||||
|
||||
const Spacer = () => <p style={{ height: 6 }} />;
|
||||
|
||||
export const Info = (props: {
|
||||
@@ -6,8 +8,14 @@ export const Info = (props: {
|
||||
extraInfo?: React.ReactNode;
|
||||
withSpacer?: boolean;
|
||||
lineThrough?: boolean;
|
||||
formatted?: boolean;
|
||||
}) => {
|
||||
if (!props.description || props.description === "") return null;
|
||||
|
||||
const descriptionCSS = "color: '#101010'; font-weight: 400; line-height: 24px; margin: 0;";
|
||||
|
||||
const safeDescription = markdownAndSanitize(props.description.toString()) || "";
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.withSpacer && <Spacer />}
|
||||
@@ -21,7 +29,18 @@ export const Info = (props: {
|
||||
whiteSpace: "pre-wrap",
|
||||
textDecoration: props.lineThrough ? "line-through" : undefined,
|
||||
}}>
|
||||
{props.description}
|
||||
{props.formatted ? (
|
||||
<p
|
||||
className="dark:text-darkgray-600 mt-2 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: safeDescription
|
||||
.replaceAll("<p>", `<p style="${descriptionCSS}">`)
|
||||
.replaceAll("<li>", `<li style="${descriptionCSS}">`),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
props.description
|
||||
)}
|
||||
</p>
|
||||
{props.extraInfo}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { TFunction } from "next-i18next";
|
||||
import { RRule } from "rrule";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
// TODO: Use browser locale, implement Intl in Dayjs maybe?
|
||||
import "@calcom/dayjs/locales";
|
||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
|
||||
@@ -76,7 +76,7 @@ export const BaseScheduledEmail = (
|
||||
<WhenInfo calEvent={props.calEvent} t={t} timeZone={timeZone} />
|
||||
<WhoInfo calEvent={props.calEvent} t={t} />
|
||||
<LocationInfo calEvent={props.calEvent} t={t} />
|
||||
<Info label={t("description")} description={props.calEvent.description} withSpacer />
|
||||
<Info label={t("description")} description={props.calEvent.description} withSpacer formatted />
|
||||
<Info label={t("additional_notes")} description={props.calEvent.additionalNotes} withSpacer />
|
||||
{props.includeAppsStatus && <AppsStatus calEvent={props.calEvent} t={t} />}
|
||||
<UserFieldsResponses calEvent={props.calEvent} />
|
||||
|
||||
@@ -344,13 +344,15 @@ function keepParentInformedAboutDimensionChanges() {
|
||||
// Use, .height as that gives more accurate value in floating point. Also, do a ceil on the total sum so that whatever happens there is enough iframe size to avoid scroll.
|
||||
const contentHeight = Math.ceil(
|
||||
parseFloat(mainElementStyles.height) +
|
||||
parseFloat(mainElementStyles.marginTop) +
|
||||
parseFloat(mainElementStyles.marginBottom));
|
||||
parseFloat(mainElementStyles.marginTop) +
|
||||
parseFloat(mainElementStyles.marginBottom)
|
||||
);
|
||||
const contentWidth = Math.ceil(
|
||||
parseFloat(mainElementStyles.width) +
|
||||
parseFloat(mainElementStyles.marginLeft) +
|
||||
parseFloat(mainElementStyles.marginRight));
|
||||
|
||||
parseFloat(mainElementStyles.marginLeft) +
|
||||
parseFloat(mainElementStyles.marginRight)
|
||||
);
|
||||
|
||||
// During first render let iframe tell parent that how much is the expected height to avoid scroll.
|
||||
// Parent would set the same value as the height of iframe which would prevent scroll.
|
||||
// On subsequent renders, consider html height as the height of the iframe. If we don't do this, then if iframe get's bigger in height, it would never shrink
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { UserPermissionRole } from "@prisma/client";
|
||||
import type { UserPermissionRole, Membership, Team } from "@prisma/client";
|
||||
import { IdentityProvider } from "@prisma/client";
|
||||
import { readFileSync } from "fs";
|
||||
import Handlebars from "handlebars";
|
||||
@@ -61,6 +61,20 @@ const signJwt = async (payload: { email: string }) => {
|
||||
const loginWithTotp = async (user: { email: string }) =>
|
||||
`/auth/login?totp=${await signJwt({ email: user.email })}`;
|
||||
|
||||
type UserTeams = {
|
||||
teams: (Membership & {
|
||||
team: Team;
|
||||
})[];
|
||||
};
|
||||
|
||||
const checkIfUserBelongsToActiveTeam = <T extends UserTeams>(user: T): boolean =>
|
||||
user.teams.filter((m: { team: { metadata: unknown } }) => {
|
||||
if (!IS_TEAM_BILLING_ENABLED) return true;
|
||||
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
|
||||
if (metadata.success && metadata.data?.subscriptionId) return true;
|
||||
return false;
|
||||
}).length > 0;
|
||||
|
||||
const providers: Provider[] = [
|
||||
CredentialsProvider({
|
||||
id: "credentials",
|
||||
@@ -157,13 +171,7 @@ const providers: Provider[] = [
|
||||
}
|
||||
}
|
||||
// Check if the user you are logging into has any active teams
|
||||
const hasActiveTeams =
|
||||
user.teams.filter((m: { team: { metadata: unknown } }) => {
|
||||
if (!IS_TEAM_BILLING_ENABLED) return true;
|
||||
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
|
||||
if (metadata.success && metadata.data?.subscriptionId) return true;
|
||||
return false;
|
||||
}).length > 0;
|
||||
const hasActiveTeams = checkIfUserBelongsToActiveTeam(user);
|
||||
|
||||
// authentication success- but does it meet the minimum password requirements?
|
||||
const validateRole = (role: UserPermissionRole) => {
|
||||
@@ -391,6 +399,11 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||
name: true,
|
||||
email: true,
|
||||
role: true,
|
||||
teams: {
|
||||
include: {
|
||||
team: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -398,9 +411,14 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||
return token;
|
||||
}
|
||||
|
||||
// Check if the existingUser has any active teams
|
||||
const belongsToActiveTeam = checkIfUserBelongsToActiveTeam(existingUser);
|
||||
const { teams, ...existingUserWithoutTeamsField } = existingUser;
|
||||
|
||||
return {
|
||||
...existingUser,
|
||||
...existingUserWithoutTeamsField,
|
||||
...token,
|
||||
belongsToActiveTeam,
|
||||
};
|
||||
};
|
||||
if (!user) {
|
||||
|
||||
@@ -494,7 +494,8 @@ async function handler(req: CustomRequest) {
|
||||
bookingToDelete.user.credentials
|
||||
.filter((credential) => credential.type.endsWith("_video"))
|
||||
.forEach((credential) => {
|
||||
apiDeletes.push(deleteMeeting(credential, bookingToDelete.uid));
|
||||
const uidToDelete = bookingToDelete?.references?.[0].uid ?? bookingToDelete.uid;
|
||||
apiDeletes.push(deleteMeeting(credential, uidToDelete));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -143,6 +143,9 @@ const isWithinAvailableHours = (
|
||||
return true;
|
||||
}
|
||||
}
|
||||
log.error(
|
||||
`NAUF: isWithinAvailableHours ${JSON.stringify({ ...timeSlot, organizerTimeZone, workingHours })}`
|
||||
);
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -158,10 +161,22 @@ function checkForConflicts(busyTimes: BufferedBusyTimes, time: dayjs.ConfigType,
|
||||
const endTime = dayjs(busyTime.end);
|
||||
// Check if time is between start and end times
|
||||
if (dayjs(time).isBetween(startTime, endTime, null, "[)")) {
|
||||
log.error(
|
||||
`NAUF: start between a busy time slot ${JSON.stringify({
|
||||
...busyTime,
|
||||
time: dayjs(time).format(),
|
||||
})}`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
// Check if slot end time is between start and end time
|
||||
if (dayjs(time).add(length, "minutes").isBetween(startTime, endTime)) {
|
||||
log.error(
|
||||
`NAUF: Ends between a busy time slot ${JSON.stringify({
|
||||
...busyTime,
|
||||
time: dayjs(time).add(length, "minutes").format(),
|
||||
})}`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
// Check if startTime is between slot
|
||||
@@ -537,14 +552,14 @@ async function handler(
|
||||
periodCountCalendarDays: eventType.periodCountCalendarDays,
|
||||
});
|
||||
} catch (error) {
|
||||
log.warn({
|
||||
message: "NewBooking: Unable set timeOutOfBounds. Using false. ",
|
||||
});
|
||||
if (error instanceof BookingDateInPastError) {
|
||||
// TODO: HttpError should not bleed through to the console.
|
||||
log.info(`Booking eventType ${eventTypeId} failed`, error);
|
||||
throw new HttpError({ statusCode: 400, message: error.message });
|
||||
}
|
||||
log.debug({
|
||||
message: "Unable set timeOutOfBounds. Using false. ",
|
||||
});
|
||||
}
|
||||
|
||||
if (timeOutOfBounds) {
|
||||
@@ -552,7 +567,9 @@ async function handler(
|
||||
errorCode: "BookingTimeOutOfBounds",
|
||||
message: `EventType '${eventType.eventName}' cannot be booked at this time.`,
|
||||
};
|
||||
|
||||
log.warn({
|
||||
message: `NewBooking: EventType '${eventType.eventName}' cannot be booked at this time.`,
|
||||
});
|
||||
throw new HttpError({ statusCode: 400, message: error.message });
|
||||
}
|
||||
|
||||
@@ -583,6 +600,7 @@ async function handler(
|
||||
|
||||
const isDynamicAllowed = !users.some((user) => !user.allowDynamicBooking);
|
||||
if (!isDynamicAllowed && !eventTypeId) {
|
||||
log.warn({ message: "NewBooking: Some of the users in this group do not allow dynamic booking" });
|
||||
throw new HttpError({
|
||||
message: "Some of the users in this group do not allow dynamic booking",
|
||||
statusCode: 400,
|
||||
@@ -598,7 +616,10 @@ async function handler(
|
||||
},
|
||||
...userSelect,
|
||||
});
|
||||
if (!eventTypeUser) throw new HttpError({ statusCode: 404, message: "eventTypeUser.notFound" });
|
||||
if (!eventTypeUser) {
|
||||
log.warn({ message: "NewBooking: eventTypeUser.notFound" });
|
||||
throw new HttpError({ statusCode: 404, message: "eventTypeUser.notFound" });
|
||||
}
|
||||
users.push(eventTypeUser);
|
||||
}
|
||||
|
||||
@@ -1424,8 +1445,7 @@ async function handler(
|
||||
// Otherwise, an owner rescheduling should be always accepted.
|
||||
// Before comparing make sure that userId is set, otherwise undefined === undefined
|
||||
const userReschedulingIsOwner = userId && originalRescheduledBooking?.user?.id === userId;
|
||||
const isConfirmedByDefault =
|
||||
(!eventType.requiresConfirmation && !paymentAppData.price) || userReschedulingIsOwner;
|
||||
const isConfirmedByDefault = (!requiresConfirmation && !paymentAppData.price) || userReschedulingIsOwner;
|
||||
|
||||
const attendeesData = evt.attendees.map((attendee) => {
|
||||
//if attendee is team member, it should fetch their locale not booker's locale
|
||||
@@ -1654,7 +1674,7 @@ async function handler(
|
||||
}
|
||||
|
||||
// Use EventManager to conditionally use all needed integrations.
|
||||
|
||||
addVideoCallDataToEvt(originalRescheduledBooking.references);
|
||||
const updateManager = await eventManager.reschedule(evt, originalRescheduledBooking.uid);
|
||||
// This gets overridden when updating the event - to check if notes have been hidden or not. We just reset this back
|
||||
// to the default description when we are sending the emails.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import noop from "lodash/noop";
|
||||
import { noop } from "lodash";
|
||||
import { useIntercom as useIntercomLib } from "react-use-intercom";
|
||||
import { z } from "zod";
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { z } from "zod";
|
||||
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { markdownAndSanitize } from "@calcom/lib/markdownAndSanitizeClientSide";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
import objectKeys from "@calcom/lib/objectKeys";
|
||||
import turndown from "@calcom/lib/turndownService";
|
||||
@@ -256,7 +257,7 @@ const ProfileView = () => {
|
||||
<Label className="mt-5 text-black">{t("about")}</Label>
|
||||
<div
|
||||
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
dangerouslySetInnerHTML={{ __html: md.render(team.bio || "") }}
|
||||
dangerouslySetInnerHTML={{ __html: markdownAndSanitize(team.bio) }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -7,7 +7,6 @@ import type { z } from "zod";
|
||||
import { classNames, parseRecurringEvent } from "@calcom/lib";
|
||||
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { addListFormatting, md } from "@calcom/lib/markdownIt";
|
||||
import type { baseEventTypeSelect } from "@calcom/prisma";
|
||||
import type { EventTypeModel } from "@calcom/prisma/zod";
|
||||
import { Badge } from "@calcom/ui";
|
||||
@@ -26,8 +25,9 @@ export type EventTypeDescriptionProps = {
|
||||
z.infer<typeof EventTypeModel>,
|
||||
Exclude<keyof typeof baseEventTypeSelect, "recurringEvent"> | "metadata"
|
||||
> & {
|
||||
descriptionAsSafeHTML?: string | null;
|
||||
recurringEvent: Prisma.JsonValue;
|
||||
seatsPerTimeSlot?: number;
|
||||
seatsPerTimeSlot?: number | null;
|
||||
};
|
||||
className?: string;
|
||||
shortenDescription?: boolean;
|
||||
@@ -57,7 +57,7 @@ export const EventTypeDescription = ({
|
||||
shortenDescription ? "line-clamp-4" : ""
|
||||
)}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: addListFormatting(md.render(eventType.description)),
|
||||
__html: eventType.descriptionAsSafeHTML || "",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import DOMPurify from "dompurify";
|
||||
import { JSDOM } from "jsdom";
|
||||
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
|
||||
export function markdownAndSanitize(markdown: string | null) {
|
||||
if (!markdown) return null;
|
||||
|
||||
const window = new JSDOM("").window;
|
||||
// @ts-expect-error as suggested here: https://github.com/cure53/DOMPurify/issues/437#issuecomment-632021941
|
||||
const purify = DOMPurify(window);
|
||||
|
||||
const html = md
|
||||
.render(markdown)
|
||||
.replaceAll(
|
||||
"<ul>",
|
||||
"<ul style='list-style-type: disc; list-style-position: inside; margin-left: 12px; margin-bottom: 4px'>"
|
||||
)
|
||||
.replaceAll(
|
||||
"<ol>",
|
||||
"<ol style='list-style-type: decimal; list-style-position: inside; margin-left: 12px; margin-bottom: 4px'>"
|
||||
);
|
||||
const safeHtml = purify.sanitize(html);
|
||||
return safeHtml;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import DOMPurify from "dompurify";
|
||||
|
||||
import { md } from "./markdownIt";
|
||||
|
||||
export function markdownAndSanitize(markdown: string | null) {
|
||||
if (!markdown) return "";
|
||||
|
||||
const html = md
|
||||
.render(markdown)
|
||||
.replaceAll(
|
||||
"<ul>",
|
||||
"<ul style='list-style-type: disc; list-style-position: inside; margin-left: 12px; margin-bottom: 4px'>"
|
||||
)
|
||||
.replaceAll(
|
||||
"<ol>",
|
||||
"<ol style='list-style-type: decimal; list-style-position: inside; margin-left: 12px; margin-bottom: 4px'>"
|
||||
);
|
||||
|
||||
const safeHtml = DOMPurify.sanitize(html);
|
||||
return safeHtml;
|
||||
}
|
||||
@@ -1,12 +1,3 @@
|
||||
import MarkdownIt from "markdown-it";
|
||||
|
||||
export const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
||||
|
||||
export function addListFormatting(html: string) {
|
||||
return html
|
||||
.replaceAll("<ul>", "<ul style='list-style-type: disc; list-style-position: inside; margin-left: 12px'>")
|
||||
.replaceAll(
|
||||
"<ol>",
|
||||
"<ol style='list-style-type: decimal; list-style-position: inside; margin-left: 12px'>"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -203,7 +203,8 @@
|
||||
"categories": ["video"],
|
||||
"slug": "facetime",
|
||||
"type": "facetime_video",
|
||||
"isTemplate": false},
|
||||
"isTemplate": false
|
||||
},
|
||||
{
|
||||
"dirName": "zohocrm",
|
||||
"categories": ["other"],
|
||||
|
||||
@@ -3,108 +3,32 @@ import type { Session } from "next-auth";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { defaultAvatarSrc } from "@calcom/lib/defaultAvatarImage";
|
||||
import { getLocaleFromHeaders } from "@calcom/lib/i18n";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { SelectedCalendar, User as PrismaUser, Credential } from "@calcom/prisma/client";
|
||||
|
||||
import type { Maybe } from "@trpc/server";
|
||||
import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
|
||||
|
||||
type CreateContextOptions = CreateNextContextOptions | GetServerSidePropsContext;
|
||||
|
||||
async function getUserFromSession({
|
||||
session,
|
||||
req,
|
||||
}: {
|
||||
session: Maybe<Session>;
|
||||
req: CreateContextOptions["req"];
|
||||
}) {
|
||||
if (!session?.user?.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: session.user.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
email: true,
|
||||
bio: true,
|
||||
timeZone: true,
|
||||
weekStart: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
defaultScheduleId: true,
|
||||
bufferTime: true,
|
||||
theme: true,
|
||||
createdDate: true,
|
||||
hideBranding: true,
|
||||
avatar: true,
|
||||
twoFactorEnabled: true,
|
||||
disableImpersonation: true,
|
||||
identityProvider: true,
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
away: true,
|
||||
credentials: {
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
key: true,
|
||||
userId: true,
|
||||
appId: true,
|
||||
invalid: true,
|
||||
},
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
},
|
||||
selectedCalendars: {
|
||||
select: {
|
||||
externalId: true,
|
||||
integration: true,
|
||||
},
|
||||
},
|
||||
completedOnboarding: true,
|
||||
destinationCalendar: true,
|
||||
locale: true,
|
||||
timeFormat: true,
|
||||
trialEndsAt: true,
|
||||
metadata: true,
|
||||
role: true,
|
||||
},
|
||||
});
|
||||
|
||||
// some hacks to make sure `username` and `email` are never inferred as `null`
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
const { email, username } = user;
|
||||
if (!email) {
|
||||
return null;
|
||||
}
|
||||
const rawAvatar = user.avatar;
|
||||
// This helps to prevent reaching the 4MB payload limit by avoiding base64 and instead passing the avatar url
|
||||
user.avatar = rawAvatar ? `${WEBAPP_URL}/${user.username}/avatar.png` : defaultAvatarSrc({ email });
|
||||
|
||||
const locale = user.locale || getLocaleFromHeaders(req);
|
||||
return {
|
||||
...user,
|
||||
rawAvatar,
|
||||
email,
|
||||
username,
|
||||
locale,
|
||||
};
|
||||
}
|
||||
|
||||
type CreateInnerContextOptions = {
|
||||
session: Session | null;
|
||||
locale: string;
|
||||
user: Awaited<ReturnType<typeof getUserFromSession>>;
|
||||
user?: Omit<
|
||||
PrismaUser,
|
||||
| "locale"
|
||||
| "twoFactorSecret"
|
||||
| "emailVerified"
|
||||
| "password"
|
||||
| "identityProviderId"
|
||||
| "invitedTo"
|
||||
| "allowDynamicBooking"
|
||||
| "verified"
|
||||
> & {
|
||||
locale: NonNullable<PrismaUser["locale"]>;
|
||||
credentials?: Credential[];
|
||||
selectedCalendars?: Partial<SelectedCalendar>[];
|
||||
};
|
||||
i18n: Awaited<ReturnType<typeof serverSideTranslations>>;
|
||||
} & Partial<CreateContextOptions>;
|
||||
|
||||
@@ -144,11 +68,9 @@ export const createContext = async (
|
||||
// for API-response caching see https://trpc.io/docs/caching
|
||||
const session = await sessionGetter({ req, res });
|
||||
|
||||
const user = await getUserFromSession({ session, req });
|
||||
const locale = user?.locale ?? getLocaleFromHeaders(req);
|
||||
const i18n = await serverSideTranslations(locale, ["common", "vital"]);
|
||||
|
||||
const contextInner = await createContextInner({ session, i18n, locale, user });
|
||||
const locale = getLocaleFromHeaders(req);
|
||||
const i18n = await serverSideTranslations(getLocaleFromHeaders(req), ["common", "vital"]);
|
||||
const contextInner = await createContextInner({ session, i18n, locale });
|
||||
return {
|
||||
...contextInner,
|
||||
req,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { DestinationCalendar, Prisma } from "@prisma/client";
|
||||
import { AppCategories, BookingStatus, IdentityProvider } from "@prisma/client";
|
||||
import { cityMapping } from "city-timezones";
|
||||
import _ from "lodash";
|
||||
import { reverse } from "lodash";
|
||||
import { authenticator } from "otplib";
|
||||
import z from "zod";
|
||||
|
||||
@@ -755,7 +755,7 @@ const loggedInViewerRouter = router({
|
||||
});
|
||||
}
|
||||
await Promise.all(
|
||||
_.reverse(input.ids).map((id, position) => {
|
||||
reverse(input.ids).map((id, position) => {
|
||||
return prisma.eventType.update({
|
||||
where: {
|
||||
id,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { MembershipRole, PeriodType, Prisma, SchedulingType } from "@prisma/client";
|
||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
||||
// REVIEW: From lint error
|
||||
import _ from "lodash";
|
||||
import { orderBy } from "lodash";
|
||||
import type { NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -336,7 +335,7 @@ export const eventTypesRouter = router({
|
||||
name: user.name,
|
||||
image: user.avatar || undefined,
|
||||
},
|
||||
eventTypes: _.orderBy(mergedEventTypes, ["position", "id"], ["desc", "asc"]),
|
||||
eventTypes: orderBy(mergedEventTypes, ["position", "id"], ["desc", "asc"]),
|
||||
metadata: {
|
||||
membershipCount: 1,
|
||||
readOnly: false,
|
||||
|
||||
+102
-38
@@ -1,11 +1,96 @@
|
||||
import type { Session } from "next-auth";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import superjson from "superjson";
|
||||
|
||||
import rateLimit from "@calcom/lib/rateLimit";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { defaultAvatarSrc } from "@calcom/lib/defaultAvatarImage";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import type { Maybe } from "@trpc/server";
|
||||
import { initTRPC, TRPCError } from "@trpc/server";
|
||||
|
||||
import type { createContextInner } from "./createContext";
|
||||
|
||||
async function getUserFromSession({ session }: { session: Maybe<Session> }) {
|
||||
if (!session?.user?.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: session.user.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
email: true,
|
||||
bio: true,
|
||||
timeZone: true,
|
||||
weekStart: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
defaultScheduleId: true,
|
||||
bufferTime: true,
|
||||
theme: true,
|
||||
createdDate: true,
|
||||
hideBranding: true,
|
||||
avatar: true,
|
||||
twoFactorEnabled: true,
|
||||
disableImpersonation: true,
|
||||
identityProvider: true,
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
away: true,
|
||||
credentials: {
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
key: true,
|
||||
userId: true,
|
||||
appId: true,
|
||||
invalid: true,
|
||||
},
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
},
|
||||
selectedCalendars: {
|
||||
select: {
|
||||
externalId: true,
|
||||
integration: true,
|
||||
},
|
||||
},
|
||||
completedOnboarding: true,
|
||||
destinationCalendar: true,
|
||||
locale: true,
|
||||
timeFormat: true,
|
||||
trialEndsAt: true,
|
||||
metadata: true,
|
||||
role: true,
|
||||
},
|
||||
});
|
||||
|
||||
// some hacks to make sure `username` and `email` are never inferred as `null`
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
const { email, username } = user;
|
||||
if (!email) {
|
||||
return null;
|
||||
}
|
||||
const rawAvatar = user.avatar;
|
||||
// This helps to prevent reaching the 4MB payload limit by avoiding base64 and instead passing the avatar url
|
||||
user.avatar = rawAvatar ? `${WEBAPP_URL}/${user.username}/avatar.png` : defaultAvatarSrc({ email });
|
||||
|
||||
return {
|
||||
...user,
|
||||
rawAvatar,
|
||||
email,
|
||||
username,
|
||||
};
|
||||
}
|
||||
|
||||
const t = initTRPC.context<typeof createContextInner>().create({
|
||||
transformer: superjson,
|
||||
});
|
||||
@@ -18,20 +103,30 @@ const perfMiddleware = t.middleware(async ({ path, type, next }) => {
|
||||
return result;
|
||||
});
|
||||
|
||||
const isAuthedMiddleware = t.middleware(({ ctx, next }) => {
|
||||
if (!ctx.user || !ctx.session) {
|
||||
const isAuthed = t.middleware(async ({ ctx: { session, locale, ...ctx }, next }) => {
|
||||
const user = await getUserFromSession({ session });
|
||||
if (!user || !session) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
const i18n =
|
||||
user.locale && user.locale !== locale
|
||||
? await serverSideTranslations(user.locale, ["common", "vital"])
|
||||
: ctx.i18n;
|
||||
locale = user.locale || locale;
|
||||
return next({
|
||||
ctx: {
|
||||
i18n,
|
||||
// infers that `user` and `session` are non-nullable to downstream procedures
|
||||
session: ctx.session,
|
||||
user: ctx.user,
|
||||
session,
|
||||
user: {
|
||||
...user,
|
||||
locale,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const isAdminMiddleware = isAuthedMiddleware.unstable_pipe(({ ctx, next }) => {
|
||||
const isAdminMiddleware = isAuthed.unstable_pipe(({ ctx, next }) => {
|
||||
if (ctx.user.role !== "ADMIN") {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
@@ -40,41 +135,10 @@ const isAdminMiddleware = isAuthedMiddleware.unstable_pipe(({ ctx, next }) => {
|
||||
});
|
||||
});
|
||||
|
||||
interface IRateLimitOptions {
|
||||
intervalInMs: number;
|
||||
limit: number;
|
||||
}
|
||||
const isRateLimitedByUserIdMiddleware = ({ intervalInMs, limit }: IRateLimitOptions) =>
|
||||
t.middleware(({ ctx, next }) => {
|
||||
// validate user exists
|
||||
if (!ctx.user) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
|
||||
const { isRateLimited } = rateLimit({ intervalInMs }).check(limit, ctx.user.id.toString());
|
||||
|
||||
if (isRateLimited) {
|
||||
throw new TRPCError({ code: "TOO_MANY_REQUESTS" });
|
||||
}
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
// infers that `user` and `session` are non-nullable to downstream procedures
|
||||
session: ctx.session,
|
||||
user: ctx.user,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export const router = t.router;
|
||||
export const mergeRouters = t.mergeRouters;
|
||||
export const middleware = t.middleware;
|
||||
export const publicProcedure = t.procedure.use(perfMiddleware);
|
||||
export const authedProcedure = t.procedure.use(perfMiddleware).use(isAuthedMiddleware);
|
||||
export const authedRateLimitedProcedure = ({ intervalInMs, limit }: IRateLimitOptions) =>
|
||||
t.procedure
|
||||
.use(perfMiddleware)
|
||||
.use(isAuthedMiddleware)
|
||||
.use(isRateLimitedByUserIdMiddleware({ intervalInMs, limit }));
|
||||
export const authedProcedure = t.procedure.use(perfMiddleware).use(isAuthed);
|
||||
|
||||
export const authedAdminProcedure = t.procedure.use(perfMiddleware).use(isAdminMiddleware);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { $createHeadingNode, $isHeadingNode } from "@lexical/rich-text";
|
||||
import { $isAtNodeEnd, $wrapNodes } from "@lexical/selection";
|
||||
import { $getNearestNodeOfType, mergeRegister } from "@lexical/utils";
|
||||
import classNames from "classnames";
|
||||
import DOMPurify from "dompurify";
|
||||
import type { EditorState, GridSelection, LexicalEditor, NodeSelection, RangeSelection } from "lexical";
|
||||
import {
|
||||
$createParagraphNode,
|
||||
@@ -351,8 +352,8 @@ export default function ToolbarPlugin(props: TextEditorProps) {
|
||||
|
||||
editor.registerUpdateListener(({ editorState, prevEditorState }) => {
|
||||
editorState.read(() => {
|
||||
const textInHtml = $generateHtmlFromNodes(editor);
|
||||
props.setText(textInHtml);
|
||||
const textInHtml = $generateHtmlFromNodes(editor).replace(/</g, "<").replace(/>/g, ">");
|
||||
props.setText(DOMPurify.sanitize(textInHtml));
|
||||
});
|
||||
if (!prevEditorState._selection) editor.blur();
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import merge from "lodash/merge";
|
||||
import { merge } from "lodash";
|
||||
import type { NextSeoProps } from "next-seo";
|
||||
import { NextSeo } from "next-seo";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -75,7 +75,8 @@ export const HeadSeo = (props: HeadSeoProps): JSX.Element => {
|
||||
// Get the current URL from the window object
|
||||
const url = getBrowserInfo()?.url;
|
||||
// Check if the URL is from cal.com
|
||||
const isCalcom = url && new URL(url).hostname.endsWith("cal.com");
|
||||
const isCalcom =
|
||||
url && (new URL(url).hostname.endsWith("cal.com") || new URL(url).hostname.endsWith("cal.dev"));
|
||||
// Get the router's path
|
||||
const path = useRouter().asPath;
|
||||
// Build the canonical URL using the router's path, without query parameters. Note: on homepage it omits the trailing slash
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { XIcon } from "@heroicons/react/solid";
|
||||
import classNames from "classnames";
|
||||
import noop from "lodash/noop";
|
||||
import { noop } from "lodash";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { FiAlertTriangle, FiInfo } from "../icon";
|
||||
|
||||
Reference in New Issue
Block a user