fix: case-insensitive email matching in auth and password reset
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m10s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m10s
Email lookups used findUnique (case-sensitive on PostgreSQL) but user input was lowercased, causing login failures for users with mixed-case emails stored in the DB (e.g. Laurent_Faure@dietsmann.com). Also normalized 7 affected emails to lowercase on the production DB. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,8 +24,8 @@ export async function POST(req: NextRequest) {
|
|||||||
return NextResponse.json({ exists: false }, { status: 400 })
|
return NextResponse.json({ exists: false }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findFirst({
|
||||||
where: { email: email.toLowerCase().trim() },
|
where: { email: { equals: email.toLowerCase().trim(), mode: 'insensitive' } },
|
||||||
select: { status: true },
|
select: { status: true },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|||||||
// Send magic links to any existing, non-suspended user.
|
// Send magic links to any existing, non-suspended user.
|
||||||
// This is the primary first-login path for applicants seeded from CSV
|
// This is the primary first-login path for applicants seeded from CSV
|
||||||
// (status NONE) who have no password yet.
|
// (status NONE) who have no password yet.
|
||||||
const existingUser = await prisma.user.findUnique({
|
const existingUser = await prisma.user.findFirst({
|
||||||
where: { email: email.toLowerCase().trim() },
|
where: { email: { equals: email.toLowerCase().trim(), mode: 'insensitive' } },
|
||||||
select: { id: true, status: true },
|
select: { id: true, status: true },
|
||||||
})
|
})
|
||||||
if (!existingUser || existingUser.status === 'SUSPENDED') {
|
if (!existingUser || existingUser.status === 'SUSPENDED') {
|
||||||
@@ -164,9 +164,9 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|||||||
throw new Error('Account temporarily locked due to too many failed attempts. Try again later.')
|
throw new Error('Account temporarily locked due to too many failed attempts. Try again later.')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find user by email
|
// Find user by email (case-insensitive — DB may store mixed-case emails)
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findFirst({
|
||||||
where: { email },
|
where: { email: { equals: email, mode: 'insensitive' } },
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
email: true,
|
email: true,
|
||||||
@@ -371,8 +371,8 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|||||||
async signIn({ user, account }) {
|
async signIn({ user, account }) {
|
||||||
// For email provider (magic link), check user status and get password info
|
// For email provider (magic link), check user status and get password info
|
||||||
if (account?.provider === 'email') {
|
if (account?.provider === 'email') {
|
||||||
const dbUser = await prisma.user.findUnique({
|
const dbUser = await prisma.user.findFirst({
|
||||||
where: { email: user.email! },
|
where: { email: { equals: user.email!, mode: 'insensitive' } },
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
status: true,
|
status: true,
|
||||||
@@ -404,22 +404,24 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|||||||
|
|
||||||
// Update last login time on actual sign-in, and activate INVITED users on login
|
// Update last login time on actual sign-in, and activate INVITED users on login
|
||||||
if (user.email) {
|
if (user.email) {
|
||||||
const loginUser = await prisma.user.findUnique({
|
const loginUser = await prisma.user.findFirst({
|
||||||
where: { email: user.email },
|
where: { email: { equals: user.email, mode: 'insensitive' } },
|
||||||
select: { status: true },
|
select: { id: true, status: true },
|
||||||
})
|
})
|
||||||
|
if (loginUser) {
|
||||||
await prisma.user.update({
|
await prisma.user.update({
|
||||||
where: { email: user.email },
|
where: { id: loginUser.id },
|
||||||
data: {
|
data: {
|
||||||
lastLoginAt: new Date(),
|
lastLoginAt: new Date(),
|
||||||
// If user is still INVITED but successfully logged in, activate them
|
// If user is still INVITED but successfully logged in, activate them
|
||||||
...(loginUser && loginUser.status === 'INVITED'
|
...(loginUser.status === 'INVITED'
|
||||||
? { status: 'ACTIVE' }
|
? { status: 'ACTIVE' }
|
||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
// Ignore errors from updating last login
|
// Ignore errors from updating last login
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Log successful login
|
// Log successful login
|
||||||
await prisma.auditLog.create({
|
await prisma.auditLog.create({
|
||||||
|
|||||||
@@ -1595,9 +1595,9 @@ export const userRouter = router({
|
|||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const email = input.email.toLowerCase().trim()
|
const email = input.email.toLowerCase().trim()
|
||||||
|
|
||||||
// Find user by email
|
// Find user by email (case-insensitive — DB may store mixed-case emails)
|
||||||
const user = await ctx.prisma.user.findUnique({
|
const user = await ctx.prisma.user.findFirst({
|
||||||
where: { email },
|
where: { email: { equals: email, mode: 'insensitive' } },
|
||||||
select: { id: true, email: true, name: true, status: true },
|
select: { id: true, email: true, name: true, status: true },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user