refactor(launch): collapse helcim-pay duplication and use setAuthCookie helper
Follow-up to51230e5. /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 in208638e. - 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:
parent
51230e5151
commit
8e76ce9366
4 changed files with 24 additions and 70 deletions
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"));
|
||||||
|
|
|
||||||
|
|
@ -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`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue