validate-env.js now reads all four required vars (MONGODB_URI, JWT_SECRET, RESEND_API_KEY, HELCIM_API_TOKEN) from useRuntimeConfig() instead of mixing direct process.env reads with a JWT-only special case. Mongoose and the six Resend instantiations follow suit. Either bare or NUXT_-prefixed env names are accepted, so Dokploy no longer needs duplicate entries.
98 lines
3.4 KiB
JavaScript
98 lines
3.4 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, '&')
|
|
.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 }
|
|
})
|