From be4322147258d449fc001a762caf32f6c76e700c Mon Sep 17 00:00:00 2001 From: NickMonrad Date: Wed, 3 Jun 2026 10:11:31 +1000 Subject: [PATCH] fix: correct password reset endpoint query logic The original code tried to use a compound where clause with usedAt: null which Prisma cannot handle reliably. Changed to: 1. Find token by tokenHash alone 2. Verify usedAt is null explicitly 3. Use a transaction to atomically mark token as used and update password This fixes the 'Invalid or expired reset token' error that was preventing password resets from working. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- server/src/routes/auth.ts | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 584bbfce..ef5a0478 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -131,22 +131,29 @@ router.post('/reset-password', validate(resetPasswordSchema), asyncHandler(async const tokenHash = crypto.createHash('sha256').update(token).digest('hex') - // Atomic update: only succeeds if usedAt IS NULL (prevents race condition) - const resetToken = await prisma.passwordResetToken.update({ - where: { tokenHash, usedAt: null }, - data: { usedAt: new Date() }, - }).catch(() => null) + // Fetch the token record + const resetToken = await prisma.passwordResetToken.findUnique({ + where: { tokenHash }, + }) - if (!resetToken || resetToken.expiresAt < new Date()) { + if (!resetToken || resetToken.usedAt !== null || resetToken.expiresAt < new Date()) { res.status(400).json({ error: 'Invalid or expired reset token' }) return } const hashed = await bcrypt.hash(password, 10) - await prisma.user.update({ - where: { id: resetToken.userId }, - data: { password: hashed }, + // Mark token as used and update password atomically + await prisma.$transaction(async (tx) => { + await tx.passwordResetToken.update({ + where: { tokenHash }, + data: { usedAt: new Date() }, + }) + + await tx.user.update({ + where: { id: resetToken.userId }, + data: { password: hashed }, + }) }) logger.info({ userId: resetToken.userId }, 'Password reset successfully')