Skip to content

Commit

Permalink
refactor: gatsby functions
Browse files Browse the repository at this point in the history
  • Loading branch information
raae committed Oct 8, 2021
1 parent cf62c82 commit ff9f56e
Show file tree
Hide file tree
Showing 10 changed files with 433 additions and 1 deletion.
5 changes: 4 additions & 1 deletion netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@
[[plugins]]
package = 'netlify-plugin-contextual-env'
[plugins.inputs]
mode = 'suffix'
mode = 'suffix'

[[plugins]]
package = "netlify-plugin-inline-functions-env"
25 changes: 25 additions & 0 deletions src/api-utils/error-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Joi from "joi"

const errorHandler = (req, res, error) => {
// Error response present if it's an axios error
// statusCode present if it's a custom http-error
let status = error.response?.status || error.statusCode || 500
const message = error.response?.data?.message || error.message

// Check to see if it's a Joi error,
// and make sure to expose it
if (Joi.isError(error)) {
status = 422
error.expose = true
}

// Something went wrong, log it
console.error(`${status} -`, message)

// Respond with error code and message
res.status(status).json({
message: error.expose ? message : `Faulty ${req.baseUrl}`,
})
}

export default errorHandler
23 changes: 23 additions & 0 deletions src/api-utils/services/stripe.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import StripeAPI from "stripe"

export const Stripe = (secretKey = process.env.STRIPE_SECRET_KEY) => {
const stripeApi = StripeAPI(secretKey)

const log = (...args) => {
console.log("Stripe:", ...args)
}

const getStripeSubscription = async ({ id }) => {
const subscription = await stripeApi.subscriptions.retrieve(id)

log("Fetched subscription", { stripeSubscriptionId: subscription.id })

return subscription
}

return {
getStripeSubscription,
}
}

export default Stripe()
54 changes: 54 additions & 0 deletions src/api-utils/services/userbase.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import axios from "axios"

export const Userbase = (
accessToken = process.env.USERBASE_ADMIN_API_ACCESS_TOKEN
) => {
const userbaseApi = axios.create({
baseURL: "https://v1.userbase.com/v1/admin",
headers: {
Authorization: `Bearer ${accessToken}`,
},
})

const log = (...args) => {
console.log("Userbase:", ...args)
}

const verifyUserbaseAuthToken = async ({
userbaseAuthToken,
userbaseUserId,
}) => {
const { data: user } = await userbaseApi.get(
"auth-tokens/" + userbaseAuthToken
)

log("Auth token verified", {
userbaseUserId: data.userId,
})

if (user.userId !== userbaseUserId) {
throw Error("userbaseAuthToken / userbaseUserId mismatch")
}

return user
}

const getUserbaseUser = async ({ userbaseUserId }) => {
let { data: user } = await userbaseApi.get("users/" + userbaseUserId)

user.stripeData = user[process.env.USERBASE_STRIPE_ENV] || {}
user.protectedProfile = user.protectedProfile || {}
user.profile = user.profile || {}

log("Fetched user", { userbaseUserId: user.userId, user })

return user
}

return {
verifyUserbaseAuthToken,
getUserbaseUser,
}
}

export default Userbase()
37 changes: 37 additions & 0 deletions src/api-utils/services/userlist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import axios from "axios"

export const Userlist = (pushKey = process.env.USERLIST_PUSH_KEY) => {
const userlistApi = axios.create({
baseURL: "https://push.userlist.com",
headers: {
Authorization: `Push ${pushKey}`,
},
})

const log = (...args) => {
console.log("Userlist:", ...args)
}

const upsertUserlistSubscriber = async ({
identifier,
email,
signed_up_at,
properties,
}) => {
const { data, status } = await userlistApi.post(`users`, {
identifier,
email,
signed_up_at,
properties,
})

log("Subscriber updated", identifier, data)
log("Subscriber updated - status", status)
}

return {
upsertUserlistSubscriber,
}
}

export default Userlist()
50 changes: 50 additions & 0 deletions src/api-utils/sync-settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import createError from "http-errors"
import userbase from "./services/userbase"
import userlist from "./services/userlist"

const syncSettings = async (userbaseUserId) => {
try {
// Get the Userbase User and pluck data wanted
const userbaseUser = await userbase.getUserbaseUser({ userbaseUserId })

const {
userId,
email,
creationDate,
profile: { newsletter },
} = userbaseUser

const props = {
newsletter_at: null,
test: null,
}

if (newsletter == "1") {
// == Matches both 1 and "1"
// Signed up before using date
props.newsletter_at = new Date(0)
} else if (newsletter == "0") {
// == Matches both 0 and "0"
props.newsletter_at = null
} else if (newsletter) {
// Hopefully a date
props.newsletter_at = new Date(newsletter)
}

// Push data on user into Userlist
await userlist.upsertUserlistSubscriber({
identifier: userId,
email: email,
signed_up_at: creationDate,
properties: props,
})
console.log("syncSettings - success", userId)
} catch (error) {
const { message } = error.response?.data || error.request?.data || error
throw new createError.InternalServerError(
"syncSettings - error: " + message
)
}
}

export default syncSettings
85 changes: 85 additions & 0 deletions src/api-utils/sync-subscription.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import createError from "http-errors"
import userbase from "./services/userbase"
import userlist from "./services/userlist"
import stripe from "./services/stripe"

const timestampDate = (timestamp) => {
return timestamp ? new Date(timestamp * 1000) : null
}

const syncSubscription = async (userbaseUserId) => {
try {
// Get the Userbase User and pluck data wanted
const userbaseUser = await userbase.getUserbaseUser({ userbaseUserId })

const {
userId,
email,
username,
creationDate,
size,
protectedProfile: {
stripeCustomerId: oldStripeCustomerId,
stripePlanId: oldStripePlanId,
},
stripeData: { subscriptionPlanId, subscriptionId },
} = userbaseUser

const props = {
username: username,
size: size,
// Must use null to null stuff out in userlist,
// undefined does not work
subscription_id: subscriptionId || null,
subscription_plan_id: subscriptionPlanId || null,
subscription_status: null,
subscription_created_at: null,
subscription_start_date: null,
subscription_cancel_at: null,
subscription_canceled_at: null,
subscription_ended_at: null,
}

if (subscriptionId) {
const {
status,
created,
start_date,
cancel_at,
canceled_at,
ended_at,
} = await stripe.getStripeSubscription({
id: subscriptionId,
})

props.subscription_status = status
props.subscription_created_at = timestampDate(created)
props.subscription_start_date = timestampDate(start_date)
props.subscription_cancel_at = timestampDate(cancel_at)
props.subscription_canceled_at = timestampDate(canceled_at)
props.subscription_ended_at = timestampDate(ended_at)
} else if (oldStripeCustomerId) {
// Older users that cancelled before migrating to Userbase's integrated payment
props.subscription_id = null // Did not save subscription id
props.subscription_plan_id = oldStripePlanId
props.subscription_status = "canceled" // "canceled" is same as used by Stripe, no double "ll"
}

// Push data on user into Userlist
await userlist.upsertUserlistSubscriber({
identifier: userId,
email: email,
signed_up_at: creationDate,
properties: props,
})

console.log("syncSubscription - success", userId)
} catch (error) {
const { message } = error.response?.data || error.request?.data || error
throw new createError.InternalServerError(
"syncSubscription - error: " + message
)
}
}

export default syncSubscription
65 changes: 65 additions & 0 deletions src/api/stripe-webhook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import createError from "http-errors"
import Joi from "joi"
import errorHandler from "../api-utils/error-handler"
import syncSubscription from "../api-utils/sync-subscription"

/*
* Webhook used to keep the Stripe Subscription information
* in sync with Userlist on all subscription events.
*
* @todo add a secret as query param, to mimimize abuse?
*
*/

export default async function handler(req, res) {
console.log(`${req.baseUrl} - ${req.method}`)

try {
// Only handle POST requests for webhooks
if (req.method === "POST") {
await webhookHandler(req, res)
} else {
throw createError(405, `${req.method} not allowed`)
}
} catch (error) {
errorHandler(req, res, error)
}
}

const webhookHandler = async (req, res) => {
// 1. Validate

const bodySchema = Joi.object({
type: Joi.string()
.valid(
"customer.subscription.created",
"customer.subscription.updated",
"customer.subscription.deleted"
)
.required(),
data: Joi.object({
object: Joi.object({
metadata: {
__userbase_user_id: Joi.string().required(),
},
}).required(),
}).required(),
}).options({ allowUnknown: true })

// Deconstruct and rename user id
const {
data: {
object: {
metadata: { __userbase_user_id: userbaseUserId },
},
},
} = await bodySchema.validateAsync(req.body)

// 2. Do the thing

await syncSubscription(userbaseUserId)

// 3. Respond

res.send("OK")
}
Loading

0 comments on commit ff9f56e

Please sign in to comment.