ghostguild-org/server/api/admin/pre-registrants/invite.post.js
Jennie Robinson Faber 1c8f30fe6f feat(invite): skip Resend dispatch when ALLOW_DEV_TEST_ENDPOINTS=true
Pre-registrant invite was the only email route calling Resend directly
(bypassing server/utils/resend.js), so dev/e2e runs were dispatching
real email. Gate just the network call; DB updates (jti, status,
activity log) still run. Mirrors the bypass pattern in
server/middleware/03.rate-limit.js.

Other email routes via server/utils/resend.js still send live in dev
mode — wrapper refactor tracked in BACKLOG.
2026-04-30 22:25:41 +01:00

104 lines
3.6 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(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 = `<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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\{name\}/g, preReg.name || 'there')
.replace(/(https?:\/\/[^\s<]+)/g, '<a href="$1">$1</a>')
.replace(/\n/g, '<br>')
.replace(/\{acceptLink\}/g, acceptButton)
const subject = "You're invited to Ghost Guild! 👻"
if (process.env.ALLOW_DEV_TEST_ENDPOINTS === 'true') {
console.log('[resend] DEV MODE — skipping invite send', { to: preReg.email, subject })
} else {
const { error: emailError } = await resend.emails.send({
from: 'Ghost Guild <welcome@babyghosts.org>',
to: [preReg.email],
subject,
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 }
})