diff --git a/server/api/helcim/customer.post.js b/server/api/helcim/customer.post.js index 2d09ff3..28ceda3 100644 --- a/server/api/helcim/customer.post.js +++ b/server/api/helcim/customer.post.js @@ -4,7 +4,7 @@ import { connectDB } from '../../utils/mongoose.js' import { createHelcimCustomer } from '../../utils/helcim.js' import PreRegistration from '../../models/preRegistration.js' import { sendMagicLink } from '../../utils/magicLink.js' -import { setPaymentBridgeCookie } from '../../utils/auth.js' +import { setSignupBridgeCookie } from '../../utils/auth.js' import { rateLimit } from '../../utils/rateLimit.js' export default defineEventHandler(async (event) => { @@ -116,10 +116,10 @@ export default defineEventHandler(async (event) => { }) // Signup completes (paid checkout or free activation) before the magic - // link is clicked, so issue a short-lived, payment-only bridge cookie - // that lets /api/helcim/initialize-payment and /api/helcim/subscription - // identify the member without a verified auth session. - setPaymentBridgeCookie(event, member) + // link is clicked, so issue a short-lived signup-bridge cookie that lets + // /api/helcim/initialize-payment and /api/helcim/subscription identify + // the member without a verified auth session. + setSignupBridgeCookie(event, member) return { success: true, diff --git a/server/api/helcim/initialize-payment.post.js b/server/api/helcim/initialize-payment.post.js index a01b8d0..3826b08 100644 --- a/server/api/helcim/initialize-payment.post.js +++ b/server/api/helcim/initialize-payment.post.js @@ -2,7 +2,7 @@ import Member from '../../models/member.js' import { loadPublicEvent } from '../../utils/loadEvent.js' import { loadPublicSeries } from '../../utils/loadSeries.js' import { calculateTicketPrice, calculateSeriesTicketPrice, hasMemberAccess } from '../../utils/tickets.js' -import { requireAuth, getOptionalMember, getPaymentBridgeMember } from '../../utils/auth.js' +import { requireAuth, getOptionalMember, getSignupBridgeMember } from '../../utils/auth.js' import { initializeHelcimPaySession } from '../../utils/helcim.js' export default defineEventHandler(async (event) => { @@ -17,7 +17,7 @@ export default defineEventHandler(async (event) => { if (!isTicket) { if (isMembershipSignup) { - const bridgeMember = await getPaymentBridgeMember(event) + const bridgeMember = await getSignupBridgeMember(event) if (!bridgeMember) { await requireAuth(event) } diff --git a/server/api/helcim/subscription.post.js b/server/api/helcim/subscription.post.js index 577e264..d02846f 100644 --- a/server/api/helcim/subscription.post.js +++ b/server/api/helcim/subscription.post.js @@ -3,7 +3,7 @@ import { getHelcimPlanId, requiresPayment } from '../../config/contributions.js' import Member from '../../models/member.js' import { connectDB } from '../../utils/mongoose.js' import { getSlackService } from '../../utils/slack.ts' -import { requireAuth, getPaymentBridgeMember } from '../../utils/auth.js' +import { requireAuth, getSignupBridgeMember } from '../../utils/auth.js' import { createHelcimSubscription, generateIdempotencyKey, listHelcimCustomerTransactions } from '../../utils/helcim.js' import { sendWelcomeEmail } from '../../utils/resend.js' import { upsertPaymentFromHelcim } from '../../utils/payments.js' @@ -11,8 +11,8 @@ import { upsertPaymentFromHelcim } from '../../utils/payments.js' export default defineEventHandler(async (event) => { try { // Membership signup completes subscription before email verify; allow the - // payment-bridge cookie set by /api/helcim/customer to satisfy auth here. - const bridgeMember = await getPaymentBridgeMember(event) + // signup-bridge cookie set by /api/helcim/customer to satisfy auth here. + const bridgeMember = await getSignupBridgeMember(event) if (!bridgeMember) { await requireAuth(event) } diff --git a/server/utils/auth.js b/server/utils/auth.js index 1876636..6603d6d 100644 --- a/server/utils/auth.js +++ b/server/utils/auth.js @@ -23,26 +23,27 @@ export function setAuthCookie(event, member) { } /** - * Issue a 30-minute payment-bridge cookie scoped to membership-signup checkout. + * Issue a 30-minute signup-bridge cookie scoped to membership-signup flow. * * The signup flow (POST /api/helcim/customer) defers the full session cookie - * to email-verify (magic link). For paid tiers the user still needs to complete - * Helcim checkout in the same browser tab — this short-lived, payment-only - * token lets `/api/helcim/initialize-payment` accept the call without a full - * session. The cookie is NOT honored by requireAuth and grants nothing else. + * to email-verify (magic link). The bridge cookie lets the in-progress signup + * complete its activation step (free or paid) before that magic link is + * clicked: /api/helcim/subscription accepts it for $0 activation, and + * /api/helcim/initialize-payment accepts it for paid Helcim checkout. + * The cookie is NOT honored by requireAuth and grants nothing else. */ -export function setPaymentBridgeCookie(event, member) { +export function setSignupBridgeCookie(event, member) { const token = jwt.sign( { memberId: member._id.toString(), email: member.email, - scope: 'payment_bridge' + scope: 'signup_bridge' }, useRuntimeConfig(event).jwtSecret, { expiresIn: '30m' } ) - setCookie(event, 'payment-bridge', token, { + setCookie(event, 'signup-bridge', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', @@ -52,12 +53,12 @@ export function setPaymentBridgeCookie(event, member) { } /** - * Verify a payment-bridge cookie and return the associated Member, or null. - * Used by /api/helcim/initialize-payment to allow the membership-signup - * checkout to proceed before email verification. + * Verify a signup-bridge cookie and return the associated Member, or null. + * Used by /api/helcim/subscription and /api/helcim/initialize-payment to + * let the in-progress signup complete activation before email verification. */ -export async function getPaymentBridgeMember(event) { - const token = getCookie(event, 'payment-bridge') +export async function getSignupBridgeMember(event) { + const token = getCookie(event, 'signup-bridge') if (!token) return null let decoded @@ -67,7 +68,7 @@ export async function getPaymentBridgeMember(event) { return null } - if (decoded.scope !== 'payment_bridge') return null + if (decoded.scope !== 'signup_bridge') return null await connectDB() const member = await Member.findById(decoded.memberId) diff --git a/tests/server/api/activation-auto-flag.test.js b/tests/server/api/activation-auto-flag.test.js index bcf7493..2895506 100644 --- a/tests/server/api/activation-auto-flag.test.js +++ b/tests/server/api/activation-auto-flag.test.js @@ -45,7 +45,7 @@ vi.mock('../../../server/models/preRegistration.js', () => ({ vi.mock('../../../server/utils/mongoose.js', () => ({ connectDB: vi.fn() })) vi.mock('../../../server/utils/auth.js', () => ({ requireAuth: vi.fn(), - getPaymentBridgeMember: vi.fn().mockResolvedValue(null), + getSignupBridgeMember: vi.fn().mockResolvedValue(null), setAuthCookie: vi.fn() })) vi.mock('../../../server/utils/slack.ts', () => ({ diff --git a/tests/server/api/free-signup-flow.test.js b/tests/server/api/free-signup-flow.test.js index bee73b3..b95b752 100644 --- a/tests/server/api/free-signup-flow.test.js +++ b/tests/server/api/free-signup-flow.test.js @@ -60,9 +60,9 @@ const SUBSCRIPTION_BODY = { function extractBridgeCookie(event) { const setCookie = event.node.res.getHeader('set-cookie') const cookies = Array.isArray(setCookie) ? setCookie : [setCookie].filter(Boolean) - const match = cookies.find(c => typeof c === 'string' && c.startsWith('payment-bridge=')) + const match = cookies.find(c => typeof c === 'string' && c.startsWith('signup-bridge=')) if (!match) return null - return match.match(/payment-bridge=([^;]+)/)[1] + return match.match(/signup-bridge=([^;]+)/)[1] } describe('signup → subscription bridge-cookie hand-off', () => { @@ -104,7 +104,7 @@ describe('signup → subscription bridge-cookie hand-off', () => { expect(result1.member.status).toBe('pending_payment') const bridgeToken = extractBridgeCookie(customerEvent) - expect(bridgeToken, 'payment-bridge cookie missing on $0 signup').toBeTruthy() + expect(bridgeToken, 'signup-bridge cookie missing on $0 signup').toBeTruthy() Member.findOneAndUpdate.mockResolvedValue({ _id: MEMBER_ID, status: 'pending_payment' }) Member.findById.mockResolvedValue({ @@ -120,7 +120,7 @@ describe('signup → subscription bridge-cookie hand-off', () => { method: 'POST', path: '/api/helcim/subscription', headers: { origin: ALLOWED_ORIGIN }, - cookies: { 'payment-bridge': bridgeToken }, + cookies: { 'signup-bridge': bridgeToken }, body: SUBSCRIPTION_BODY }) diff --git a/tests/server/api/helcim-customer.test.js b/tests/server/api/helcim-customer.test.js index a023c27..2aa6ae3 100644 --- a/tests/server/api/helcim-customer.test.js +++ b/tests/server/api/helcim-customer.test.js @@ -3,7 +3,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' import Member from '../../../server/models/member.js' import { createHelcimCustomer } from '../../../server/utils/helcim.js' import { sendMagicLink } from '../../../server/utils/magicLink.js' -import { setAuthCookie, setPaymentBridgeCookie } from '../../../server/utils/auth.js' +import { setAuthCookie, setSignupBridgeCookie } from '../../../server/utils/auth.js' import customerHandler from '../../../server/api/helcim/customer.post.js' import { resetRateLimit } from '../../../server/utils/rateLimit.js' import { createMockEvent } from '../helpers/createMockEvent.js' @@ -24,7 +24,7 @@ vi.mock('../../../server/utils/magicLink.js', () => ({ })) vi.mock('../../../server/utils/auth.js', () => ({ setAuthCookie: vi.fn(), - setPaymentBridgeCookie: vi.fn() + setSignupBridgeCookie: vi.fn() })) // helcimCustomerSchema is auto-imported in the handler — stub it to a passthrough @@ -303,7 +303,7 @@ describe('POST /api/helcim/customer', () => { 'guest@example.com', expect.objectContaining({ subject: 'Verify your Ghost Guild signup' }) ) - expect(setPaymentBridgeCookie).toHaveBeenCalled() + expect(setSignupBridgeCookie).toHaveBeenCalled() expect(setAuthCookie).not.toHaveBeenCalled() // Response shape mirrors new-signup case AND surfaces the preserved _id. @@ -365,7 +365,7 @@ describe('POST /api/helcim/customer', () => { ) }) - it('sets a payment-bridge cookie on paid-tier signup so checkout can proceed', async () => { + it('sets a signup-bridge cookie on paid-tier signup so checkout can proceed', async () => { const event = build({ body: { name: 'Paid User', @@ -376,7 +376,7 @@ describe('POST /api/helcim/customer', () => { } }) await customerHandler(event) - expect(setPaymentBridgeCookie).toHaveBeenCalled() + expect(setSignupBridgeCookie).toHaveBeenCalled() expect(sendMagicLink).toHaveBeenCalledWith( 'paid@example.com', expect.objectContaining({ subject: 'Verify your Ghost Guild signup' }) diff --git a/tests/server/api/helcim-subscription.test.js b/tests/server/api/helcim-subscription.test.js index 8e6bc77..6845cd4 100644 --- a/tests/server/api/helcim-subscription.test.js +++ b/tests/server/api/helcim-subscription.test.js @@ -15,7 +15,7 @@ vi.mock('../../../server/models/member.js', () => ({ vi.mock('../../../server/utils/mongoose.js', () => ({ connectDB: vi.fn() })) vi.mock('../../../server/utils/auth.js', () => ({ requireAuth: vi.fn(), - getPaymentBridgeMember: vi.fn().mockResolvedValue(null) + getSignupBridgeMember: vi.fn().mockResolvedValue(null) })) vi.mock('../../../server/utils/slack.ts', () => ({ getSlackService: vi.fn().mockReturnValue(null)