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(
props.eventId,
form.value.email,
ticketInfo.value.price,
props.eventTitle,
);

View file

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

View file

@ -122,6 +122,7 @@ See `docs/TODO.md` for:
- Members table NAME column clipping.
- 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.
- 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
@ -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 error log message referencing "tier" in `server/api/members/update-contribution.post.js:221`.
- 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 { validateBody } from '../../utils/validateBody.js'
import { verifyMagicLinkSchema } from '../../utils/schemas.js'
import { setAuthCookie } from '../../utils/auth.js'
export default defineEventHandler(async (event) => {
const { token } = await validateBody(event, verifyMagicLinkSchema)
@ -57,20 +58,7 @@ export default defineEventHandler(async (event) => {
{ runValidators: false }
)
// Issue session token with tokenVersion claim for revocation support
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
})
setAuthCookie(event, member)
const redirectUrl = member.role === 'admin' ? '/admin' : '/member/dashboard'
return { success: true, redirectUrl }