Skip to content

Commit ff9f56e

Browse files
committed
refactor: gatsby functions
1 parent cf62c82 commit ff9f56e

File tree

10 files changed

+433
-1
lines changed

10 files changed

+433
-1
lines changed

netlify.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,7 @@
66
[[plugins]]
77
package = 'netlify-plugin-contextual-env'
88
[plugins.inputs]
9-
mode = 'suffix'
9+
mode = 'suffix'
10+
11+
[[plugins]]
12+
package = "netlify-plugin-inline-functions-env"

src/api-utils/error-handler.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Joi from "joi"
2+
3+
const errorHandler = (req, res, error) => {
4+
// Error response present if it's an axios error
5+
// statusCode present if it's a custom http-error
6+
let status = error.response?.status || error.statusCode || 500
7+
const message = error.response?.data?.message || error.message
8+
9+
// Check to see if it's a Joi error,
10+
// and make sure to expose it
11+
if (Joi.isError(error)) {
12+
status = 422
13+
error.expose = true
14+
}
15+
16+
// Something went wrong, log it
17+
console.error(`${status} -`, message)
18+
19+
// Respond with error code and message
20+
res.status(status).json({
21+
message: error.expose ? message : `Faulty ${req.baseUrl}`,
22+
})
23+
}
24+
25+
export default errorHandler

src/api-utils/services/stripe.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import StripeAPI from "stripe"
2+
3+
export const Stripe = (secretKey = process.env.STRIPE_SECRET_KEY) => {
4+
const stripeApi = StripeAPI(secretKey)
5+
6+
const log = (...args) => {
7+
console.log("Stripe:", ...args)
8+
}
9+
10+
const getStripeSubscription = async ({ id }) => {
11+
const subscription = await stripeApi.subscriptions.retrieve(id)
12+
13+
log("Fetched subscription", { stripeSubscriptionId: subscription.id })
14+
15+
return subscription
16+
}
17+
18+
return {
19+
getStripeSubscription,
20+
}
21+
}
22+
23+
export default Stripe()

src/api-utils/services/userbase.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import axios from "axios"
2+
3+
export const Userbase = (
4+
accessToken = process.env.USERBASE_ADMIN_API_ACCESS_TOKEN
5+
) => {
6+
const userbaseApi = axios.create({
7+
baseURL: "https://v1.userbase.com/v1/admin",
8+
headers: {
9+
Authorization: `Bearer ${accessToken}`,
10+
},
11+
})
12+
13+
const log = (...args) => {
14+
console.log("Userbase:", ...args)
15+
}
16+
17+
const verifyUserbaseAuthToken = async ({
18+
userbaseAuthToken,
19+
userbaseUserId,
20+
}) => {
21+
const { data: user } = await userbaseApi.get(
22+
"auth-tokens/" + userbaseAuthToken
23+
)
24+
25+
log("Auth token verified", {
26+
userbaseUserId: data.userId,
27+
})
28+
29+
if (user.userId !== userbaseUserId) {
30+
throw Error("userbaseAuthToken / userbaseUserId mismatch")
31+
}
32+
33+
return user
34+
}
35+
36+
const getUserbaseUser = async ({ userbaseUserId }) => {
37+
let { data: user } = await userbaseApi.get("users/" + userbaseUserId)
38+
39+
user.stripeData = user[process.env.USERBASE_STRIPE_ENV] || {}
40+
user.protectedProfile = user.protectedProfile || {}
41+
user.profile = user.profile || {}
42+
43+
log("Fetched user", { userbaseUserId: user.userId, user })
44+
45+
return user
46+
}
47+
48+
return {
49+
verifyUserbaseAuthToken,
50+
getUserbaseUser,
51+
}
52+
}
53+
54+
export default Userbase()

src/api-utils/services/userlist.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import axios from "axios"
2+
3+
export const Userlist = (pushKey = process.env.USERLIST_PUSH_KEY) => {
4+
const userlistApi = axios.create({
5+
baseURL: "https://push.userlist.com",
6+
headers: {
7+
Authorization: `Push ${pushKey}`,
8+
},
9+
})
10+
11+
const log = (...args) => {
12+
console.log("Userlist:", ...args)
13+
}
14+
15+
const upsertUserlistSubscriber = async ({
16+
identifier,
17+
email,
18+
signed_up_at,
19+
properties,
20+
}) => {
21+
const { data, status } = await userlistApi.post(`users`, {
22+
identifier,
23+
email,
24+
signed_up_at,
25+
properties,
26+
})
27+
28+
log("Subscriber updated", identifier, data)
29+
log("Subscriber updated - status", status)
30+
}
31+
32+
return {
33+
upsertUserlistSubscriber,
34+
}
35+
}
36+
37+
export default Userlist()

src/api-utils/sync-settings.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import createError from "http-errors"
2+
import userbase from "./services/userbase"
3+
import userlist from "./services/userlist"
4+
5+
const syncSettings = async (userbaseUserId) => {
6+
try {
7+
// Get the Userbase User and pluck data wanted
8+
const userbaseUser = await userbase.getUserbaseUser({ userbaseUserId })
9+
10+
const {
11+
userId,
12+
email,
13+
creationDate,
14+
profile: { newsletter },
15+
} = userbaseUser
16+
17+
const props = {
18+
newsletter_at: null,
19+
test: null,
20+
}
21+
22+
if (newsletter == "1") {
23+
// == Matches both 1 and "1"
24+
// Signed up before using date
25+
props.newsletter_at = new Date(0)
26+
} else if (newsletter == "0") {
27+
// == Matches both 0 and "0"
28+
props.newsletter_at = null
29+
} else if (newsletter) {
30+
// Hopefully a date
31+
props.newsletter_at = new Date(newsletter)
32+
}
33+
34+
// Push data on user into Userlist
35+
await userlist.upsertUserlistSubscriber({
36+
identifier: userId,
37+
email: email,
38+
signed_up_at: creationDate,
39+
properties: props,
40+
})
41+
console.log("syncSettings - success", userId)
42+
} catch (error) {
43+
const { message } = error.response?.data || error.request?.data || error
44+
throw new createError.InternalServerError(
45+
"syncSettings - error: " + message
46+
)
47+
}
48+
}
49+
50+
export default syncSettings

src/api-utils/sync-subscription.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import createError from "http-errors"
2+
import userbase from "./services/userbase"
3+
import userlist from "./services/userlist"
4+
import stripe from "./services/stripe"
5+
6+
const timestampDate = (timestamp) => {
7+
return timestamp ? new Date(timestamp * 1000) : null
8+
}
9+
10+
const syncSubscription = async (userbaseUserId) => {
11+
try {
12+
// Get the Userbase User and pluck data wanted
13+
const userbaseUser = await userbase.getUserbaseUser({ userbaseUserId })
14+
15+
const {
16+
userId,
17+
email,
18+
username,
19+
creationDate,
20+
size,
21+
protectedProfile: {
22+
stripeCustomerId: oldStripeCustomerId,
23+
stripePlanId: oldStripePlanId,
24+
},
25+
stripeData: { subscriptionPlanId, subscriptionId },
26+
} = userbaseUser
27+
28+
const props = {
29+
username: username,
30+
size: size,
31+
// Must use null to null stuff out in userlist,
32+
// undefined does not work
33+
subscription_id: subscriptionId || null,
34+
subscription_plan_id: subscriptionPlanId || null,
35+
subscription_status: null,
36+
subscription_created_at: null,
37+
subscription_start_date: null,
38+
subscription_cancel_at: null,
39+
subscription_canceled_at: null,
40+
subscription_ended_at: null,
41+
}
42+
43+
if (subscriptionId) {
44+
const {
45+
status,
46+
created,
47+
start_date,
48+
cancel_at,
49+
canceled_at,
50+
ended_at,
51+
} = await stripe.getStripeSubscription({
52+
id: subscriptionId,
53+
})
54+
55+
props.subscription_status = status
56+
props.subscription_created_at = timestampDate(created)
57+
props.subscription_start_date = timestampDate(start_date)
58+
props.subscription_cancel_at = timestampDate(cancel_at)
59+
props.subscription_canceled_at = timestampDate(canceled_at)
60+
props.subscription_ended_at = timestampDate(ended_at)
61+
} else if (oldStripeCustomerId) {
62+
// Older users that cancelled before migrating to Userbase's integrated payment
63+
props.subscription_id = null // Did not save subscription id
64+
props.subscription_plan_id = oldStripePlanId
65+
props.subscription_status = "canceled" // "canceled" is same as used by Stripe, no double "ll"
66+
}
67+
68+
// Push data on user into Userlist
69+
await userlist.upsertUserlistSubscriber({
70+
identifier: userId,
71+
email: email,
72+
signed_up_at: creationDate,
73+
properties: props,
74+
})
75+
76+
console.log("syncSubscription - success", userId)
77+
} catch (error) {
78+
const { message } = error.response?.data || error.request?.data || error
79+
throw new createError.InternalServerError(
80+
"syncSubscription - error: " + message
81+
)
82+
}
83+
}
84+
85+
export default syncSubscription

src/api/stripe-webhook.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import createError from "http-errors"
2+
import Joi from "joi"
3+
import errorHandler from "../api-utils/error-handler"
4+
import syncSubscription from "../api-utils/sync-subscription"
5+
6+
/*
7+
* Webhook used to keep the Stripe Subscription information
8+
* in sync with Userlist on all subscription events.
9+
*
10+
* @todo add a secret as query param, to mimimize abuse?
11+
*
12+
*/
13+
14+
export default async function handler(req, res) {
15+
console.log(`${req.baseUrl} - ${req.method}`)
16+
17+
try {
18+
// Only handle POST requests for webhooks
19+
if (req.method === "POST") {
20+
await webhookHandler(req, res)
21+
} else {
22+
throw createError(405, `${req.method} not allowed`)
23+
}
24+
} catch (error) {
25+
errorHandler(req, res, error)
26+
}
27+
}
28+
29+
const webhookHandler = async (req, res) => {
30+
// 1. Validate
31+
32+
const bodySchema = Joi.object({
33+
type: Joi.string()
34+
.valid(
35+
"customer.subscription.created",
36+
"customer.subscription.updated",
37+
"customer.subscription.deleted"
38+
)
39+
.required(),
40+
data: Joi.object({
41+
object: Joi.object({
42+
metadata: {
43+
__userbase_user_id: Joi.string().required(),
44+
},
45+
}).required(),
46+
}).required(),
47+
}).options({ allowUnknown: true })
48+
49+
// Deconstruct and rename user id
50+
const {
51+
data: {
52+
object: {
53+
metadata: { __userbase_user_id: userbaseUserId },
54+
},
55+
},
56+
} = await bodySchema.validateAsync(req.body)
57+
58+
// 2. Do the thing
59+
60+
await syncSubscription(userbaseUserId)
61+
62+
// 3. Respond
63+
64+
res.send("OK")
65+
}

0 commit comments

Comments
 (0)