ghostguild-org/server/utils/magicLink.js
Jennie Robinson Faber 04eb33df6e
Some checks failed
Test / vitest (push) Successful in 11m10s
Test / playwright (push) Failing after 14m51s
Test / visual (push) Failing after 11m1s
Test / Notify on failure (push) Successful in 3s
refactor(env): unify required-env validation through useRuntimeConfig
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.
2026-04-26 14:47:02 +01:00

68 lines
2.2 KiB
JavaScript

// 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(useRuntimeConfig().resendApiKey)
/**
* 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 <ghostguild@babyghosts.org>',
to: email,
subject,
text
})
logActivity(member._id, 'email_sent', {
emailType: 'magic_link',
subject,
body: text
})
return { sent: true }
}