From e17b7bb6b340d79fb71ab57fa2ea742e7420115b Mon Sep 17 00:00:00 2001 From: Cameron Gagnon Date: Mon, 11 May 2026 00:19:02 -0400 Subject: [PATCH 1/5] feat: add Netlify Function for Stripe Checkout session creation Adds the serverless backend needed to replace the MIT SAO payment system. The function validates the amount, creates a Stripe Checkout Session, and returns the hosted URL. All PCI scope stays on Stripe's side. Stripe Link, Apple Pay, and Google Pay work automatically via Stripe Checkout with no extra config. --- .gitignore | 2 + netlify.toml | 3 + netlify/functions/create-checkout-session.js | 50 +++ package-lock.json | 304 +++++++++++++++++++ package.json | 7 + 5 files changed, 366 insertions(+) create mode 100644 netlify/functions/create-checkout-session.js create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore index 3372c3b8..a80850a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ *.swp _site .DS_Store +.env +node_modules diff --git a/netlify.toml b/netlify.toml index 6621d15f..5388575e 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,3 +1,6 @@ [build] publish = "_site" command = "jekyll build" + +[functions] + directory = "netlify/functions" diff --git a/netlify/functions/create-checkout-session.js b/netlify/functions/create-checkout-session.js new file mode 100644 index 00000000..764eb735 --- /dev/null +++ b/netlify/functions/create-checkout-session.js @@ -0,0 +1,50 @@ +const Stripe = require('stripe'); + +exports.handler = async function (event) { + if (event.httpMethod !== 'POST') { + return { statusCode: 405, body: 'Method Not Allowed' }; + } + + let body; + try { + body = JSON.parse(event.body); + } catch { + return { statusCode: 400, body: 'Invalid request body' }; + } + + const { amount_cents, description, metadata } = body; + + if (!Number.isInteger(amount_cents) || amount_cents < 50) { + return { statusCode: 400, body: 'amount_cents must be an integer >= 50' }; + } + + const stripe = Stripe(process.env.STRIPE_SECRET_KEY); + const origin = event.headers.origin || 'https://mitoc.mit.edu'; + + try { + const session = await stripe.checkout.sessions.create({ + mode: 'payment', + line_items: [ + { + price_data: { + currency: 'usd', + unit_amount: amount_cents, + product_data: { name: description }, + }, + quantity: 1, + }, + ], + metadata: metadata || {}, + success_url: `${origin}/pay/success`, + cancel_url: `${origin}/pay`, + }); + + return { + statusCode: 200, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url: session.url }), + }; + } catch (err) { + return { statusCode: 500, body: err.message }; + } +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..ca5b3552 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,304 @@ +{ + "name": "mitoc-website", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mitoc-website", + "dependencies": { + "stripe": "^16.0.0" + } + }, + "node_modules/@types/node": { + "version": "25.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz", + "integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stripe": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-16.12.0.tgz", + "integrity": "sha512-H7eFVLDxeTNNSn4JTRfL2//LzCbDrMSZ+2q1c7CanVWgK2qIW5TwS+0V7N9KcKZZNpYh/uCqK0PyZh/2UsaAtQ==", + "license": "MIT", + "dependencies": { + "@types/node": ">=8.1.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=12.*" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..4247bbc6 --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "name": "mitoc-website", + "private": true, + "dependencies": { + "stripe": "^16.0.0" + } +} From 1ef1a62d5a6cd7c2f3d033cab7207bec7fb70f3f Mon Sep 17 00:00:00 2001 From: Cameron Gagnon Date: Mon, 11 May 2026 00:20:02 -0400 Subject: [PATCH 2/5] feat: replace MIT SAO payment forms with Stripe Checkout Removes the PayPal/MIT SAO payment system and replaces it with Stripe Checkout. Forms now call the Netlify Function to get a hosted checkout URL, then redirect the browser there. Stripe handles card entry, PCI compliance, Link, and receipt emails. Field names are cleaned up from merchantDefinedData* to readable names. The PayPal warning banner is removed. --- _pages/pay.md | 146 +++++++++++------------------------------- _pages/pay/success.md | 8 +++ js/mitoc.js | 116 ++++++++++++++++++++++++--------- 3 files changed, 130 insertions(+), 140 deletions(-) create mode 100644 _pages/pay/success.md diff --git a/_pages/pay.md b/_pages/pay.md index 027c1d2a..4bc674b9 100644 --- a/_pages/pay.md +++ b/_pages/pay.md @@ -3,14 +3,9 @@ title: Pay MITOC --- -
-

Please be advised that we have been having issues processing payments from PayPal.

-

We recommend that you use a credit or debit card for online payment (we always accept personal checks in the office).

-
- -This page can be used to pay money to MITOC using PayPal or a credit/debit card. Payments can also be made by check at the [MITOC office](/where-is-mitoc) during [office hours](/calendar). **All payments are final and non-refundable.** +This page can be used to pay MITOC by credit or debit card. Payments can also be made by check at the [MITOC office](/where-is-mitoc) during [office hours](/calendar). **All payments are final and non-refundable.** -If you are having trouble with the payment system, contact [mitoc-bursar@mit.edu](mailto:mitoc-bursar@mit.edu). The most common issue is that the billing address must exactly match what your bank has on file, so if your card is declined, double-check the address you entered. +If you are having trouble with the payment system, contact [mitoc-bursar@mit.edu](mailto:mitoc-bursar@mit.edu). The most common issue is that the billing address must exactly match what your bank has on file. ### Financial Assistance We can provide need-blind financial assistance to any MIT undergrad, upon request. @@ -25,20 +20,10 @@ See the signup page [here](/join). ### Gear Rentals and Purchases
-
- - +
- - @@ -48,17 +33,12 @@ See the signup page [here](/join).
- - + +
- - + +
@@ -67,37 +47,21 @@ See the signup page [here](/join). ### Trip and Event Fees
-
- - +
- - + {% for fee in site.data.trip_fees %} - + {% endfor %}
- +
- - + +
- - + +
-
@@ -136,68 +88,42 @@ Be sure to fill out the visitor log ([Camelot](https://docs.google.com/spreadshe Days spent on-site but not in the cabin (e.g. Yurt / Lean-to / camping) cost 0.5 person-days (i.e. half price of using the cabin.) Enter the total number of fractional days spent outside and full days spent inside in the form below.
-
- +
- -
- +
- - + +
- - + +
diff --git a/_pages/pay/success.md b/_pages/pay/success.md new file mode 100644 index 00000000..18d63206 --- /dev/null +++ b/_pages/pay/success.md @@ -0,0 +1,8 @@ +--- +permalink: /pay/success +title: Payment Received +--- + +Your payment was successful. A receipt has been sent to your email. + +Questions? Contact [mitoc-bursar@mit.edu](mailto:mitoc-bursar@mit.edu). diff --git a/js/mitoc.js b/js/mitoc.js index 21c9712b..765b4254 100644 --- a/js/mitoc.js +++ b/js/mitoc.js @@ -1,30 +1,86 @@ ---- ---- - -//Load trip fee schedule from Google Sheets on the payment page -function load_trip_fees() { - //Populate dropdown menu and list of prices - var form = document.forms['trip_form']; - var options = form.merchantDefinedData2.options; - var prices = { - {% for fee in site.data.trip_fees %} - "{{fee.name}}": { "price": "{{ fee.price }}", "category": "{{ fee.category }}" }, - {% endfor %} - }; - //Add handler to calculate price when quantity changes - var calculate_amount = function() { - if(form.merchantDefinedData2.value) { - form.amount.value = form.merchantDefinedData4.value * prices[form.merchantDefinedData2.value].price; - } - }; - $('#merchantDefinedData4trip').keyup(calculate_amount); - $('#merchantDefinedData4trip').change(calculate_amount); - - //Add handler to calculate price and populate category when trip selection changes - $('#merchantDefinedData2trip').change(function() { - if(form.merchantDefinedData2.value) { - calculate_amount(); - form.merchantDefinedData1.value = prices[form.merchantDefinedData2.value].category; - } - }); -} +--- +--- + +function submit_payment(amount_dollars, description, metadata) { + var amount_cents = Math.round(parseFloat(amount_dollars) * 100); + if (!amount_cents || amount_cents < 50) { + alert('Please enter a valid amount (minimum $0.50).'); + return; + } + + fetch('/.netlify/functions/create-checkout-session', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ amount_cents: amount_cents, description: description, metadata: metadata }), + }) + .then(function(res) { return res.json(); }) + .then(function(data) { + if (data.url) { + window.location = data.url; + } else { + alert('Payment error. Please try again or contact mitoc-bursar@mit.edu.'); + } + }) + .catch(function() { + alert('Payment error. Please try again or contact mitoc-bursar@mit.edu.'); + }); +} + +function init_payment_forms() { + var gear_form = document.getElementById('gear_form'); + if (gear_form) { + gear_form.addEventListener('submit', function(e) { + e.preventDefault(); + var type = gear_form.querySelector('[name="payment_type"]').value; + var renter = gear_form.querySelector('[name="renter_name"]').value; + var amount = gear_form.querySelector('[name="amount"]').value; + submit_payment(amount, 'Gear: ' + type, { payment_type: type, renter_name: renter }); + }); + } + + var trip_form = document.getElementById('trip_form'); + if (trip_form) { + var prices = { + {% for fee in site.data.trip_fees %} + "{{ fee.name }}": {{ fee.price }}, + {% endfor %} + }; + + var calc_trip_amount = function() { + var trip = trip_form.querySelector('[name="trip_name"]').value; + var qty = parseInt(trip_form.querySelector('[name="quantity"]').value, 10) || 1; + trip_form.querySelector('[name="amount"]').value = + trip && prices[trip] ? (prices[trip] * qty).toFixed(2) : '0.00'; + }; + + trip_form.querySelector('[name="trip_name"]').addEventListener('change', calc_trip_amount); + trip_form.querySelector('[name="quantity"]').addEventListener('change', calc_trip_amount); + trip_form.querySelector('[name="quantity"]').addEventListener('keyup', calc_trip_amount); + + trip_form.addEventListener('submit', function(e) { + e.preventDefault(); + var trip = trip_form.querySelector('[name="trip_name"]').value; + var qty = trip_form.querySelector('[name="quantity"]').value; + var comments = trip_form.querySelector('[name="comments"]').value; + var amount = trip_form.querySelector('[name="amount"]').value; + if (!trip) { alert('Please select a trip.'); return; } + submit_payment(amount, trip, { trip: trip, quantity: qty, comments: comments }); + }); + } + + var cabin_form = document.getElementById('cabin_form'); + if (cabin_form) { + cabin_form.addEventListener('submit', function(e) { + e.preventDefault(); + var cabin = cabin_form.querySelector('[name="cabin"]').value; + var nights = cabin_form.querySelector('[name="person_nights"]').value; + var keyholder = cabin_form.querySelector('[name="keyholder_name"]').value; + var amount = cabin_form.querySelector('[name="amount"]').value; + submit_payment( + amount, + 'Cabin: ' + cabin + ' x ' + nights + ' person-nights', + { cabin: cabin, person_nights: nights, keyholder_name: keyholder } + ); + }); + } +} From 093192c02fd380f90f2f52a0b8607f2ba202f9ef Mon Sep 17 00:00:00 2001 From: Cameron Gagnon Date: Mon, 11 May 2026 00:43:09 -0400 Subject: [PATCH 3/5] chore: add Docker dev environment for local function testing The Netlify CLI is blocked by Santa on this machine. This sets up a three-container dev environment: Jekyll on 4000, a minimal Node.js wrapper for the Netlify function on 9999, and nginx proxying both under localhost:8888 so the browser sees one origin. Run with: docker-compose up Then open: http://localhost:8888/pay --- docker-compose.yml | 19 +++++++++++++++++++ docker/functions-server.js | 26 ++++++++++++++++++++++++++ docker/nginx.conf | 16 ++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 docker/functions-server.js create mode 100644 docker/nginx.conf diff --git a/docker-compose.yml b/docker-compose.yml index 971c7a10..aeec6360 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,3 +7,22 @@ services: - .:/site ports: - "4000:4000" + + functions: + image: node:18-alpine + working_dir: /app + volumes: + - .:/app + command: node docker/functions-server.js + env_file: + - .env + + proxy: + image: nginx:alpine + volumes: + - ./docker/nginx.conf:/etc/nginx/nginx.conf:ro + ports: + - "8888:8888" + depends_on: + - jekyll + - functions diff --git a/docker/functions-server.js b/docker/functions-server.js new file mode 100644 index 00000000..14badd43 --- /dev/null +++ b/docker/functions-server.js @@ -0,0 +1,26 @@ +const http = require('http'); +const handler = require('../netlify/functions/create-checkout-session'); + +const server = http.createServer(async (req, res) => { + if (req.method !== 'POST') { + res.writeHead(405); + res.end('Method Not Allowed'); + return; + } + + let body = ''; + req.on('data', chunk => body += chunk); + req.on('end', async () => { + const event = { + httpMethod: 'POST', + body, + headers: { origin: req.headers.origin || 'http://localhost:8888' }, + }; + + const result = await handler.handler(event); + res.writeHead(result.statusCode, result.headers || {}); + res.end(result.body); + }); +}); + +server.listen(9999, () => console.log('Functions server on port 9999')); diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 00000000..5c10568e --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,16 @@ +events {} + +http { + server { + listen 8888; + + location /.netlify/functions/ { + proxy_pass http://functions:9999/; + proxy_set_header Origin http://localhost:8888; + } + + location / { + proxy_pass http://jekyll:4000; + } + } +} From a90ddf2c092ad2cdfe9a74f0e5159b6d74c51f54 Mon Sep 17 00:00:00 2001 From: Cameron Gagnon Date: Mon, 11 May 2026 01:07:24 -0400 Subject: [PATCH 4/5] feat: add back to home button on payment success page --- _pages/pay/success.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/_pages/pay/success.md b/_pages/pay/success.md index 18d63206..56b70ef6 100644 --- a/_pages/pay/success.md +++ b/_pages/pay/success.md @@ -6,3 +6,5 @@ title: Payment Received Your payment was successful. A receipt has been sent to your email. Questions? Contact [mitoc-bursar@mit.edu](mailto:mitoc-bursar@mit.edu). + +Back to home page From dd5b14109691bad13e281a8e2ef314f302f5f57e Mon Sep 17 00:00:00 2001 From: Cameron Gagnon Date: Mon, 11 May 2026 01:37:28 -0400 Subject: [PATCH 5/5] feat: link back to payments page from success page --- _pages/pay/success.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_pages/pay/success.md b/_pages/pay/success.md index 56b70ef6..99a44a26 100644 --- a/_pages/pay/success.md +++ b/_pages/pay/success.md @@ -7,4 +7,4 @@ Your payment was successful. A receipt has been sent to your email. Questions? Contact [mitoc-bursar@mit.edu](mailto:mitoc-bursar@mit.edu). -Back to home page +Back to payments