import jwt from 'jsonwebtoken' import { randomUUID } from 'crypto' import { Resend } from 'resend' import PreRegistration from '../../../models/preRegistration.js' import { connectDB } from '../../../utils/mongoose.js' const resend = new Resend(useRuntimeConfig().resendApiKey) export default defineEventHandler(async (event) => { await requireAdmin(event) const { preRegistrantIds, emailTemplate } = await validateBody(event, preRegistrantInviteSchema) await connectDB() const baseUrl = process.env.BASE_URL if (!baseUrl) { throw createError({ statusCode: 500, statusMessage: 'BASE_URL environment variable is not set' }) } const config = useRuntimeConfig(event) const preRegs = await PreRegistration.find({ _id: { $in: preRegistrantIds } }) if (preRegs.length === 0) { throw createError({ statusCode: 404, statusMessage: 'No pre-registrants found for the provided IDs' }) } const results = [] for (const preReg of preRegs) { // Only send to pending/selected/invited (allow resend); skip accepted/expired if (preReg.status !== 'selected' && preReg.status !== 'pending' && preReg.status !== 'invited') { results.push({ preRegistrantId: preReg._id, email: preReg.email, success: false, error: `Skipped: status is ${preReg.status}`, }) continue } try { const jti = randomUUID() const token = jwt.sign( { preRegistrationId: preReg._id.toString(), jti, type: 'prereg-invite' }, config.jwtSecret, { expiresIn: '48h' }, ) // Token in fragment — never hits server logs const acceptLink = `${baseUrl}/accept-invite#${token}` const emailText = emailTemplate .replace(/\{name\}/g, preReg.name || 'there') .replace(/\{acceptLink\}/g, acceptLink) // Build HTML version const acceptButton = `Accept Your Invitation` const emailHtml = emailTemplate .replace(/&/g, '&') .replace(//g, '>') .replace(/\{name\}/g, preReg.name || 'there') .replace(/(https?:\/\/[^\s<]+)/g, '$1') .replace(/\n/g, '
') .replace(/\{acceptLink\}/g, acceptButton) const { error: emailError } = await resend.emails.send({ from: 'Ghost Guild ', to: [preReg.email], subject: "You're invited to Ghost Guild! 👻", text: emailText, html: emailHtml, }) if (emailError) { results.push({ preRegistrantId: preReg._id, email: preReg.email, success: false, error: emailError.message }) continue } await PreRegistration.findByIdAndUpdate(preReg._id, { $set: { magicLinkJti: jti, magicLinkJtiUsed: false, status: 'invited', inviteEmailSentAt: new Date(), }, }) results.push({ preRegistrantId: preReg._id, email: preReg.email, success: true }) } catch (err) { results.push({ preRegistrantId: preReg._id, email: preReg.email, success: false, error: err.message }) } } const sent = results.filter(r => r.success).length const failed = results.filter(r => !r.success).length return { sent, failed, results } })