From f0e533eb44c8a0cc9858ddb8f85e4b6ab3f59b26 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 22 Jul 2026 08:22:42 +0100 Subject: [PATCH] fix(webapp): limit account email address length --- .../app/routes/account._index/route.tsx | 45 ++++++++++--------- .../app/routes/confirm-basic-details.tsx | 44 +++++++++--------- apps/webapp/app/utils/emailValidation.ts | 8 ++++ apps/webapp/test/emailValidation.test.ts | 28 ++++++++++++ 4 files changed, 84 insertions(+), 41 deletions(-) create mode 100644 apps/webapp/app/utils/emailValidation.ts create mode 100644 apps/webapp/test/emailValidation.test.ts diff --git a/apps/webapp/app/routes/account._index/route.tsx b/apps/webapp/app/routes/account._index/route.tsx index 99ef9b87bb3..3becdb99f16 100644 --- a/apps/webapp/app/routes/account._index/route.tsx +++ b/apps/webapp/app/routes/account._index/route.tsx @@ -22,6 +22,7 @@ import { useUser } from "~/hooks/useUser"; import { redirectWithSuccessMessage } from "~/models/message.server"; import { updateUser } from "~/models/user.server"; import { requireUserId } from "~/services/session.server"; +import { emailSchema, MAX_EMAIL_LENGTH } from "~/utils/emailValidation"; import { accountPath } from "~/utils/pathBuilder"; export const meta: MetaFunction = () => { @@ -42,30 +43,31 @@ function createSchema( .string({ required_error: "You must enter a name" }) .min(2, "Your name must be at least 2 characters long") .max(50), - email: z - .string() - .email() - .superRefine((email, ctx) => { - if (constraints.isEmailUnique === undefined) { - //client-side validation skips this + email: emailSchema.superRefine((email, ctx) => { + if (email.length > MAX_EMAIL_LENGTH) { + return; + } + + if (constraints.isEmailUnique === undefined) { + //client-side validation skips this + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: conformZodMessage.VALIDATION_UNDEFINED, + }); + } else { + // Tell zod this is an async validation by returning the promise + return constraints.isEmailUnique(email).then((isUnique) => { + if (isUnique) { + return; + } + ctx.addIssue({ code: z.ZodIssueCode.custom, - message: conformZodMessage.VALIDATION_UNDEFINED, + message: "Email is already being used by a different account", }); - } else { - // Tell zod this is an async validation by returning the promise - return constraints.isEmailUnique(email).then((isUnique) => { - if (isUnique) { - return; - } - - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Email is already being used by a different account", - }); - }); - } - }), + }); + } + }), marketingEmails: z.preprocess((value) => value === "on", z.boolean()), }); } @@ -177,6 +179,7 @@ export default function Page() {
diff --git a/apps/webapp/app/routes/confirm-basic-details.tsx b/apps/webapp/app/routes/confirm-basic-details.tsx index 9187823a734..ca14b53f0c4 100644 --- a/apps/webapp/app/routes/confirm-basic-details.tsx +++ b/apps/webapp/app/routes/confirm-basic-details.tsx @@ -27,6 +27,7 @@ import { useUser } from "~/hooks/useUser"; import { redirectWithSuccessMessage } from "~/models/message.server"; import { updateUser } from "~/models/user.server"; import { requireUserId } from "~/services/session.server"; +import { emailSchema, MAX_EMAIL_LENGTH } from "~/utils/emailValidation"; import { rootPath } from "~/utils/pathBuilder"; import { getVercelInstallParams } from "~/v3/vercel"; @@ -72,29 +73,30 @@ function createSchema( return z .object({ name: z.string().min(3, "Your name must be at least 3 characters").max(50), - email: z - .string() - .email() - .superRefine((email, ctx) => { - if (constraints.isEmailUnique === undefined) { + email: emailSchema.superRefine((email, ctx) => { + if (email.length > MAX_EMAIL_LENGTH) { + return; + } + + if (constraints.isEmailUnique === undefined) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: conformZodMessage.VALIDATION_UNDEFINED, + }); + } else { + return constraints.isEmailUnique(email).then((isUnique) => { + if (isUnique) { + return; + } + ctx.addIssue({ code: z.ZodIssueCode.custom, - message: conformZodMessage.VALIDATION_UNDEFINED, - }); - } else { - return constraints.isEmailUnique(email).then((isUnique) => { - if (isUnique) { - return; - } - - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Email is already being used by a different account", - }); + message: "Email is already being used by a different account", }); - } - }), - confirmEmail: z.string(), + }); + } + }), + confirmEmail: emailSchema, referralSource: z.string().optional(), referralSourceOther: z.string().optional(), role: z.string().optional(), @@ -290,6 +292,7 @@ export default function Page() { { setEnteredEmail(e.target.value); @@ -306,6 +309,7 @@ export default function Page() { { + it("accepts an email at the maximum length", () => { + expect(emailSchema.safeParse(emailWithLength(MAX_EMAIL_LENGTH)).success).toBe(true); + }); + + it("rejects an email over the maximum length", () => { + const result = emailSchema.safeParse(emailWithLength(MAX_EMAIL_LENGTH + 1)); + + expect(result.success).toBe(false); + if (result.success) { + throw new Error("Expected an overlong email to be rejected"); + } + + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + message: `Email must be ${MAX_EMAIL_LENGTH} characters or fewer`, + }) + ); + }); +});