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