Skip to content

Commit 99af812

Browse files
committed
Normalize emails to lowercase across auth
1 parent 4b6a718 commit 99af812

8 files changed

Lines changed: 44 additions & 36 deletions

File tree

apps/web/app/(org)/login/form.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -269,8 +269,9 @@ export function LoginForm() {
269269
method: "email",
270270
is_signup: !oauthError,
271271
});
272+
const normalizedEmail = email.trim().toLowerCase();
272273
signIn("email", {
273-
email,
274+
email: normalizedEmail,
274275
redirect: false,
275276
...(next && next.length > 0
276277
? { callbackUrl: next }
@@ -283,10 +284,10 @@ export function LoginForm() {
283284
setEmailSent(true);
284285
setLastEmailSentTime(Date.now());
285286
trackEvent("auth_email_sent", {
286-
email_domain: email.split("@")[1],
287+
email_domain: normalizedEmail.split("@")[1],
287288
});
288289
const params = new URLSearchParams({
289-
email,
290+
email: normalizedEmail,
290291
...(next && { next }),
291292
lastSent: Date.now().toString(),
292293
});
@@ -421,7 +422,7 @@ const NormalLogin = ({
421422
value={email}
422423
disabled={emailSent || loading}
423424
onChange={(e) => {
424-
setEmail(e.target.value);
425+
setEmail(e.target.value.toLowerCase());
425426
}}
426427
/>
427428
<MotionButton

apps/web/app/(org)/signup/form.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -269,8 +269,9 @@ export function SignupForm() {
269269
method: "email",
270270
is_signup: true,
271271
});
272+
const normalizedEmail = email.trim().toLowerCase();
272273
signIn("email", {
273-
email,
274+
email: normalizedEmail,
274275
redirect: false,
275276
...(next && next.length > 0
276277
? { callbackUrl: next }
@@ -283,10 +284,10 @@ export function SignupForm() {
283284
setEmailSent(true);
284285
setLastEmailSentTime(Date.now());
285286
trackEvent("auth_email_sent", {
286-
email_domain: email.split("@")[1],
287+
email_domain: normalizedEmail.split("@")[1],
287288
});
288289
const params = new URLSearchParams({
289-
email,
290+
email: normalizedEmail,
290291
...(next && { next }),
291292
lastSent: Date.now().toString(),
292293
});
@@ -435,7 +436,7 @@ const NormalSignup = ({
435436
value={email}
436437
disabled={emailSent || loading}
437438
onChange={(e) => {
438-
setEmail(e.target.value);
439+
setEmail(e.target.value.toLowerCase());
439440
}}
440441
/>
441442
<MotionButton

apps/web/app/(org)/verify-otp/form.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,15 @@ export function VerifyOTPForm({
7070
}
7171
};
7272

73+
const normalizedEmail = email.toLowerCase();
74+
7375
const handleVerify = useMutation({
7476
mutationFn: async () => {
7577
const otpCode = code.join("");
7678
if (otpCode.length !== 6) throw "Please enter a complete 6-digit code";
7779

78-
// shoutout https://github.com/buoyad/Tally/pull/14
7980
const res = await fetch(
80-
`/api/auth/callback/email?email=${encodeURIComponent(email)}&token=${encodeURIComponent(otpCode)}&callbackUrl=${encodeURIComponent("/login-success")}`,
81+
`/api/auth/callback/email?email=${encodeURIComponent(normalizedEmail)}&token=${encodeURIComponent(otpCode)}&callbackUrl=${encodeURIComponent("/login-success")}`,
8182
);
8283

8384
if (!res.url.includes("/login-success")) {
@@ -115,7 +116,7 @@ export function VerifyOTPForm({
115116
}
116117

117118
const result = await signIn("email", {
118-
email,
119+
email: normalizedEmail,
119120
redirect: false,
120121
});
121122

@@ -164,7 +165,7 @@ export function VerifyOTPForm({
164165
Enter verification code
165166
</h1>
166167
<p className="text-sm text-gray-10">
167-
We sent a 6-digit code to {email}
168+
We sent a 6-digit code to {normalizedEmail}
168169
</p>
169170
</div>
170171

apps/web/app/(org)/verify-otp/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export default async function VerifyOTPPage(props: {
2525
<div className="flex h-screen w-full items-center justify-center">
2626
<Suspense fallback={null}>
2727
<VerifyOTPForm
28-
email={searchParams.email}
28+
email={searchParams.email?.toLowerCase() ?? ""}
2929
next={searchParams.next}
3030
lastSent={searchParams.lastSent}
3131
/>

apps/web/app/s/[videoId]/_components/AuthOverlay.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ const StepOne = ({
154154

155155
setLoading(true);
156156
signIn("email", {
157-
email,
157+
email: email.trim().toLowerCase(),
158158
redirect: false,
159159
})
160160
.then((res) => {
@@ -189,7 +189,7 @@ const StepOne = ({
189189
value={email}
190190
disabled={emailSent || loading}
191191
onChange={(e) => {
192-
setEmail(e.target.value);
192+
setEmail(e.target.value.toLowerCase());
193193
}}
194194
/>
195195
</div>

apps/web/app/s/[videoId]/_components/OtpForm.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,15 @@ const OtpForm = ({
7070
}
7171
};
7272

73+
const normalizedEmail = email.toLowerCase();
74+
7375
const handleVerify = useMutation({
7476
mutationFn: async () => {
7577
const otpCode = code.join("");
7678
if (otpCode.length !== 6) throw "Please enter a complete 6-digit code";
7779

7880
const res = await fetch(
79-
`/api/auth/callback/email?email=${encodeURIComponent(email)}&token=${encodeURIComponent(otpCode)}&callbackUrl=${encodeURIComponent("/login-success")}`,
81+
`/api/auth/callback/email?email=${encodeURIComponent(normalizedEmail)}&token=${encodeURIComponent(otpCode)}&callbackUrl=${encodeURIComponent("/login-success")}`,
8082
);
8183

8284
if (!res.url.includes("/login-success")) {
@@ -114,7 +116,7 @@ const OtpForm = ({
114116
}
115117

116118
const result = await signIn("email", {
117-
email,
119+
email: normalizedEmail,
118120
redirect: false,
119121
});
120122

packages/database/auth/auth-options.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,15 +122,15 @@ export const authOptions = (): NextAuthOptions => {
122122
const allowedDomains = serverEnv().CAP_ALLOWED_SIGNUP_DOMAINS;
123123
if (!allowedDomains) return true;
124124

125-
// Get email from either user object (OAuth) or email parameter (email provider)
126-
const userEmail =
125+
const rawEmail =
127126
user?.email ||
128127
(typeof email === "string"
129128
? email
130129
: typeof credentials?.email === "string"
131130
? credentials.email
132131
: null);
133-
if (!userEmail || typeof userEmail !== "string") return true;
132+
if (!rawEmail || typeof rawEmail !== "string") return true;
133+
const userEmail = rawEmail.toLowerCase();
134134

135135
const [existingUser] = await db()
136136
.select()
@@ -172,7 +172,7 @@ export const authOptions = (): NextAuthOptions => {
172172
image: users.image,
173173
})
174174
.from(users)
175-
.where(eq(users.email, token.email || ""))
175+
.where(eq(users.email, (token.email || "").toLowerCase()))
176176
.limit(1);
177177

178178
if (!dbUser) {

packages/database/auth/drizzle-adapter.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,23 @@ import {
1818
export function DrizzleAdapter(db: MySql2Database): Adapter {
1919
return {
2020
async createUser(userData: any) {
21+
const normalizedEmail = (userData.email as string)?.toLowerCase() ?? "";
2122
const userId = User.UserId.make(nanoId());
2223
await db.transaction(async (tx) => {
2324
const [pendingInvite] = await tx
2425
.select({ id: organizationInvites.id })
2526
.from(organizationInvites)
2627
.where(
2728
and(
28-
eq(organizationInvites.invitedEmail, userData.email),
29+
eq(organizationInvites.invitedEmail, normalizedEmail),
2930
eq(organizationInvites.status, "pending"),
3031
),
3132
)
3233
.limit(1);
3334

3435
await tx.insert(users).values({
3536
id: userId,
36-
email: userData.email,
37+
email: normalizedEmail,
3738
emailVerified: userData.emailVerified,
3839
name: userData.name,
3940
image: userData.image,
@@ -78,7 +79,7 @@ export function DrizzleAdapter(db: MySql2Database): Adapter {
7879

7980
if (STRIPE_AVAILABLE()) {
8081
const existingCustomers = await stripe().customers.list({
81-
email: userData.email,
82+
email: normalizedEmail,
8283
limit: 1,
8384
});
8485

@@ -94,7 +95,7 @@ export function DrizzleAdapter(db: MySql2Database): Adapter {
9495
});
9596
} else {
9697
customer = await stripe().customers.create({
97-
email: userData.email,
98+
email: normalizedEmail,
9899
metadata: {
99100
userId: row.id,
100101
},
@@ -153,10 +154,11 @@ export function DrizzleAdapter(db: MySql2Database): Adapter {
153154
return row ?? null;
154155
},
155156
async getUserByEmail(email) {
157+
const normalizedEmail = email?.toLowerCase() ?? "";
156158
const rows = await db
157159
.select()
158160
.from(users)
159-
.where(eq(users.email, email))
161+
.where(eq(users.email, normalizedEmail))
160162
.limit(1)
161163
.catch((e) => {
162164
throw e;
@@ -288,10 +290,12 @@ export function DrizzleAdapter(db: MySql2Database): Adapter {
288290
await db.delete(sessions).where(eq(sessions.sessionToken, sessionToken));
289291
},
290292
async createVerificationToken(verificationToken) {
293+
const normalizedIdentifier =
294+
verificationToken.identifier?.toLowerCase() ?? "";
291295
const existingTokens = await db
292296
.select()
293297
.from(verificationTokens)
294-
.where(eq(verificationTokens.identifier, verificationToken.identifier))
298+
.where(eq(verificationTokens.identifier, normalizedIdentifier))
295299
.limit(1);
296300

297301
if (existingTokens.length > 0) {
@@ -301,23 +305,19 @@ export function DrizzleAdapter(db: MySql2Database): Adapter {
301305
token: verificationToken.token,
302306
expires: verificationToken.expires,
303307
})
304-
.where(
305-
eq(verificationTokens.identifier, verificationToken.identifier),
306-
);
308+
.where(eq(verificationTokens.identifier, normalizedIdentifier));
307309

308310
return await db
309311
.select()
310312
.from(verificationTokens)
311-
.where(
312-
eq(verificationTokens.identifier, verificationToken.identifier),
313-
)
313+
.where(eq(verificationTokens.identifier, normalizedIdentifier))
314314
.limit(1)
315315
.then((rows) => rows[0]);
316316
}
317317

318318
await db.insert(verificationTokens).values({
319319
expires: verificationToken.expires,
320-
identifier: verificationToken.identifier,
320+
identifier: normalizedIdentifier,
321321
token: verificationToken.token,
322322
});
323323

@@ -338,15 +338,18 @@ export function DrizzleAdapter(db: MySql2Database): Adapter {
338338
.limit(1);
339339
const row = rows[0];
340340
if (!row) return null;
341+
const normalizedIdentifier = identifier?.toLowerCase() ?? "";
342+
const storedIdentifier = row.identifier?.toLowerCase() ?? "";
343+
if (normalizedIdentifier !== storedIdentifier) return null;
341344
await db
342345
.delete(verificationTokens)
343346
.where(
344347
and(
345348
eq(verificationTokens.token, token),
346-
eq(verificationTokens.identifier, identifier),
349+
eq(verificationTokens.identifier, row.identifier),
347350
),
348351
);
349-
return row;
352+
return { ...row, identifier: storedIdentifier };
350353
},
351354
};
352355
}

0 commit comments

Comments
 (0)