fix: use private helcimApiToken for all server-side Helcim API calls

This commit is contained in:
Jennie Robinson Faber 2026-04-04 13:37:34 +01:00
parent ccd1d0783a
commit d31b5b4dac
53 changed files with 1755 additions and 572 deletions

View file

@ -0,0 +1,42 @@
import Member from '../../../models/member.js'
import { connectDB } from '../../../utils/mongoose.js'
export default defineEventHandler(async (event) => {
await requireAdmin(event)
const body = await validateBody(event, adminMemberUpdateSchema)
const memberId = getRouterParam(event, 'id')
await connectDB()
// If email changed, check for duplicates
const existing = await Member.findById(memberId)
if (!existing) {
throw createError({ statusCode: 404, statusMessage: 'Member not found' })
}
if (body.email !== existing.email) {
const emailTaken = await Member.findOne({ email: body.email })
if (emailTaken) {
throw createError({ statusCode: 409, statusMessage: 'Email already in use by another member' })
}
}
const updated = await Member.findByIdAndUpdate(memberId, {
name: body.name,
email: body.email,
circle: body.circle,
contributionTier: body.contributionTier,
status: body.status,
}, { new: true })
return {
_id: updated._id,
name: updated.name,
email: updated.email,
circle: updated.circle,
contributionTier: updated.contributionTier,
status: updated.status,
role: updated.role,
}
})

View file

@ -1,4 +1,5 @@
import jwt from 'jsonwebtoken'
import { randomUUID } from 'crypto'
import { Resend } from 'resend'
import Member from '../../../models/member.js'
import { connectDB } from '../../../utils/mongoose.js'
@ -10,12 +11,12 @@ export default defineEventHandler(async (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 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) {
@ -28,15 +29,32 @@ export default defineEventHandler(async (event) => {
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 48-hour magic login token (same format as login.post.js)
// Generate single-use invite token (48h), same jti pattern as login.post.js
const jti = randomUUID()
const token = jwt.sign(
{ memberId: member._id },
{ memberId: member._id, jti },
config.jwtSecret,
{ expiresIn: '48h' }
{ expiresIn: '48h' },
)
const loginLink = `${baseUrl}/api/auth/verify?token=${token}`
// Store jti for single-use enforcement in verify.post.js
member.magicLinkJti = jti
member.magicLinkJtiUsed = false
// Token in fragment — never hits server logs
const loginLink = `${baseUrl}/verify#${token}`
// Interpolate template variables
const emailText = emailTemplate
@ -59,9 +77,9 @@ export default defineEventHandler(async (event) => {
const { error: emailError } = await resend.emails.send({
from: 'Ghost Guild <welcome@babyghosts.org>',
to: [member.email],
subject: 'You\'re invited to Ghost Guild',
subject: "You're invited to Ghost Guild",
text: emailText,
html: emailHtml
html: emailHtml,
})
if (emailError) {