Skip to content

Commit f0e533e

Browse files
committed
fix(webapp): limit account email address length
1 parent 95307ba commit f0e533e

4 files changed

Lines changed: 84 additions & 41 deletions

File tree

apps/webapp/app/routes/account._index/route.tsx

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { useUser } from "~/hooks/useUser";
2222
import { redirectWithSuccessMessage } from "~/models/message.server";
2323
import { updateUser } from "~/models/user.server";
2424
import { requireUserId } from "~/services/session.server";
25+
import { emailSchema, MAX_EMAIL_LENGTH } from "~/utils/emailValidation";
2526
import { accountPath } from "~/utils/pathBuilder";
2627

2728
export const meta: MetaFunction = () => {
@@ -42,30 +43,31 @@ function createSchema(
4243
.string({ required_error: "You must enter a name" })
4344
.min(2, "Your name must be at least 2 characters long")
4445
.max(50),
45-
email: z
46-
.string()
47-
.email()
48-
.superRefine((email, ctx) => {
49-
if (constraints.isEmailUnique === undefined) {
50-
//client-side validation skips this
46+
email: emailSchema.superRefine((email, ctx) => {
47+
if (email.length > MAX_EMAIL_LENGTH) {
48+
return;
49+
}
50+
51+
if (constraints.isEmailUnique === undefined) {
52+
//client-side validation skips this
53+
ctx.addIssue({
54+
code: z.ZodIssueCode.custom,
55+
message: conformZodMessage.VALIDATION_UNDEFINED,
56+
});
57+
} else {
58+
// Tell zod this is an async validation by returning the promise
59+
return constraints.isEmailUnique(email).then((isUnique) => {
60+
if (isUnique) {
61+
return;
62+
}
63+
5164
ctx.addIssue({
5265
code: z.ZodIssueCode.custom,
53-
message: conformZodMessage.VALIDATION_UNDEFINED,
66+
message: "Email is already being used by a different account",
5467
});
55-
} else {
56-
// Tell zod this is an async validation by returning the promise
57-
return constraints.isEmailUnique(email).then((isUnique) => {
58-
if (isUnique) {
59-
return;
60-
}
61-
62-
ctx.addIssue({
63-
code: z.ZodIssueCode.custom,
64-
message: "Email is already being used by a different account",
65-
});
66-
});
67-
}
68-
}),
68+
});
69+
}
70+
}),
6971
marketingEmails: z.preprocess((value) => value === "on", z.boolean()),
7072
});
7173
}
@@ -177,6 +179,7 @@ export default function Page() {
177179
<div className="flex w-56 flex-none flex-col gap-1">
178180
<Input
179181
{...getInputProps(email, { type: "text" })}
182+
maxLength={MAX_EMAIL_LENGTH}
180183
placeholder="Your email"
181184
defaultValue={user?.email ?? ""}
182185
/>

apps/webapp/app/routes/confirm-basic-details.tsx

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { useUser } from "~/hooks/useUser";
2727
import { redirectWithSuccessMessage } from "~/models/message.server";
2828
import { updateUser } from "~/models/user.server";
2929
import { requireUserId } from "~/services/session.server";
30+
import { emailSchema, MAX_EMAIL_LENGTH } from "~/utils/emailValidation";
3031
import { rootPath } from "~/utils/pathBuilder";
3132
import { getVercelInstallParams } from "~/v3/vercel";
3233

@@ -72,29 +73,30 @@ function createSchema(
7273
return z
7374
.object({
7475
name: z.string().min(3, "Your name must be at least 3 characters").max(50),
75-
email: z
76-
.string()
77-
.email()
78-
.superRefine((email, ctx) => {
79-
if (constraints.isEmailUnique === undefined) {
76+
email: emailSchema.superRefine((email, ctx) => {
77+
if (email.length > MAX_EMAIL_LENGTH) {
78+
return;
79+
}
80+
81+
if (constraints.isEmailUnique === undefined) {
82+
ctx.addIssue({
83+
code: z.ZodIssueCode.custom,
84+
message: conformZodMessage.VALIDATION_UNDEFINED,
85+
});
86+
} else {
87+
return constraints.isEmailUnique(email).then((isUnique) => {
88+
if (isUnique) {
89+
return;
90+
}
91+
8092
ctx.addIssue({
8193
code: z.ZodIssueCode.custom,
82-
message: conformZodMessage.VALIDATION_UNDEFINED,
83-
});
84-
} else {
85-
return constraints.isEmailUnique(email).then((isUnique) => {
86-
if (isUnique) {
87-
return;
88-
}
89-
90-
ctx.addIssue({
91-
code: z.ZodIssueCode.custom,
92-
message: "Email is already being used by a different account",
93-
});
94+
message: "Email is already being used by a different account",
9495
});
95-
}
96-
}),
97-
confirmEmail: z.string(),
96+
});
97+
}
98+
}),
99+
confirmEmail: emailSchema,
98100
referralSource: z.string().optional(),
99101
referralSourceOther: z.string().optional(),
100102
role: z.string().optional(),
@@ -290,6 +292,7 @@ export default function Page() {
290292
</Label>
291293
<Input
292294
{...getInputProps(email, { type: "email" })}
295+
maxLength={MAX_EMAIL_LENGTH}
293296
defaultValue={enteredEmail}
294297
onChange={(e) => {
295298
setEnteredEmail(e.target.value);
@@ -306,6 +309,7 @@ export default function Page() {
306309
<Label htmlFor={confirmEmail.id}>Confirm email</Label>
307310
<Input
308311
{...getInputProps(confirmEmail, { type: "email" })}
312+
maxLength={MAX_EMAIL_LENGTH}
309313
placeholder="Your email, again"
310314
icon={EnvelopeIcon}
311315
spellCheck={false}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { z } from "zod";
2+
3+
export const MAX_EMAIL_LENGTH = 254;
4+
5+
export const emailSchema = z
6+
.string()
7+
.max(MAX_EMAIL_LENGTH, `Email must be ${MAX_EMAIL_LENGTH} characters or fewer`)
8+
.pipe(z.string().email());
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { describe, expect, it } from "vitest";
2+
import { emailSchema, MAX_EMAIL_LENGTH } from "~/utils/emailValidation";
3+
4+
function emailWithLength(length: number) {
5+
const domain = "@example.com";
6+
return `${"a".repeat(length - domain.length)}${domain}`;
7+
}
8+
9+
describe("emailSchema", () => {
10+
it("accepts an email at the maximum length", () => {
11+
expect(emailSchema.safeParse(emailWithLength(MAX_EMAIL_LENGTH)).success).toBe(true);
12+
});
13+
14+
it("rejects an email over the maximum length", () => {
15+
const result = emailSchema.safeParse(emailWithLength(MAX_EMAIL_LENGTH + 1));
16+
17+
expect(result.success).toBe(false);
18+
if (result.success) {
19+
throw new Error("Expected an overlong email to be rejected");
20+
}
21+
22+
expect(result.error.issues).toContainEqual(
23+
expect.objectContaining({
24+
message: `Email must be ${MAX_EMAIL_LENGTH} characters or fewer`,
25+
})
26+
);
27+
});
28+
});

0 commit comments

Comments
 (0)