refactor(launch): collapse helcim-pay duplication and use setAuthCookie helper
Some checks failed
Test / vitest (push) Successful in 11m49s
Test / playwright (push) Failing after 9m43s
Test / visual (push) Failing after 9m24s
Test / Notify on failure (push) Successful in 2s

Follow-up to 51230e5. /simplify review surfaced residual duplication
and a timer leak.

- useHelcimPay: extract _initializeTicket(metadata, errorPrefix) to
  collapse initializeTicketPayment + initializeSeriesTicketPayment
  (95% identical bodies). Drop the dead `amount` arg from initialize-
  TicketPayment — server re-derives ticket amounts in initialize-
  payment.post.js and never reads body.amount for ticket types.
  Capture timer ids and clearTimeout on resolve/reject so the 10-min
  payment timer and 5-second observer timer stop leaking after every
  payment.
- EventTicketPurchase: caller updated for the dropped arg.
- verify.post.js: replace inline jwt.sign + setCookie block with the
  setAuthCookie(event, member) helper. verify was the last hand-rolled
  caller after the helper was extracted in 208638e.
- LAUNCH_READINESS: add simplify-pass-followups bullet pointing to the
  six deferred items in docs/TODO.md.

Tests: 758 passing, 2 skipped, 0 failing.
This commit is contained in:
Jennie Robinson Faber 2026-04-25 22:13:24 +01:00
parent 51230e5151
commit 8e76ce9366
4 changed files with 24 additions and 70 deletions

View file

@ -330,7 +330,6 @@ const handleSubmit = async () => {
await initializeTicketPayment( await initializeTicketPayment(
props.eventId, props.eventId,
form.value.email, form.value.email,
ticketInfo.value.price,
props.eventTitle, props.eventTitle,
); );

View file

@ -29,26 +29,14 @@ export const useHelcimPay = () => {
} }
}; };
// Initialize payment for event ticket purchase const _initializeTicket = async (metadata, errorPrefix) => {
const initializeTicketPayment = async (
eventId,
email,
amount,
eventTitle = null,
) => {
try { try {
const response = await $fetch("/api/helcim/initialize-payment", { const response = await $fetch("/api/helcim/initialize-payment", {
method: "POST", method: "POST",
body: { body: {
customerId: null, customerId: null,
customerCode: email, // Use email as customer code for event tickets customerCode: metadata.email,
amount, metadata,
metadata: {
type: "event_ticket",
eventId,
email,
eventTitle,
},
}, },
}); });
@ -62,50 +50,24 @@ export const useHelcimPay = () => {
}; };
} }
throw new Error("Failed to initialize ticket payment session"); throw new Error(`Failed to initialize ${errorPrefix} session`);
} catch (error) { } catch (error) {
console.error("Ticket payment initialization error:", error); console.error(`${errorPrefix} initialization error:`, error);
throw error; throw error;
} }
}; };
// Initialize payment for series pass purchase const initializeTicketPayment = (eventId, email, eventTitle = null) =>
const initializeSeriesTicketPayment = async ( _initializeTicket(
seriesId, { type: "event_ticket", eventId, email, eventTitle },
email, "ticket payment",
seriesTitle = null, );
) => {
try {
const response = await $fetch("/api/helcim/initialize-payment", {
method: "POST",
body: {
customerId: null,
customerCode: email,
metadata: {
type: "series_ticket",
seriesId,
email,
eventTitle: seriesTitle,
},
},
});
if (response.success) { const initializeSeriesTicketPayment = (seriesId, email, seriesTitle = null) =>
checkoutToken = response.checkoutToken; _initializeTicket(
secretToken = response.secretToken; { type: "series_ticket", seriesId, email, eventTitle: seriesTitle },
return { "series payment",
success: true, );
checkoutToken: response.checkoutToken,
amount: response.amount,
};
}
throw new Error("Failed to initialize series payment session");
} catch (error) {
console.error("Series payment initialization error:", error);
throw error;
}
};
// Show payment modal // Show payment modal
const showPaymentModal = () => { const showPaymentModal = () => {
@ -179,6 +141,7 @@ export const useHelcimPay = () => {
if (typeof window.appendHelcimPayIframe === "function") { if (typeof window.appendHelcimPayIframe === "function") {
// Set up event listener for HelcimPay.js responses // Set up event listener for HelcimPay.js responses
const helcimPayJsIdentifierKey = "helcim-pay-js-" + checkoutToken; const helcimPayJsIdentifierKey = "helcim-pay-js-" + checkoutToken;
let observerTimer, paymentTimer;
const handleHelcimPayEvent = (event) => { const handleHelcimPayEvent = (event) => {
console.log("Received window message:", event.data); console.log("Received window message:", event.data);
@ -188,6 +151,8 @@ export const useHelcimPay = () => {
// Remove event listener to prevent multiple responses // Remove event listener to prevent multiple responses
window.removeEventListener("message", handleHelcimPayEvent); window.removeEventListener("message", handleHelcimPayEvent);
clearTimeout(observerTimer);
clearTimeout(paymentTimer);
// Close the Helcim modal // Close the Helcim modal
if (typeof window.removeHelcimPayIframe === "function") { if (typeof window.removeHelcimPayIframe === "function") {
@ -277,10 +242,10 @@ export const useHelcimPay = () => {
); );
// Clean up observer after a timeout // Clean up observer after a timeout
setTimeout(() => observer.disconnect(), 5000); observerTimer = setTimeout(() => observer.disconnect(), 5000);
// Add timeout to clean up if no response (10 minutes for manual card entry) // Add timeout to clean up if no response (10 minutes for manual card entry)
setTimeout(() => { paymentTimer = setTimeout(() => {
console.log("Payment timeout reached, cleaning up event listener..."); console.log("Payment timeout reached, cleaning up event listener...");
window.removeEventListener("message", handleHelcimPayEvent); window.removeEventListener("message", handleHelcimPayEvent);
reject(new Error("Payment timeout - no response received")); reject(new Error("Payment timeout - no response received"));

View file

@ -122,6 +122,7 @@ See `docs/TODO.md` for:
- Members table NAME column clipping. - Members table NAME column clipping.
- OWASP ASVS L1 Phase 4 (file-upload validation pipeline, granular RBAC, credential encryption). - OWASP ASVS L1 Phase 4 (file-upload validation pipeline, granular RBAC, credential encryption).
- `tickets/available.get.js:115` `memberSavings` block reports `$0 saved` for inactive members — cosmetic; suppress comparison block when `!hasMemberAccess(member)` if it ever surfaces in UI. - `tickets/available.get.js:115` `memberSavings` block reports `$0 saved` for inactive members — cosmetic; suppress comparison block when `!hasMemberAccess(member)` if it ever surfaces in UI.
- Simplify-pass follow-ups (2026-04-25): source-grep test bloat, login/verify rate-limit gap, stringly-typed `metadata.type`, reconcile-payments sequential loop, stale `new Date()` in events list, `loadPublicSeries` helper extraction.
### Known gotchas worth addressing post-launch ### Known gotchas worth addressing post-launch
@ -148,3 +149,4 @@ Context: Phase 4 audit against `docs/specs/events-visual-audit-findings.md` fixe
- Update stale tier comment in `app/composables/useMemberPayment.js:59`. - Update stale tier comment in `app/composables/useMemberPayment.js:59`.
- Update error log message referencing "tier" in `server/api/members/update-contribution.post.js:221`. - Update error log message referencing "tier" in `server/api/members/update-contribution.post.js:221`.
- Rename `handleUpdateTier` handler in `app/pages/member/account.vue`. - Rename `handleUpdateTier` handler in `app/pages/member/account.vue`.

View file

@ -3,6 +3,7 @@ import jwt from 'jsonwebtoken'
import Member from '../../models/member.js' import Member from '../../models/member.js'
import { validateBody } from '../../utils/validateBody.js' import { validateBody } from '../../utils/validateBody.js'
import { verifyMagicLinkSchema } from '../../utils/schemas.js' import { verifyMagicLinkSchema } from '../../utils/schemas.js'
import { setAuthCookie } from '../../utils/auth.js'
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const { token } = await validateBody(event, verifyMagicLinkSchema) const { token } = await validateBody(event, verifyMagicLinkSchema)
@ -57,20 +58,7 @@ export default defineEventHandler(async (event) => {
{ runValidators: false } { runValidators: false }
) )
// Issue session token with tokenVersion claim for revocation support setAuthCookie(event, member)
const sessionToken = jwt.sign(
{ memberId: member._id, email: member.email, tv: member.tokenVersion },
config.jwtSecret,
{ expiresIn: '7d' },
)
setCookie(event, 'auth-token', sessionToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
maxAge: 60 * 60 * 24 * 7, // 7 days
})
const redirectUrl = member.role === 'admin' ? '/admin' : '/member/dashboard' const redirectUrl = member.role === 'admin' ? '/admin' : '/member/dashboard'
return { success: true, redirectUrl } return { success: true, redirectUrl }