fix: email-address with apostrophe not accepted (#15535)

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
This commit is contained in:
Rajdeep Das
2024-10-15 00:21:26 +00:00
committed by GitHub
co-authored by Keith Williams Omar López Amit Sharma
parent 5927b42560
commit 2be9fc8bc0
27 changed files with 77 additions and 42 deletions
+3 -2
View File
@@ -1,5 +1,6 @@
import { z } from "zod";
import { emailSchema } from "@calcom/lib/emailSchema";
import { _AttendeeModel as Attendee } from "@calcom/prisma/zod";
import { timeZone } from "~/lib/validations/shared/timeZone";
@@ -14,7 +15,7 @@ export const schemaAttendeeBaseBodyParams = Attendee.pick({
const schemaAttendeeCreateParams = z
.object({
bookingId: z.number().int(),
email: z.string().email(),
email: emailSchema,
name: z.string(),
timeZone: timeZone,
})
@@ -23,7 +24,7 @@ const schemaAttendeeCreateParams = z
const schemaAttendeeEditParams = z
.object({
name: z.string().optional(),
email: z.string().email().optional(),
email: emailSchema.optional(),
timeZone: timeZone.optional(),
})
.strict();
@@ -1,16 +1,18 @@
import { withValidation } from "next-validations";
import { z } from "zod";
import { emailSchema } from "@calcom/lib/emailSchema";
import { baseApiParams } from "./baseApiParams";
// Extracted out as utility function so can be reused
// at different endpoints that require this validation.
export const schemaQueryAttendeeEmail = baseApiParams.extend({
attendeeEmail: z.string().email(),
attendeeEmail: emailSchema,
});
export const schemaQuerySingleOrMultipleAttendeeEmails = z.object({
attendeeEmail: z.union([z.string().email(), z.array(z.string().email())]).optional(),
attendeeEmail: z.union([emailSchema, z.array(emailSchema)]).optional(),
});
export const withValidQueryAttendeeEmail = withValidation({
@@ -1,16 +1,18 @@
import { withValidation } from "next-validations";
import { z } from "zod";
import { emailSchema } from "@calcom/lib/emailSchema";
import { baseApiParams } from "./baseApiParams";
// Extracted out as utility function so can be reused
// at different endpoints that require this validation.
export const schemaQueryUserEmail = baseApiParams.extend({
email: z.string().email(),
email: emailSchema,
});
export const schemaQuerySingleOrMultipleUserEmails = z.object({
email: z.union([z.string().email(), z.array(z.string().email())]),
email: z.union([emailSchema, z.array(emailSchema)]),
});
export const withValidQueryUserEmail = withValidation({
+3 -2
View File
@@ -1,5 +1,6 @@
import { z } from "zod";
import { emailSchema } from "@calcom/lib/emailSchema";
import { checkUsername } from "@calcom/lib/server/checkUsername";
import { _UserModel as User } from "@calcom/prisma/zod";
import { iso8601 } from "@calcom/prisma/zod-utils";
@@ -92,7 +93,7 @@ export const schemaUserBaseBodyParams = User.pick({
// Here we can both require or not (adding optional or nullish) and also rewrite validations for any value
// for example making weekStart only accept weekdays as input
const schemaUserEditParams = z.object({
email: z.string().email().toLowerCase(),
email: emailSchema.toLowerCase(),
username: usernameSchema,
weekStart: z.nativeEnum(weekdays).optional(),
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
@@ -115,7 +116,7 @@ const schemaUserEditParams = z.object({
// merging both BaseBodyParams with RequiredParams, and omiting whatever we want at the end.
const schemaUserCreateParams = z.object({
email: z.string().email().toLowerCase(),
email: emailSchema.toLowerCase(),
username: usernameSchema,
weekStart: z.nativeEnum(weekdays).optional(),
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
@@ -3,6 +3,7 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { emailSchema } from "@calcom/lib/emailSchema";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import {
Dialog,
@@ -37,7 +38,7 @@ const SecondaryEmailModal = ({
const formMethods = useForm<FormValues>({
resolver: zodResolver(
z.object({
email: z.string().email(),
email: emailSchema,
})
),
});
+2 -1
View File
@@ -7,6 +7,7 @@ import { z } from "zod";
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { emailRegex } from "@calcom/lib/emailSchema";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui";
@@ -39,7 +40,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
username: z
.string()
.refine((val) => val.trim().length >= 1, { message: t("at_least_characters", { count: 1 }) }),
email_address: z.string().email({ message: t("enter_valid_email") }),
email_address: z.string().regex(emailRegex, { message: t("enter_valid_email") }),
full_name: z.string().min(3, t("at_least_characters", { count: 3 })),
password: z.string().superRefine((data, ctx) => {
const isStrict = true;
+3 -2
View File
@@ -6,20 +6,21 @@ import { checkPremiumUsername } from "@calcom/features/ee/common/lib/checkPremiu
import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
import { IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
import { emailSchema } from "@calcom/lib/emailSchema";
import slugify from "@calcom/lib/slugify";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
import { ssrInit } from "@server/lib/ssr";
const checkValidEmail = (email: string) => z.string().email().safeParse(email).success;
const checkValidEmail = (email: string) => emailSchema.safeParse(email).success;
const querySchema = z.object({
username: z
.string()
.optional()
.transform((val) => val || ""),
email: z.string().email().optional(),
email: emailSchema.optional(),
});
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
+2 -1
View File
@@ -12,6 +12,7 @@ import { z } from "zod";
import { SAMLLogin } from "@calcom/features/auth/SAMLLogin";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { HOSTED_CAL_FEATURES, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
import { emailRegex } from "@calcom/lib/emailSchema";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLastUsed, LastUsed } from "@calcom/lib/hooks/useLastUsed";
@@ -59,7 +60,7 @@ PageProps & WithNonceProps<{}>) {
email: z
.string()
.min(1, `${t("error_required_field")}`)
.email(`${t("enter_valid_email")}`),
.regex(emailRegex, `${t("enter_valid_email")}`),
...(!!totpEmail ? {} : { password: z.string().min(1, `${t("error_required_field")}`) }),
})
// Passthrough other fields like totpCode
@@ -12,6 +12,7 @@ import { z } from "zod";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
import { APP_NAME, FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
import { emailSchema } from "@calcom/lib/emailSchema";
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
@@ -523,12 +524,12 @@ const ProfileForm = ({
.max(FULL_NAME_LENGTH_MAX_LIMIT, {
message: t("max_limit_allowed_hint", { limit: FULL_NAME_LENGTH_MAX_LIMIT }),
}),
email: z.string().email(),
email: emailSchema,
bio: z.string(),
secondaryEmails: z.array(
z.object({
id: z.number(),
email: z.string().email(),
email: emailSchema,
emailVerified: z.union([z.string(), z.null()]).optional(),
emailPrimary: z.boolean().optional(),
})
+2 -6
View File
@@ -1,17 +1,13 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { passwordResetRequest } from "@calcom/features/auth/lib/passwordResetRequest";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import { emailSchema } from "@calcom/lib/emailSchema";
import { defaultHandler } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
async function handler(req: NextApiRequest, res: NextApiResponse) {
const email = z
.string()
.email()
.transform((val) => val.toLowerCase())
.safeParse(req.body?.email);
const email = emailSchema.transform((val) => val.toLowerCase()).safeParse(req.body?.email);
if (!email.success) {
return res.status(400).json({ message: "email is required" });
+2 -1
View File
@@ -3,6 +3,7 @@ import z from "zod";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
import { emailRegex } from "@calcom/lib/emailSchema";
import { HttpError } from "@calcom/lib/http-error";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import slugify from "@calcom/lib/slugify";
@@ -14,7 +15,7 @@ const querySchema = z.object({
.string()
.refine((val) => val.trim().length >= 1, { message: "Please enter at least one character" }),
full_name: z.string().min(3, "Please enter at least 3 characters"),
email_address: z.string().email({ message: "Please enter a valid email" }),
email_address: z.string().regex(emailRegex, { message: "Please enter a valid email" }),
password: z.string().refine((val) => isPasswordValid(val.trim(), false, true), {
message:
"The password must be a minimum of 15 characters long containing at least one number and have a mixture of uppercase and lowercase letters",
+2 -1
View File
@@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import getRawBody from "raw-body";
import z from "zod";
import { emailSchema } from "@calcom/lib/emailSchema";
import { default as webPrisma } from "@calcom/prisma";
export const config = {
@@ -13,7 +14,7 @@ export const config = {
const helpscoutRequestBodySchema = z.object({
customer: z.object({
email: z.string().email(),
email: emailSchema,
}),
});
@@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { symmetricEncrypt } from "@calcom/lib/crypto";
import { emailSchema } from "@calcom/lib/emailSchema";
import logger from "@calcom/lib/logger";
import { defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
@@ -14,7 +15,7 @@ import { CalendarService } from "../lib";
const formSchema = z
.object({
url: z.string().url(),
username: z.string().email(),
username: emailSchema,
password: z.string(),
authenticationMethod: z.number().default(ExchangeAuthentication.STANDARD),
exchangeVersion: z.number().default(ExchangeVersion.Exchange2016),
@@ -5,6 +5,7 @@ import { Controller, useForm } from "react-hook-form";
import { Toaster } from "react-hot-toast";
import z from "zod";
import { emailSchema } from "@calcom/lib/emailSchema";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Alert, Button, EmailField, Form, PasswordField, SelectField, TextField } from "@calcom/ui";
@@ -22,7 +23,7 @@ interface IFormData {
const schema = z
.object({
url: z.string().url(),
username: z.string().email(),
username: emailSchema,
password: z.string(),
authenticationMethod: z.number().default(ExchangeAuthentication.STANDARD),
exchangeVersion: z.number().default(ExchangeVersion.Exchange2016),
@@ -1,6 +1,7 @@
import { Prisma } from "@prisma/client";
import { z } from "zod";
import { emailSchema } from "@calcom/lib/emailSchema";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import type { PrismaClient } from "@calcom/prisma";
@@ -75,7 +76,7 @@ export const responseHandler = async ({ ctx, input }: ResponseHandlerOptions) =>
}
let schema;
if (field.type === "email") {
schema = z.string().email();
schema = emailSchema;
} else if (field.type === "phone") {
schema = z.any();
} else {
+2 -1
View File
@@ -4,6 +4,7 @@ import { useFormContext } from "react-hook-form";
import z from "zod";
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
import { emailRegex } from "@calcom/lib/emailSchema";
import { LastUsed, useLastUsed } from "@calcom/lib/hooks/useLastUsed";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
@@ -17,7 +18,7 @@ interface Props {
}
const schema = z.object({
email: z.string().email({ message: "Please enter a valid email" }),
email: z.string().regex(emailRegex, { message: "Please enter a valid email" }),
});
export function SAMLLogin({
@@ -5,6 +5,7 @@ import { useMemo, useState } from "react";
import { Controller, useForm, useFormContext } from "react-hook-form";
import { z } from "zod";
import { emailSchema } from "@calcom/lib/emailSchema";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { MembershipRole } from "@calcom/prisma/enums";
import { trpc, type RouterOutputs } from "@calcom/trpc/react";
@@ -46,7 +47,7 @@ const attributeSchema = z.object({
const editSchema = z.object({
name: z.string(),
username: z.string(),
email: z.string().email(),
email: emailSchema,
avatar: z.string(),
bio: z.string(),
role: z.enum([MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER]),
+7
View File
@@ -0,0 +1,7 @@
import { z } from "zod";
/** @see https://github.com/colinhacks/zod/issues/3155#issuecomment-2060045794 */
export const emailRegex =
/^(?!\.)(?!.*\.\.)([A-Z0-9_+-\.']*)[A-Z0-9_+'-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i;
export const emailSchema = z.string().regex(emailRegex);
+3 -3
View File
@@ -202,7 +202,7 @@ model DestinationCalendar {
id Int @id @default(autoincrement())
integration String
externalId String
/// @zod.email()
/// @zod.custom(imports.emailSchema)
primaryEmail String?
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int? @unique
@@ -247,7 +247,7 @@ model User {
id Int @id @default(autoincrement())
username String?
name String?
/// @zod.email()
/// @zod.custom(imports.emailSchema)
email String
emailVerified DateTime?
password UserPassword?
@@ -578,7 +578,7 @@ model Booking {
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int?
// User's email at the time of booking
/// @zod.email()
/// @zod.custom(imports.emailSchema)
userPrimaryEmail String?
references BookingReference[]
eventType EventType? @relation(fields: [eventTypeId], references: [id])
+6 -4
View File
@@ -18,6 +18,7 @@ import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
import type { FieldType as FormBuilderFieldType } from "@calcom/features/form-builder/schema";
import { fieldsSchema as formBuilderFieldsSchema } from "@calcom/features/form-builder/schema";
import { isSupportedTimeZone } from "@calcom/lib/date-fns";
import { emailSchema as emailRegexSchema, emailRegex } from "@calcom/lib/emailSchema";
import { slugify } from "@calcom/lib/slugify";
import { EventTypeCustomInputType } from "@calcom/prisma/enums";
@@ -699,11 +700,12 @@ export const unlockedManagedEventTypeProps = {
destinationCalendar: allManagedEventTypeProps.destinationCalendar,
};
export const emailSchema = emailRegexSchema;
// The PR at https://github.com/colinhacks/zod/pull/2157 addresses this issue and improves email validation
// I introduced this refinement(to be used with z.email()) as a short term solution until we upgrade to a zod
// version that will include updates in the above PR.
export const emailSchemaRefinement = (value: string) => {
const emailRegex = /^([A-Z0-9_+-]+\.?)*[A-Z0-9_+-]@([A-Z0-9][A-Z0-9-]*\.)+[A-Z]{2,}$/i;
return emailRegex.test(value);
};
@@ -711,7 +713,7 @@ export const signupSchema = z.object({
// Username is marked optional here because it's requirement depends on if it's the Organization invite or a team invite which isn't easily done in zod
// It's better handled beyond zod in `validateAndGetCorrectedUsernameAndEmail`
username: z.string().optional(),
email: z.string().email({ message: "Invalid email" }),
email: z.string().regex(emailRegex, { message: "Invalid email" }),
password: z.string().superRefine((data, ctx) => {
const isStrict = false;
const result = isPasswordValid(data, true, isStrict);
@@ -730,7 +732,7 @@ export const signupSchema = z.object({
});
export const ZVerifyCodeInputSchema = z.object({
email: z.string().email(),
email: emailSchema,
code: z.string(),
});
@@ -743,4 +745,4 @@ export const getStringAsNumberRequiredSchema = (t: TFunction) =>
export const bookingSeatDataSchema = z.object({
description: z.string().optional(),
responses: bookingResponses,
});
});
@@ -1,7 +1,9 @@
import { z } from "zod";
import { emailSchema } from "@calcom/lib/emailSchema";
export const ZSamlTenantProductInputSchema = z.object({
email: z.string().email(),
email: emailSchema,
});
export type TSamlTenantProductInputSchema = z.infer<typeof ZSamlTenantProductInputSchema>;
@@ -1,5 +1,7 @@
import { z } from "zod";
import { emailSchema } from "@calcom/lib/emailSchema";
const BillingType = z.enum(["PER_BOOKING", "PER_USER"]);
const BillingPeriod = z.enum(["MONTHLY", "ANNUALLY"]);
@@ -9,7 +11,7 @@ export const ZCreateSelfHostedLicenseSchema = z.object({
entityPrice: z.number().nonnegative(),
billingPeriod: BillingPeriod,
overages: z.number().nonnegative(),
billingEmail: z.string().email(),
billingEmail: emailSchema,
});
export type TCreateSelfHostedLicenseSchema = z.infer<typeof ZCreateSelfHostedLicenseSchema>;
@@ -1,8 +1,10 @@
import { z } from "zod";
import { emailSchema } from "@calcom/lib/emailSchema";
export const ZResendVerifyEmailSchema = z
.object({
email: z.string().email(),
email: emailSchema,
})
.optional();
@@ -1,5 +1,6 @@
import { z } from "zod";
import { emailSchema } from "@calcom/lib/emailSchema";
import slugify from "@calcom/lib/slugify";
export enum BillingPeriod {
@@ -10,7 +11,7 @@ export enum BillingPeriod {
export const ZCreateInputSchema = z.object({
name: z.string(),
slug: z.string().transform((val) => slugify(val.trim())),
orgOwnerEmail: z.string().email(),
orgOwnerEmail: emailSchema,
language: z.string().optional(),
seats: z.number().optional(),
pricePerSeat: z.number().optional(),
@@ -1,6 +1,7 @@
import { z } from "zod";
import { MAX_NB_INVITES } from "@calcom/lib/constants";
import { emailSchema } from "@calcom/lib/emailSchema";
import { MembershipRole } from "@calcom/prisma/enums";
export const ZInviteMemberInputSchema = z.object({
@@ -12,7 +13,7 @@ export const ZInviteMemberInputSchema = z.object({
.union([
z.string(),
z.object({
email: z.string().email(),
email: emailSchema,
role: z.nativeEnum(MembershipRole),
}),
])
@@ -47,7 +48,7 @@ export const ZInviteMemberInputSchema = z.object({
.refine(
(value) => {
if (Array.isArray(value)) {
return !value.some((email) => !z.string().email().safeParse(email).success);
return !value.some((email) => !emailSchema.safeParse(email).success);
}
return true;
},
@@ -179,7 +179,9 @@ describe("Invite Member Utils", () => {
it("should not throw an error if the email is valid", () => {
const validEmail = "valid-email@example.com";
const validEmailWithApostrophe = "valid'email@example.com";
expect(() => checkInputEmailIsValid(validEmail)).not.toThrow();
expect(() => checkInputEmailIsValid(validEmailWithApostrophe)).not.toThrow();
});
});
describe("getOrgConnectionInfo", () => {
@@ -1,8 +1,10 @@
import { z } from "zod";
import { emailSchema } from "@calcom/lib/emailSchema";
export const ZResendInvitationInputSchema = z.object({
teamId: z.number(),
email: z.string().email(),
email: emailSchema,
language: z.string(),
isOrg: z.boolean().default(false),
});