// server/api/auth/verify.post.js import jwt from 'jsonwebtoken' import Member from '../../models/member.js' import { validateBody } from '../../utils/validateBody.js' import { verifyMagicLinkSchema } from '../../utils/schemas.js' export default defineEventHandler(async (event) => { const { token } = await validateBody(event, verifyMagicLinkSchema) 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 } })