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..1935810 --- /dev/null +++ b/src/actions/createCheckout.js @@ -0,0 +1,74 @@ +'use strict'; + +const Archetype = require('archetype'); +const connect = require('../db'); +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 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); + } + + 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/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; 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, 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; +};