// Send a magic-link verification email. Mirrors the token/email logic in // server/api/auth/login.post.js so callers (signup, login, etc.) can request // a verification link with their own subject/intro copy. import jwt from 'jsonwebtoken' import { randomUUID } from 'crypto' import { Resend } from 'resend' import Member from '../models/member.js' const resend = new Resend(process.env.RESEND_API_KEY) /** * Issue a 15-minute magic-link JWT for `email` and email it. * * @param {string} email * @param {object} [options] * @param {string} [options.subject] - Email subject (default: "Your Ghost Guild login link") * @param {string} [options.intro] - Optional one-line intro before the link. * @param {object} [options.member] - Pre-loaded Member doc; skips the findOne lookup. * @returns {Promise<{ sent: boolean }>} - sent=false when no member exists for the email * (caller can decide whether to surface that; the auth/login endpoint hides it for * anti-enumeration, signup knows the member was just created). */ export async function sendMagicLink(email, options = {}) { const baseUrl = process.env.BASE_URL if (!baseUrl) { throw createError({ statusCode: 500, statusMessage: 'BASE_URL environment variable is not set' }) } email = email.toLowerCase() const member = options.member || await Member.findOne({ email }) if (!member) return { sent: false } const jti = randomUUID() const token = jwt.sign( { memberId: member._id, jti }, useRuntimeConfig().jwtSecret, { expiresIn: '15m' } ) await Member.findByIdAndUpdate( member._id, { $set: { magicLinkJti: jti, magicLinkJtiUsed: false } }, { runValidators: false } ) const magicLink = `${baseUrl}/verify#${token}` const subject = options.subject || 'Your Ghost Guild login link' const intro = options.intro || 'Sign in to Ghost Guild:' const text = `Hi,\n\n${intro}\n${magicLink}\n\nThis link expires in 15 minutes. If you didn't request it, ignore this email.` await resend.emails.send({ from: 'Ghost Guild ', to: email, subject, text }) logActivity(member._id, 'email_sent', { emailType: 'magic_link', subject, body: text }) return { sent: true } }