ghostguild-org/server/api/auth/verify.post.js

86 lines
2.1 KiB
JavaScript

// 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 }
})