From 804eed1b77f9e1474fbd2b46bae373d2cdfa12a5 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 4 Apr 2026 17:44:34 -0400 Subject: [PATCH 1/3] add createCheckout backed by recaptcha and honeypot --- netlify/functions/createCheckout.js | 5 +++ src/actions/createCheckout.js | 64 +++++++++++++++++++++++++++++ src/integrations/recaptcha.js | 36 ++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 netlify/functions/createCheckout.js create mode 100644 src/actions/createCheckout.js create mode 100644 src/integrations/recaptcha.js diff --git a/netlify/functions/createCheckout.js b/netlify/functions/createCheckout.js new file mode 100644 index 0000000..06dd4ab --- /dev/null +++ b/netlify/functions/createCheckout.js @@ -0,0 +1,5 @@ +'use strict'; + +const extrovert = require('extrovert'); + +module.exports = extrovert.toNetlifyFunction(require('../../src/actions/createCheckout')); diff --git a/src/actions/createCheckout.js b/src/actions/createCheckout.js new file mode 100644 index 0000000..399ecb3 --- /dev/null +++ b/src/actions/createCheckout.js @@ -0,0 +1,64 @@ +'use strict'; + +const Archetype = require('archetype'); +const recaptcha = require('../integrations/recaptcha'); +const stripe = require('../integrations/stripe'); + +const CreateCheckoutParams = new Archetype({ + name: { + $type: 'string', + $required: true + }, + email: { + $type: 'string', + $required: true + }, + plan: { + $type: 'string', + $required: true, + $enum: ['solo', 'pro'] + }, + website: { + $type: 'string' + }, + response: { + $type: 'string' + } +}).compile('CreateCheckoutParams'); + +const priceIds = { + solo: process.env.STRIPE_SOLO_PRICE_ID, + pro: process.env.STRIPE_PRO_PRICE_ID +}; + +module.exports = async function createCheckout(params) { + const { name, email, plan, website, response } = new CreateCheckoutParams(params); + + if (website) { + throw new Error('This request was flagged as spam. If this is an error, please refresh and try again.'); + } + + const recaptchaResult = await recaptcha.verify(response); + console.log(recaptchaResult); + if (!recaptchaResult.success || recaptchaResult.score < 0.7 || recaptchaResult.action !== 'checkout') { + throw new Error('Captcha verification failed'); + } + + const priceId = priceIds[plan]; + if (!priceId) { + throw new Error('Price not configured for plan: ' + plan); + } + + const returnUrl = (process.env.PUBLIC_APP_BASE_URL || 'https://studio.mongoosejs.io') + '/my-account.html'; + + const session = await stripe.client.checkout.sessions.create({ + ui_mode: 'embedded', + mode: 'subscription', + customer_email: email, + line_items: [{ price: priceId, quantity: 1 }], + return_url: returnUrl, + metadata: { name, plan } + }); + + return { clientSecret: session.client_secret }; +}; diff --git a/src/integrations/recaptcha.js b/src/integrations/recaptcha.js new file mode 100644 index 0000000..4da5eed --- /dev/null +++ b/src/integrations/recaptcha.js @@ -0,0 +1,36 @@ +'use strict'; + +const qs = require('querystring'); + +const verifyUrl = 'https://www.google.com/recaptcha/api/siteverify'; + +exports.verify = async function verify(response, remoteIp) { + if (!response) { + return { success: false, score: 0 }; + } + + const secret = process.env.RECAPTCHA_SECRET_KEY; + + const body = qs.stringify({ + secret, + response, + remoteip: remoteIp + }); + + const res = await fetch(verifyUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body + }); + + if (!res.ok) { + console.log('Error verifying reCAPTCHA', await res.text()); + throw new Error('Failed to verify reCAPTCHA'); + } + + const data = await res.json(); + + return data; +}; From c3502c446142180b79432825f120a4c2d80f9c79 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 4 Apr 2026 17:52:02 -0400 Subject: [PATCH 2/3] store contact on create checkout and support solo tier --- src/actions/createCheckout.js | 10 ++++++++++ src/db/index.js | 2 ++ src/db/workspace.js | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/actions/createCheckout.js b/src/actions/createCheckout.js index 399ecb3..1935810 100644 --- a/src/actions/createCheckout.js +++ b/src/actions/createCheckout.js @@ -1,6 +1,7 @@ 'use strict'; const Archetype = require('archetype'); +const connect = require('../db'); const recaptcha = require('../integrations/recaptcha'); const stripe = require('../integrations/stripe'); @@ -44,6 +45,15 @@ module.exports = async function createCheckout(params) { throw new Error('Captcha verification failed'); } + const db = await connect(); + const { Contact } = db.models; + + await Contact.findOneAndUpdate( + { email: email.toLowerCase() }, + { $set: { name }, $setOnInsert: { email: email.toLowerCase() } }, + { upsert: true } + ); + const priceId = priceIds[plan]; if (!priceId) { throw new Error('Price not configured for plan: ' + plan); diff --git a/src/db/index.js b/src/db/index.js index 407c701..3a904dd 100644 --- a/src/db/index.js +++ b/src/db/index.js @@ -3,6 +3,7 @@ const mongoose = require('mongoose'); const accessTokenSchema = require('./AccessToken'); +const contactSchema = require('./Contact'); const contentSchema = require('./content'); const dashboardResultSchema = require('./DashboardResult'); const invitationSchema = require('./invitation'); @@ -20,6 +21,7 @@ module.exports = async function connect() { await mongoose.connect(uri, { serverSelectionTimeoutMS: 5000 }); if (Object.keys(mongoose.models).length === 0) { mongoose.model('AccessToken', accessTokenSchema, 'AccessToken'); + mongoose.model('Contact', contactSchema, 'Contact'); mongoose.model('Content', contentSchema, 'Content'); mongoose.model('DashboardResult', dashboardResultSchema, 'DashboardResult'); mongoose.model('Invitation', invitationSchema, 'Invitation'); diff --git a/src/db/workspace.js b/src/db/workspace.js index feda508..85776c6 100644 --- a/src/db/workspace.js +++ b/src/db/workspace.js @@ -40,7 +40,7 @@ const workspaceSchema = new mongoose.Schema({ }, subscriptionTier: { type: String, - enum: ['', 'free', 'pro'] + enum: ['', 'free', 'solo', 'pro'] }, stripeCustomerEmail: { type: String, From d628fd4cdefbe7d05537e2fe49d4a73bbe85a1fc Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 4 Apr 2026 17:53:00 -0400 Subject: [PATCH 3/3] add missing file --- src/db/Contact.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/db/Contact.js diff --git a/src/db/Contact.js b/src/db/Contact.js new file mode 100644 index 0000000..efdeffa --- /dev/null +++ b/src/db/Contact.js @@ -0,0 +1,12 @@ +'use strict'; + +const mongoose = require('mongoose'); + +const contactSchema = new mongoose.Schema({ + email: { type: String, required: true, unique: true }, + name: { type: String }, + company: { type: String }, + deletedAt: { type: Date, default: null } +}, { timestamps: true }); + +module.exports = contactSchema;