Send invite emails as HTML with clickable button, redirect login to wiki
Invite emails now include both plain text and HTML versions. The
{loginLink} placeholder renders as a styled button in HTML email
clients. Other URLs in the template are auto-linked. The auth verify
endpoint redirects to wiki.ghostguild.org instead of /members.
This commit is contained in:
parent
ea6c4d8329
commit
27c07cd3e9
2 changed files with 90 additions and 2 deletions
88
server/api/admin/members/invite.post.js
Normal file
88
server/api/admin/members/invite.post.js
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import jwt from 'jsonwebtoken'
|
||||
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 config = useRuntimeConfig(event)
|
||||
const headers = getHeaders(event)
|
||||
const baseUrl =
|
||||
process.env.BASE_URL ||
|
||||
`${headers.host?.includes('localhost') ? 'http' : 'https'}://${headers.host}`
|
||||
|
||||
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) {
|
||||
try {
|
||||
// Generate 48-hour magic login token (same format as login.post.js)
|
||||
const token = jwt.sign(
|
||||
{ memberId: member._id },
|
||||
config.jwtSecret,
|
||||
{ expiresIn: '48h' }
|
||||
)
|
||||
|
||||
const loginLink = `${baseUrl}/api/auth/verify?token=${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 = `<a href="${loginLink}" style="display:inline-block;padding:12px 24px;background-color:#d4a017;color:#1a1a1a;text-decoration:none;border-radius:6px;font-weight:bold;">Sign in to Ghost Guild</a>`
|
||||
const emailHtml = emailTemplate
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\{name\}/g, member.name)
|
||||
.replace(/\{circle\}/g, member.circle)
|
||||
.replace(/(https?:\/\/[^\s<]+)/g, '<a href="$1">$1</a>')
|
||||
.replace(/\n/g, '<br>')
|
||||
.replace(/\{loginLink\}/g, loginButton)
|
||||
|
||||
const { error: sendError } = await resend.emails.send({
|
||||
from: 'Ghost Guild <welcome@babyghosts.org>',
|
||||
to: [member.email],
|
||||
subject: 'You\'re invited to Ghost Guild',
|
||||
text: emailText,
|
||||
html: emailHtml
|
||||
})
|
||||
|
||||
if (sendError) {
|
||||
results.push({ memberId: member._id, email: member.email, success: false, error: sendError.message })
|
||||
continue
|
||||
}
|
||||
|
||||
// Mark member as active and record invite sent
|
||||
member.status = 'active'
|
||||
member.inviteEmailSent = true
|
||||
member.inviteEmailSentAt = new Date()
|
||||
await member.save()
|
||||
|
||||
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 }
|
||||
})
|
||||
|
|
@ -52,8 +52,8 @@ export default defineEventHandler(async (event) => {
|
|||
maxAge: 60 * 60 * 24 * 7 // 7 days
|
||||
})
|
||||
|
||||
// Redirect to the members dashboard or home page
|
||||
await sendRedirect(event, '/members', 302)
|
||||
// Redirect to the wiki
|
||||
await sendRedirect(event, 'https://wiki.ghostguild.org', 302)
|
||||
|
||||
} catch (err) {
|
||||
if (err.statusCode && err.statusCode !== 401) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue