import jwt from 'jsonwebtoken'
import { randomUUID } from 'crypto'
import { Resend } from 'resend'
import Member from '../../../models/member.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 { memberIds, emailTemplate } = await validateBody(event, memberInviteSchema)
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 members = await Member.find({ _id: { $in: memberIds } })
if (members.length === 0) {
throw createError({
statusCode: 404,
statusMessage: 'No members found for the provided IDs'
})
}
const results = []
for (const member of members) {
// Skip suspended/cancelled — do not reactivate silently
if (member.status === 'suspended' || member.status === 'cancelled') {
results.push({
memberId: member._id,
email: member.email,
success: false,
error: `Skipped: account is ${member.status}`,
})
continue
}
try {
// Generate single-use invite token (48h), same jti pattern as login.post.js
const jti = randomUUID()
const token = jwt.sign(
{ memberId: member._id, jti },
config.jwtSecret,
{ expiresIn: '48h' },
)
// Store jti for single-use enforcement in verify.post.js (set after email succeeds below)
// Token in fragment — never hits server logs
const loginLink = `${baseUrl}/verify#${token}`
// Interpolate template variables
const emailText = emailTemplate
.replace(/\{name\}/g, member.name)
.replace(/\{loginLink\}/g, loginLink)
.replace(/\{circle\}/g, member.circle)
// Build HTML version: escape user content, linkify plain URLs, then insert button (unescaped) last
const loginButton = `Sign in to Ghost Guild`
const emailHtml = emailTemplate
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/\{name\}/g, member.name)
.replace(/\{circle\}/g, member.circle)
.replace(/(https?:\/\/[^\s<]+)/g, '$1')
.replace(/\n/g, '
')
.replace(/\{loginLink\}/g, loginButton)
const { error: emailError } = await resend.emails.send({
from: 'Ghost Guild ',
to: [member.email],
subject: "You're invited to Ghost Guild! 👻",
text: emailText,
html: emailHtml,
})
if (emailError) {
results.push({ memberId: member._id, email: member.email, success: false, error: emailError.message })
continue
}
// Mark member as active, record invite sent, store jti for single-use enforcement
await Member.findByIdAndUpdate(
member._id,
{
$set: {
magicLinkJti: jti,
magicLinkJtiUsed: false,
status: 'active',
inviteEmailSent: true,
inviteEmailSentAt: new Date(),
},
},
{ runValidators: false }
)
logActivity(member._id, 'email_sent', {
emailType: 'invite',
subject: "You're invited to Ghost Guild! 👻",
body: emailText
})
results.push({ memberId: member._id, email: member.email, success: true })
} catch (err) {
results.push({ memberId: member._id, email: member.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 }
})