From f0e533eb44c8a0cc9858ddb8f85e4b6ab3f59b26 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 22 Jul 2026 08:22:42 +0100 Subject: [PATCH 1/5] 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`, + }) + ); + }); +}); From a1d44f076b291f84a222bb2cded525234d7cb6ba Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 22 Jul 2026 08:27:06 +0100 Subject: [PATCH 2/5] refactor(webapp): sequence account email validation --- .../app/routes/account._index/route.tsx | 42 +++++++++---------- .../app/routes/confirm-basic-details.tsx | 38 ++++++++--------- 2 files changed, 38 insertions(+), 42 deletions(-) diff --git a/apps/webapp/app/routes/account._index/route.tsx b/apps/webapp/app/routes/account._index/route.tsx index 3becdb99f16..341141e638e 100644 --- a/apps/webapp/app/routes/account._index/route.tsx +++ b/apps/webapp/app/routes/account._index/route.tsx @@ -43,31 +43,29 @@ function createSchema( .string({ required_error: "You must enter a name" }) .min(2, "Your name must be at least 2 characters long") .max(50), - 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; - } - + email: emailSchema.pipe( + z.string().superRefine((email, ctx) => { + if (constraints.isEmailUnique === undefined) { + //client-side validation skips this ctx.addIssue({ code: z.ZodIssueCode.custom, - message: "Email is already being used by a different account", + 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: "Email is already being used by a different account", + }); + }); + } + }) + ), marketingEmails: z.preprocess((value) => value === "on", z.boolean()), }); } diff --git a/apps/webapp/app/routes/confirm-basic-details.tsx b/apps/webapp/app/routes/confirm-basic-details.tsx index ca14b53f0c4..4e370913240 100644 --- a/apps/webapp/app/routes/confirm-basic-details.tsx +++ b/apps/webapp/app/routes/confirm-basic-details.tsx @@ -73,29 +73,27 @@ function createSchema( return z .object({ name: z.string().min(3, "Your name must be at least 3 characters").max(50), - 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; - } - + email: emailSchema.pipe( + z.string().superRefine((email, ctx) => { + if (constraints.isEmailUnique === undefined) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: "Email is already being used by a different account", + 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", + }); }); - }); - } - }), + } + }) + ), confirmEmail: emailSchema, referralSource: z.string().optional(), referralSourceOther: z.string().optional(), From cfdf29de00f10efa121f768073a1d61ef9452237 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 22 Jul 2026 08:39:19 +0100 Subject: [PATCH 3/5] remove test --- apps/webapp/test/emailValidation.test.ts | 28 ------------------------ 1 file changed, 28 deletions(-) delete mode 100644 apps/webapp/test/emailValidation.test.ts diff --git a/apps/webapp/test/emailValidation.test.ts b/apps/webapp/test/emailValidation.test.ts deleted file mode 100644 index 968520feacb..00000000000 --- a/apps/webapp/test/emailValidation.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { emailSchema, MAX_EMAIL_LENGTH } from "~/utils/emailValidation"; - -function emailWithLength(length: number) { - const domain = "@example.com"; - return `${"a".repeat(length - domain.length)}${domain}`; -} - -describe("emailSchema", () => { - 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`, - }) - ); - }); -}); From b4ef0df532b63719398640e103b85fce12ca8481 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 22 Jul 2026 08:43:11 +0100 Subject: [PATCH 4/5] refactor(webapp): simplify account email schema --- apps/webapp/app/utils/emailValidation.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/utils/emailValidation.ts b/apps/webapp/app/utils/emailValidation.ts index e754551a462..2c4d3952db9 100644 --- a/apps/webapp/app/utils/emailValidation.ts +++ b/apps/webapp/app/utils/emailValidation.ts @@ -4,5 +4,5 @@ export const MAX_EMAIL_LENGTH = 254; export const emailSchema = z .string() - .max(MAX_EMAIL_LENGTH, `Email must be ${MAX_EMAIL_LENGTH} characters or fewer`) - .pipe(z.string().email()); + .email() + .max(MAX_EMAIL_LENGTH, `Email must be ${MAX_EMAIL_LENGTH} characters or fewer`); From b2e9439ab4127f518fe4969b43d2b9afbdd316a0 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 22 Jul 2026 09:02:38 +0100 Subject: [PATCH 5/5] docs(webapp): add account email length release note --- .server-changes/limit-account-email-length.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .server-changes/limit-account-email-length.md diff --git a/.server-changes/limit-account-email-length.md b/.server-changes/limit-account-email-length.md new file mode 100644 index 00000000000..2259f007e06 --- /dev/null +++ b/.server-changes/limit-account-email-length.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +Limit account settings email input to 254 characters.