diff --git a/.env.sample b/.env.sample index c3306a0..da8c4fc 100644 --- a/.env.sample +++ b/.env.sample @@ -1,5 +1,5 @@ NODE_ENV=production DATABASE_URL=postgres://postgres:postgres@host:5432/postgres?sslmode=disable&search_path=stripe STRIPE_SECRET_KEY=sk_test_ -STRIPE_WEBHOOK_SECRET=whsec_ +STRIPE_WEBHOOK_URL=https:// SCHEMA=stripe \ No newline at end of file diff --git a/README.md b/README.md index ca53459..bd3a4aa 100644 --- a/README.md +++ b/README.md @@ -78,12 +78,13 @@ This server synchronizes your Stripe account to a Postgres database. It can be a ## Usage -- Update your Stripe account with all valid webhooks and get the webhook secret +- Update STRIPE_WEBHOOK_URL with publicly available url of your webhook endpoint. - `mv .env.sample .env` and then rename all the variables - Make sure the database URL has search_path `stripe`. eg: `DATABASE_URL=postgres://postgres:postgres@hostname:5432/postgres?sslmode=disable&search_path=stripe` - Run `dbmate up` - Deploy the [docker image](https://hub.docker.com/r/supabase/stripe-sync-engine) to your favourite hosting service and expose port `8080` - Point your Stripe webooks to your deployed app. + ## Future ideas - Expose a "sync" endpoint for each table which will manually fetch and sync from Stripe. diff --git a/src/routes/webhooks.ts b/src/routes/webhooks.ts index bfd86ca..ec7864e 100644 --- a/src/routes/webhooks.ts +++ b/src/routes/webhooks.ts @@ -23,7 +23,7 @@ export default async function routes(fastify: FastifyInstance) { let event try { - event = stripe.webhooks.constructEvent(body.raw, sig, config.STRIPE_WEBHOOK_SECRET) + event = stripe.webhooks.constructEvent(body.raw, sig, process.env.STRIPE_WEBHOOK_SECRET!) } catch (err) { return reply.code(400).send(`Webhook Error: ${err.message}`) } diff --git a/src/server.ts b/src/server.ts index a51a317..43116b3 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,6 +1,7 @@ import { FastifyInstance } from 'fastify' import { Server, IncomingMessage, ServerResponse } from 'http' import build from './app' +import { registerWebhooks } from './utils/registerWebhooks' const loggerConfig = { prettyPrint: true, @@ -15,10 +16,25 @@ const app: FastifyInstance = build({ exposeDocs, }) -app.listen(8080, '0.0.0.0', (err, address) => { - if (err) { + +async function main() { + try { + await registerWebhooks(); + console.log(`Webhook initialization was successful.`); + } catch (error) { + console.error(`Refusing to start due to error on webhook initialization.`); + console.error(error.message); + process.exit(1); + } + + try { + const address = await app.listen(8080, '0.0.0.0'); + console.log(`Server listening at ${address}`); + } catch (err) { console.error(err) process.exit(1) } - console.log(`Server listening at ${address}`) -}) + +} + +main(); \ No newline at end of file diff --git a/src/utils/StripeClientManager.ts b/src/utils/StripeClientManager.ts index 66ce0d7..aaac93b 100644 --- a/src/utils/StripeClientManager.ts +++ b/src/utils/StripeClientManager.ts @@ -1,6 +1,9 @@ import Stripe from 'stripe' +import { getConfig } from './config' -export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', { +const config = getConfig(); + +export const stripe = new Stripe(config.STRIPE_SECRET_KEY, { // https://github.com/stripe/stripe-node#configuration apiVersion: '2020-08-27', appInfo: { diff --git a/src/utils/config.ts b/src/utils/config.ts index 089f30e..d282d4f 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -5,11 +5,11 @@ type configType = { NODE_ENV: string SCHEMA: string STRIPE_SECRET_KEY: string - STRIPE_WEBHOOK_SECRET: string + STRIPE_WEBHOOK_URL: string } -function getConfigFromEnv(key: string): string { - const value = process.env[key] +function getConfigFromEnv(key: string, _default?: string): string { + const value = process.env[key]; if (!value) { throw new Error(`${key} is undefined`) } @@ -24,6 +24,6 @@ export function getConfig(): configType { SCHEMA: getConfigFromEnv('SCHEMA'), NODE_ENV: getConfigFromEnv('NODE_ENV'), STRIPE_SECRET_KEY: getConfigFromEnv('STRIPE_SECRET_KEY'), - STRIPE_WEBHOOK_SECRET: getConfigFromEnv('STRIPE_WEBHOOK_SECRET'), + STRIPE_WEBHOOK_URL: getConfigFromEnv('STRIPE_WEBHOOK_URL') } } diff --git a/src/utils/registerWebhooks.ts b/src/utils/registerWebhooks.ts new file mode 100644 index 0000000..f6ea217 --- /dev/null +++ b/src/utils/registerWebhooks.ts @@ -0,0 +1,81 @@ +import { getConfig } from "./config"; +import { stripe } from "./StripeClientManager"; +import { Stripe } from "stripe"; + +const enabledEvents: Stripe.WebhookEndpointCreateParams.EnabledEvent[] = [ + "customer.created", + "customer.updated", + "customer.subscription.created", + "customer.subscription.deleted", + "customer.subscription.updated", + "invoice.created", + "invoice.finalized", + "invoice.paid", + "invoice.payment_failed", + "invoice.payment_succeeded", + "invoice.updated", + "product.created", + "product.updated", + "product.deleted", + "price.created", + "price.updated", + "price.deleted", +]; + +const config = getConfig(); + +let secret: string | undefined = undefined; + +export async function registerWebhooks() { + + let webhook: Stripe.WebhookEndpoint | undefined = undefined; + + let hooks = await stripe.webhookEndpoints.list(); + + do { + for (const endpoint of hooks.data) { + if ( + !endpoint.metadata || + !endpoint.metadata.createdByStripePostgresSync || + endpoint.url != config.STRIPE_WEBHOOK_URL + ) { + continue; + } + + if (endpoint.enabled_events.join() != enabledEvents.join()) { + console.debug(`Found a webhook that has different enabled events or url`); + console.debug(`Updating the webhook ${endpoint.url}`); + stripe.webhookEndpoints.update(endpoint.id, { + enabled_events: enabledEvents + }) + } + + webhook = endpoint; + } + + if (hooks.has_more) { + hooks = await stripe.webhookEndpoints.list({ + starting_after: hooks.data[hooks.data.length - 1].id + }); + } + } while (hooks.has_more); + + if (webhook == undefined) { + console.log("There was no webhook matching the url and enabled events."); + console.log(`Creating a new webhook with url ${config.STRIPE_WEBHOOK_URL}`); + webhook = await stripe.webhookEndpoints.create({ + url: config.STRIPE_WEBHOOK_URL, + enabled_events: enabledEvents, + description: "Created by Stripe Postgres Sync", + metadata: { + createdByStripePostgresSync: "true" + }, + }); + } + + process.env.STRIPE_WEBHOOK_SECRET = webhook.secret; +} + +export function getSecret(): string | undefined { + return secret; +} \ No newline at end of file