ghostguild-org/server/api/auth/verify.post.js
Jennie Robinson Faber 8e76ce9366
Some checks failed
Test / vitest (push) Successful in 11m49s
Test / playwright (push) Failing after 9m43s
Test / visual (push) Failing after 9m24s
Test / Notify on failure (push) Successful in 2s
refactor(launch): collapse helcim-pay duplication and use setAuthCookie helper
Follow-up to 51230e5. /simplify review surfaced residual duplication
and a timer leak.

- useHelcimPay: extract _initializeTicket(metadata, errorPrefix) to
  collapse initializeTicketPayment + initializeSeriesTicketPayment
  (95% identical bodies). Drop the dead `amount` arg from initialize-
  TicketPayment — server re-derives ticket amounts in initialize-
  payment.post.js and never reads body.amount for ticket types.
  Capture timer ids and clearTimeout on resolve/reject so the 10-min
  payment timer and 5-second observer timer stop leaking after every
  payment.
- EventTicketPurchase: caller updated for the dropped arg.
- verify.post.js: replace inline jwt.sign + setCookie block with the
  setAuthCookie(event, member) helper. verify was the last hand-rolled
  caller after the helper was extracted in 208638e.
- LAUNCH_READINESS: add simplify-pass-followups bullet pointing to the
  six deferred items in docs/TODO.md.

Tests: 758 passing, 2 skipped, 0 failing.
2026-04-25 22:13:24 +01:00

65 lines
1.7 KiB
JavaScript

// 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'
import { setAuthCookie } from '../../utils/auth.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 }
)
setAuthCookie(event, member)
const redirectUrl = member.role === 'admin' ? '/admin' : '/member/dashboard'
return { success: true, redirectUrl }
})