Admin interface to review, filter, and batch-invite the 95 pre-registrants from Baby Ghosts. Accept-invitation page pre-fills their data and collects circle, pronouns, motivation, contribution tier, and agreement before creating their member record.
98 lines
3.3 KiB
JavaScript
98 lines
3.3 KiB
JavaScript
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(process.env.RESEND_API_KEY)
|
|
|
|
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 selected pre-registrants (skip already invited/accepted/expired)
|
|
if (preReg.status !== 'selected' && preReg.status !== 'pending') {
|
|
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 = `<a href="${acceptLink}" style="display:inline-block;padding:12px 24px;background-color:#d4a017;color:#1a1a1a;text-decoration:none;border-radius:6px;font-weight:bold;">Accept Your Invitation</a>`
|
|
const emailHtml = emailTemplate
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/\{name\}/g, preReg.name || 'there')
|
|
.replace(/(https?:\/\/[^\s<]+)/g, '<a href="$1">$1</a>')
|
|
.replace(/\n/g, '<br>')
|
|
.replace(/\{acceptLink\}/g, acceptButton)
|
|
|
|
const { error: emailError } = await resend.emails.send({
|
|
from: 'Ghost Guild <welcome@babyghosts.org>',
|
|
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 }
|
|
})
|