From 79c712a9e929cfa09dd5e5ca3cc5ff785ad56a82 Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Sat, 4 Apr 2026 12:24:52 +0100 Subject: [PATCH] fix: replace member.save() with atomic update in verify --- server/api/auth/verify.post.js | 86 ++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 server/api/auth/verify.post.js diff --git a/server/api/auth/verify.post.js b/server/api/auth/verify.post.js new file mode 100644 index 0000000..5673cf1 --- /dev/null +++ b/server/api/auth/verify.post.js @@ -0,0 +1,86 @@ +// server/api/auth/verify.post.js +import jwt from 'jsonwebtoken' +import Member from '../../models/member.js' +import { connectDB } from '../../utils/mongoose.js' + +export default defineEventHandler(async (event) => { + await connectDB() + + const body = await readBody(event) + const token = body?.token + + if (!token) { + throw createError({ + statusCode: 400, + statusMessage: 'Token is required', + }) + } + + const config = useRuntimeConfig(event) + + let decoded + try { + decoded = jwt.verify(token, config.jwtSecret) + } catch { + throw createError({ + statusCode: 401, + statusMessage: 'Invalid or expired token', + }) + } + + const member = await Member.findById(decoded.memberId) + + if (!member) { + throw createError({ + statusCode: 401, + statusMessage: 'Invalid or expired token', + }) + } + + if (member.status === 'suspended') { + throw createError({ + statusCode: 403, + statusMessage: 'Account is suspended', + }) + } + + if (member.status === 'cancelled') { + throw createError({ + statusCode: 403, + statusMessage: 'Account is cancelled', + }) + } + + // Single-use enforcement: jti must match and must not have been used + if (!decoded.jti || decoded.jti !== member.magicLinkJti || member.magicLinkJtiUsed) { + throw createError({ + statusCode: 401, + statusMessage: 'Invalid or expired token', + }) + } + + // Atomically burn the token before issuing session + await Member.findByIdAndUpdate( + member._id, + { $set: { magicLinkJtiUsed: true, lastLogin: new Date() } }, + { runValidators: false } + ) + + // Issue session token with tokenVersion claim for revocation support + const sessionToken = jwt.sign( + { memberId: member._id, email: member.email, tv: member.tokenVersion }, + config.jwtSecret, + { expiresIn: '7d' }, + ) + + setCookie(event, 'auth-token', sessionToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/', + maxAge: 60 * 60 * 24 * 7, // 7 days + }) + + const redirectUrl = member.role === 'admin' ? '/admin' : '/member/dashboard' + return { success: true, redirectUrl } +})