import jwt from 'jsonwebtoken' import Member from '../models/member.js' import { connectDB } from './mongoose.js' /** * Issue a session JWT and set the auth-token cookie for the given member. * Mirrors the cookie options used by verify.post.js. */ export function setAuthCookie(event, member) { const token = jwt.sign( { memberId: member._id.toString(), email: member.email, tv: member.tokenVersion || 0 }, useRuntimeConfig(event).jwtSecret, { expiresIn: '7d' } ) setCookie(event, 'auth-token', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', maxAge: 60 * 60 * 24 * 7 }) } /** * Issue a 30-minute payment-bridge cookie scoped to membership-signup checkout. * * The signup flow (POST /api/helcim/customer) defers the full session cookie * to email-verify (magic link). For paid tiers the user still needs to complete * Helcim checkout in the same browser tab — this short-lived, payment-only * token lets `/api/helcim/initialize-payment` accept the call without a full * session. The cookie is NOT honored by requireAuth and grants nothing else. */ export function setPaymentBridgeCookie(event, member) { const token = jwt.sign( { memberId: member._id.toString(), email: member.email, scope: 'payment_bridge' }, useRuntimeConfig(event).jwtSecret, { expiresIn: '30m' } ) setCookie(event, 'payment-bridge', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', maxAge: 60 * 30 }) } /** * Verify a payment-bridge cookie and return the associated Member, or null. * Used by /api/helcim/initialize-payment to allow the membership-signup * checkout to proceed before email verification. */ export async function getPaymentBridgeMember(event) { const token = getCookie(event, 'payment-bridge') if (!token) return null let decoded try { decoded = jwt.verify(token, useRuntimeConfig(event).jwtSecret) } catch { return null } if (decoded.scope !== 'payment_bridge') return null await connectDB() const member = await Member.findById(decoded.memberId) if (!member) return null return member } /** * Verify JWT from cookie and return the decoded member. * Throws 401 if token is missing or invalid. */ export async function requireAuth(event) { await connectDB() const token = getCookie(event, 'auth-token') if (!token) { throw createError({ statusCode: 401, statusMessage: 'Authentication required' }) } let decoded try { decoded = jwt.verify(token, useRuntimeConfig().jwtSecret) } catch (err) { throw createError({ statusCode: 401, statusMessage: 'Invalid or expired token' }) } const member = await Member.findById(decoded.memberId) if (!member) { throw createError({ statusCode: 401, statusMessage: 'Member not found' }) } if (member.status === 'suspended' || member.status === 'cancelled') { throw createError({ statusCode: 403, statusMessage: 'Account is ' + member.status }) } // Verify session has not been revoked (tokenVersion incremented on logout) if (decoded.tv !== member.tokenVersion) { throw createError({ statusCode: 401, statusMessage: 'Session has been revoked' }) } return member } /** * Return the signed-in Member without throwing. * Returns null when no cookie, invalid token, or member not found/inactive. * Mirrors the non-throwing JWT check in api/auth/status.get.js. */ export async function getOptionalMember(event) { await connectDB() const token = getCookie(event, 'auth-token') if (!token) return null let decoded try { decoded = jwt.verify(token, useRuntimeConfig(event).jwtSecret) } catch { return null } const member = await Member.findById(decoded.memberId) if (!member) return null if (member.status === 'suspended' || member.status === 'cancelled') return null if (decoded.tv !== member.tokenVersion) return null return member } /** * Verify JWT and require admin role. * Throws 401 if not authenticated, 403 if not admin. */ export async function requireAdmin(event) { const member = await requireAuth(event) if (member.role !== 'admin') { throw createError({ statusCode: 403, statusMessage: 'Admin access required' }) } return member }