Compare commits

...
13 Commits
Author SHA1 Message Date
Peer Richelsen 47e13cd2fd Update package.json
release.yml / release (push) Canceled after 0s
2023-03-23 21:18:33 +01:00
Hariom Balhara 74ab6d209e Fix/correctly handle Pending Status and fix conditional require confirmation (#7911)
* Use pending status

* Use requiresConfirmation variable instead of eventType.requiresConfirmation as the earlier handles requiresConfirmation threshold as well
2023-03-23 19:50:39 +01:00
Alex van Andel 46f0b55a45 Techdebt/bundle size experiments (#7894)
* Checking package size effects when we remove zod from middleware

* Remove 'crypto' from client code

* Revert "Checking package size effects when we remove zod from middleware"

This reverts commit ecc2038411c73fc562d57712d5c83d66925e7e13.

* Adding lodash to dynamicImports + consistent imports

* Remove the locales from the global Dayjs object
2023-03-23 11:49:28 -07:00
Alex van Andel e4893c2218 Removes the getUser logic from createContext, instead use isAuthed (#7902)
* Removes the getUser logic from createContext, instead use isAuthed

* Fix types :party
2023-03-23 19:46:33 +01:00
Udit Takkar fa17139e8f fix: add belongsToActiveTeam (#7890)
* fix: add belongsToActiveTeam

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: create a common function

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: add suggestion

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* chore: use deconstructing

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* chore

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-03-23 19:23:06 +01:00
Efraín Rochín 1cdc97dd37 Add detailed error logs to handleNewBooking (#7907)
* Add logs to get more context on "No user available" error

* remove nit
2023-03-23 19:03:49 +01:00
Hariom Balhara f630e8af40 Add videocallData (#7899) 2023-03-23 15:18:27 +00:00
Peer Richelsen fe0745121b minor fix to adding to badge 2023-03-23 13:07:11 +01:00
f6d7568d0a Fixes formatted description in email (#7696)
* fix description in email

* add styling for lists

* sanitize editor input before saving

* sanitize eventTypeDescription

* santize html when used dangerouslySetInnerHTML

* fix server side sanitation

* add missing formatting and sanitation

* move @ts-expect-error to correct line

* fix type error and add yarn.lock

* fix build error

* sanitize description for booking page and availability page

* rename to markdownAndSanitize

* always add list formatting

* handle empty description in markdownAndSanitize for cleaner code

* create function for clientside markdown rendering and sanitizing

* fix type error

* code clean up

* Now that eventType.descriptionAsSafeHTML is added at all the missing places, we can do away with ts-ignore and get type safety

* Remove unused variable

* Remove one more ts-expect-error

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-23 12:00:42 +00:00
Nafees Nazik 520e7fe036 fix: remove "Nameless" from title in booking cancel page (#7888) 2023-03-23 10:22:30 +00:00
Nafees Nazik 1bf50e09cd fix: meeting not getting deleted (#7878) 2023-03-22 11:18:00 -07:00
darshanandBailey Pumfleet d1ca7b4bdd fix redirect for recurring events (#7821)
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2023-03-22 11:22:40 -04:00
Richard Poelderl 4e2473531f fix: set canonical to https://cal.com on .dev as well (#7883) 2023-03-22 15:43:59 +01:00
49 changed files with 943 additions and 592 deletions
+1 -1
View File
@@ -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>
+6 -5
View File
@@ -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} />
+3
View File
@@ -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}}",
+3 -1
View File
@@ -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",
+6 -2
View File
@@ -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"),
+2 -5
View File
@@ -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 =
+2
View File
@@ -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 -1
View File
@@ -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";
+8 -4
View File
@@ -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}
+2
View File
@@ -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;
+2
View File
@@ -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];
+2 -1
View File
@@ -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 -3
View File
@@ -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;
+2
View File
@@ -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 = [];
+2
View File
@@ -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];
+1 -1
View File
@@ -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";
-1
View File
@@ -29,7 +29,6 @@ export async function ssgInit<TParams extends { locale?: string }>(opts: GetStat
ctx: {
prisma,
session: null,
user: null,
locale,
i18n: _i18n,
},
+6 -6
View File
@@ -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 -2
View File
@@ -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";
-25
View File
@@ -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";
+25
View File
@@ -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";
+20 -1
View File
@@ -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 || "",
}}
/>
)}
+25
View File
@@ -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;
}
-9
View File
@@ -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'>"
);
}
+2 -1
View File
@@ -203,7 +203,8 @@
"categories": ["video"],
"slug": "facetime",
"type": "facetime_video",
"isTemplate": false},
"isTemplate": false
},
{
"dirName": "zohocrm",
"categories": ["other"],
+19 -97
View File
@@ -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,
+2 -2
View File
@@ -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
View File
@@ -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(/&lt;/g, "<").replace(/&gt;/g, ">");
props.setText(DOMPurify.sanitize(textInHtml));
});
if (!prevEditorState._selection) editor.blur();
});
+3 -2
View File
@@ -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";
+574 -337
View File
File diff suppressed because it is too large Load Diff