Skip to content

Commit ea37088

Browse files
committed
stripe onboarding
1 parent 721652f commit ea37088

9 files changed

+150
-5
lines changed

index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const topLevelFiles = new Set(
1919
fs.readdirSync('./public').filter(file => file.endsWith('.html'))
2020
);
2121

22-
app.use('/.netlify/functions', cors(), express.json(), function netlifyFunctionsMiddleware(req, res) {
22+
app.use('/.netlify/functions', cors(), express.json({ verify: (req, res, buf) => { req.rawBody = buf; } }), function netlifyFunctionsMiddleware(req, res) {
2323
const actionName = req.path.replace(/^\//, '');
2424
if (!netlifyFunctions.hasOwnProperty(actionName)) {
2525
throw new Error(`Action ${actionName} not found`);
@@ -29,6 +29,7 @@ app.use('/.netlify/functions', cors(), express.json(), function netlifyFunctions
2929
const params = {
3030
headers: req.headers,
3131
body: JSON.stringify(req.body),
32+
rawBody: req.rawBody,
3233
queryStringParameters: req.query
3334
};
3435
action.handler(params).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use strict';
2+
3+
const extrovert = require('extrovert');
4+
5+
module.exports = extrovert.toNetlifyFunction(require('../../src/actions/getWorkspaceCustomerPortalLink'));

netlify/functions/stripeWebhook.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use strict';
2+
3+
const extrovert = require('extrovert');
4+
5+
module.exports = extrovert.toNetlifyFunction(require('../../src/actions/stripeWebhook'));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use strict';
2+
3+
const Archetype = require('archetype');
4+
const connect = require('../../src/db');
5+
const mongoose = require('mongoose');
6+
const stripe = require('../integrations/stripe');
7+
8+
const GetInvitationsForWorkspaceParams = new Archetype({
9+
authorization: {
10+
$type: 'string',
11+
$required: true
12+
},
13+
workspaceId: {
14+
$type: mongoose.Types.ObjectId,
15+
$required: true
16+
}
17+
}).compile('GetInvitationsParams');
18+
19+
module.exports = async function getWorkspaceTeam(params) {
20+
const { authorization, workspaceId } = new GetInvitationsForWorkspaceParams(params);
21+
22+
const db = await connect();
23+
const { AccessToken, Invitation, User, Workspace } = db.models;
24+
25+
// Find the user linked to the access token
26+
const accessToken = await AccessToken.findById(authorization).orFail(new Error('Invalid access token'));
27+
const userId = accessToken.userId;
28+
29+
// Find the workspace and check user permissions
30+
const workspace = await Workspace.findById(workspaceId).orFail(new Error('Workspace not found'));
31+
32+
const isAuthorized = workspace.members.some(member =>
33+
member.userId.toString() === userId.toString() && member.roles.find(role => role === 'admin' || role === 'owner')
34+
);
35+
36+
if (!isAuthorized) {
37+
throw new Error('User is not authorized to view workspace team');
38+
}
39+
40+
const url = await stripe.createCustomerPortalLink(workspace.stripeCustomerId);
41+
42+
return { url };
43+
};

src/actions/inviteToWorkspace.js

+3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ module.exports = async function inviteToWorkspace(params) {
4848
if (inviterRoles == null || (!inviterRoles.includes('admin') && !inviterRoles.includes('owner'))) {
4949
throw new Error('Forbidden');
5050
}
51+
if (workspace.subscriptionTier !== 'pro') {
52+
throw new Error('Cannot invite user without creating a subscription');
53+
}
5154

5255
const isAlreadyMember = await User.exists(
5356
{ _id: { $in: workspace.members.map(member => member.userId) },

src/actions/removeFromWorkspace.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const Archetype = require('archetype');
44
const connect = require('../../src/db');
55
const mongoose = require('mongoose');
6+
const stripe = require('../integrations/stripe');
67

78
const RemoveFromWorkspaceParams = new Archetype({
89
authorization: {
@@ -21,7 +22,7 @@ const RemoveFromWorkspaceParams = new Archetype({
2122

2223
module.exports = async function removeFromWorkspace(params) {
2324
const db = await connect();
24-
const { AccessToken, Workspace } = db.models;
25+
const { AccessToken, User, Workspace } = db.models;
2526

2627
const { authorization, workspaceId, userId } = new RemoveFromWorkspaceParams(params);
2728

@@ -46,5 +47,11 @@ module.exports = async function removeFromWorkspace(params) {
4647
workspace.members.splice(memberIndex, 1);
4748
await workspace.save();
4849

49-
return { workspace };
50+
const users = await User.find({ _id: { $in: workspace.members.map(member => member.userId) } });
51+
if (workspace.stripeSubscriptionId) {
52+
const seats = users.filter(user => !user.isFreeUser).length;
53+
await stripe.updateSubscriptionSeats(workspace.stripeSubscriptionId, seats);
54+
}
55+
56+
return { workspace, users };
5057
};

src/actions/stripeWebhook.js

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'use strict';
2+
3+
const Archetype = require('archetype');
4+
const assert = require('assert');
5+
const connect = require('../../src/db');
6+
const mongoose = require('mongoose');
7+
const stripe = require('../integrations/stripe');
8+
9+
const StripeWebhookParams = new Archetype({
10+
type: {
11+
$type: 'string'
12+
},
13+
data: {
14+
object: {
15+
client_reference_id: {
16+
$type: 'string'
17+
},
18+
customer: {
19+
$type: 'string'
20+
},
21+
subscription: {
22+
$type: 'string'
23+
}
24+
}
25+
}
26+
}).compile('StripeWebhookParams');
27+
28+
module.exports = async function stripeWebhook(params, req) {
29+
console.log('AB', req, req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET);
30+
try {
31+
stripe.client.webhooks.constructEvent(req.rawBody, req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET);
32+
} catch (err) {
33+
console.log('Caught', err);
34+
throw new Error('Invalid webhook signature');
35+
}
36+
37+
const db = await connect();
38+
39+
const { Workspace } = db.models;
40+
41+
const { type, data } = new StripeWebhookParams(params);
42+
43+
if (type === 'checkout.session.completed') {
44+
const workspaceId = data?.object?.client_reference_id;
45+
assert.ok(workspaceId, 'no workspace id found');
46+
const workspace = await Workspace.findById(workspaceId).orFail();
47+
assert.ok(!workspace.stripeSubscriptionId, 'workspace already has a subscription');
48+
assert.ok(data.object.customer, 'no customer found in webhook');
49+
assert.ok(data.object.subscription, 'no subscription found in webhook');
50+
51+
workspace.stripeCustomerId = data.object.customer;
52+
workspace.stripeSubscriptionId = data.object.subscription;
53+
workspace.subscriptionTier = 'pro';
54+
await workspace.save();
55+
56+
return { workspace };
57+
}
58+
59+
return {};
60+
};

src/db/workspace.js

+11
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,15 @@ const workspaceSchema = new mongoose.Schema({
4848

4949
workspaceSchema.index({ apiKey: 1 }, { unique: true });
5050

51+
workspaceSchema.virtual('pricePerSeat').get(function pricePerSeat() {
52+
if (this.subscriptionTier === 'free') {
53+
return 0;
54+
}
55+
if (this.subscriptionTier === 'pro') {
56+
return 19;
57+
}
58+
59+
return null;
60+
});
61+
5162
module.exports = workspaceSchema;

src/integrations/stripe.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
const assert = require('assert');
44
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
55

6+
exports.client = stripe;
7+
68
exports.listSubscriptions = async function listSubscriptions() {
79
const subscriptions = await stripe.subscriptions.list();
810
return subscriptions;
911
};
1012

11-
1213
exports.getSubscription = async function getSubscription(subscriptionId) {
1314
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
1415
return subscription;
@@ -26,4 +27,13 @@ exports.updateSubscriptionSeats = async function updateSubscriptionSeats(subscri
2627
);
2728

2829
return updatedSubscription;
29-
};
30+
};
31+
32+
exports.createCustomerPortalLink = async function createCustomerPortalLink(customerId, returnUrl) {
33+
const portalSession = await stripe.billingPortal.sessions.create({
34+
customer: customerId,
35+
return_url: returnUrl // URL to redirect the user back after they leave the portal
36+
});
37+
38+
return portalSession.url; // This is the link to the customer portal
39+
};

0 commit comments

Comments
 (0)