Skip to content

Commit

Permalink
Provision Stripe and Api with SST
Browse files Browse the repository at this point in the history
  • Loading branch information
Murderlon committed Oct 24, 2024
1 parent 85a1bf3 commit 7456a7f
Show file tree
Hide file tree
Showing 17 changed files with 128 additions and 73 deletions.
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# SST secrets are for runtime values.
# For secrets needed at build and deploy time, SST automatically picks up the .env file.

# https://docs.stripe.com/keys
# NOTE: this is the Stripe secret, not the public key
STRIPE_API_KEY=""
# https://developers.cloudflare.com/fundamentals/api/get-started/create-token/
CLOUDFLARE_API_TOKEN=""
# https://developers.cloudflare.com/fundamentals/setup/find-account-and-zone-ids/
CLOUDFLARE_DEFAULT_ACCOUNT_ID=""
6 changes: 3 additions & 3 deletions app/routes/dashboard+/settings.index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export async function action({ request }: ActionFunctionArgs) {
})
}

// TODO: cancel Stripe subscription
if (intent === INTENTS.USER_DELETE_ACCOUNT) {
await db.delete(schema.user).where(eq(schema.user.id, user.id))
return redirect(HOME_PATH, {
Expand Down Expand Up @@ -224,9 +225,8 @@ export default function DashboardSettings() {
autoComplete="off"
defaultValue={user?.username ?? ''}
required
className={`w-80 bg-transparent ${
username.errors && 'border-destructive focus-visible:ring-destructive'
}`}
className={`w-80 bg-transparent ${username.errors && 'border-destructive focus-visible:ring-destructive'
}`}
{...getInputProps(username, { type: 'text' })}
/>
{username.errors && (
Expand Down
1 change: 0 additions & 1 deletion app/routes/resources+/upload-image.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { ActionFunctionArgs } from '@remix-run/router'
import {
json,
// TODO: is this still unstable?
unstable_createMemoryUploadHandler,
unstable_parseMultipartFormData,
MaxPartSizeExceededError,
Expand Down
17 changes: 1 addition & 16 deletions app/utils/misc.server.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { getClientLocales } from 'remix-utils/locales/server'
import { CURRENCIES } from '#app/modules/stripe/plans'
import { Resource } from 'sst'

/**
* HTTP.
*/
export const HOST_URL =
process.env.NODE_ENV === 'production' ? Resource.Remix.url : 'http://localhost:3000'
export const HOST_URL = process.env.HOST_URL

export function getDomainUrl(request: Request) {
const host = request.headers.get('X-Forwarded-Host') ?? request.headers.get('Host')
Expand Down Expand Up @@ -54,16 +52,3 @@ export function combineHeaders(
}
return combined
}

/**
* Singleton Server-Side Pattern.
*/
export function singleton<Value>(name: string, value: () => Value): Value {
// biome-ignore lint/suspicious/noExplicitAny: ...
const globalStore = global as any

globalStore.__singletons ??= {}
globalStore.__singletons[name] ??= value()

return globalStore.__singletons[name]
}
3 changes: 2 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"noSvgWithoutTitle": "off"
},
"style": {
"noParameterAssign": "off"
"noParameterAssign": "off",
"noNonNullAssertion": "info"
}
}
},
Expand Down
Binary file modified bun.lockb
Binary file not shown.
11 changes: 11 additions & 0 deletions functions/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { handle } from 'hono/aws-lambda'
import * as stripe from './stripe'

const app = new Hono().use(logger())

app.get('/', (ctx) => ctx.text('This works'))
app.route('/hook/stripe', stripe.route)

export const handler = handle(app)
55 changes: 22 additions & 33 deletions app/routes/api+/webhook.ts → functions/api/stripe.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { ActionFunctionArgs } from '@remix-run/node'
import { z } from 'zod'
import { stripe } from '#app/modules/stripe/stripe.server'
import { PLANS } from '#app/modules/stripe/plans'
Expand All @@ -10,34 +9,24 @@ import { ERRORS } from '#app/utils/constants/errors'
import { db, schema } from '#core/drizzle'
import { eq } from 'drizzle-orm'
import { Resource } from 'sst'
import { Hono } from 'hono'

export const ROUTE_PATH = '/api/webhook' as const
export const route = new Hono().post('/', async (ctx) => {
const sig = ctx.req.header('stripe-signature')

/**
* Gets and constructs a Stripe event signature.
*
* @throws An error if Stripe signature is missing or if event construction fails.
* @returns The Stripe event object.
*/
async function getStripeEvent(request: Request) {
try {
const signature = request.headers.get('Stripe-Signature')
if (!signature) throw new Error(ERRORS.STRIPE_MISSING_SIGNATURE)
const payload = await request.text()
const event = stripe.webhooks.constructEvent(
payload,
signature,
Resource.StripeWebhookEndpoint.value,
)
return event
} catch (err: unknown) {
console.log(err)
throw new Error(ERRORS.STRIPE_SOMETHING_WENT_WRONG)
}
}
if (!sig) throw new Error(ERRORS.STRIPE_MISSING_SIGNATURE)

console.log({
sig,
secret: Resource.StripeWebhook.secret,
id: Resource.StripeWebhook.id,
})

export async function action({ request }: ActionFunctionArgs) {
const event = await getStripeEvent(request)
const event = await stripe.webhooks.constructEventAsync(
await ctx.req.text(),
sig,
Resource.StripeWebhook.secret,
)

try {
switch (event.type) {
Expand Down Expand Up @@ -89,7 +78,7 @@ export async function action({ request }: ActionFunctionArgs) {
}
}

return new Response(null)
return ctx.json({})
}

/**
Expand Down Expand Up @@ -122,7 +111,7 @@ export async function action({ request }: ActionFunctionArgs) {
})
.where(eq(schema.subscription.userId, user.id))

return new Response(null)
return ctx.json({})
}

/**
Expand All @@ -140,7 +129,7 @@ export async function action({ request }: ActionFunctionArgs) {
.delete(schema.subscription)
.where(eq(schema.subscription.id, dbSubscription.id))

return new Response(null)
return ctx.json({})
}
}
} catch (err: unknown) {
Expand All @@ -158,7 +147,7 @@ export async function action({ request }: ActionFunctionArgs) {
if (!user) throw new Error(ERRORS.STRIPE_SOMETHING_WENT_WRONG)

await sendSubscriptionErrorEmail({ email: user.email, subscriptionId })
return new Response(null)
return ctx.json({})
}

case 'customer.subscription.updated': {
Expand All @@ -174,12 +163,12 @@ export async function action({ request }: ActionFunctionArgs) {
if (!user) throw new Error(ERRORS.STRIPE_SOMETHING_WENT_WRONG)

await sendSubscriptionErrorEmail({ email: user.email, subscriptionId })
return new Response(null)
return ctx.json({})
}
}

throw err
}

return new Response(null)
}
return ctx.json({})
})
17 changes: 17 additions & 0 deletions infra/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { domain } from './dns'
import { secret } from './secret'
import { webhook } from './stripe'

export const api = new sst.aws.Function('Api', {
url: true,
handler: 'functions/api/index.handler',
link: [secret.DATABASE_URL, secret.RESEND_API_KEY, secret.STRIPE_SECRET_KEY, webhook],
})

new sst.aws.Router('ApiRouter', {
domain: {
name: `api.${domain}`,
dns: sst.cloudflare.dns(),
},
routes: { '/*': api.url },
})
9 changes: 9 additions & 0 deletions infra/dns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const domain =
{
production: 'sivir.tech',
dev: 'dev.sivir.tech',
}[$app.stage] || `${$app.stage}.sivir.tech`

// export const zone = cloudflare.getZoneOutput({
// name: 'sivir.tech',
// })
1 change: 0 additions & 1 deletion infra/secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export const secret = {
RESEND_API_KEY: new sst.Secret('RESEND_API_KEY'),
STRIPE_PUBLIC_KEY: new sst.Secret('STRIPE_PUBLIC_KEY'),
STRIPE_SECRET_KEY: new sst.Secret('STRIPE_SECRET_KEY'),
StripeWebhookEndpoint: new sst.Secret('StripeWebhookEndpoint'),
// GitHubClientId: new sst.Secret('GitHubClientId'),
// GitHubClientSecret: new sst.Secret('GitHubClientSecret'),
HONEYPOT_ENCRYPTION_SEED: new sst.Secret('HONEYPOT_ENCRYPTION_SEED'),
Expand Down
1 change: 0 additions & 1 deletion infra/stage.ts

This file was deleted.

22 changes: 22 additions & 0 deletions infra/stripe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { domain } from './dns'

sst.Linkable.wrap(stripe.WebhookEndpoint, (endpoint) => {
return {
properties: {
id: endpoint.id,
secret: endpoint.secret,
},
}
})

export const webhook = new stripe.WebhookEndpoint('StripeWebhook', {
url: $interpolate`https://api.${domain}/hook/stripe`,
metadata: {
stage: $app.stage,
},
enabledEvents: [
'checkout.session.completed',
'customer.subscription.updated',
'customer.subscription.deleted',
],
})
14 changes: 9 additions & 5 deletions infra/www.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { domain } from './dns.ts'
import { secret } from './secret.ts'

export const www = new sst.aws.Remix('Remix', {
domain: {
name: domain,
dns: sst.cloudflare.dns(),
},
environment: {
NODE_ENV: $dev === true ? 'development' : 'production',
HOST_URL: $dev === true ? 'http://localhost:5173' : `https://${domain}`,
},
link: [
secret.SESSION_SECRET,
secret.ENCRYPTION_SECRET,
secret.DATABASE_URL,
secret.RESEND_API_KEY,
secret.STRIPE_PUBLIC_KEY,
secret.STRIPE_SECRET_KEY,
secret.StripeWebhookEndpoint,
secret.HONEYPOT_ENCRYPTION_SEED,
],
})

export const outputs = {
www: www.url,
}
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
},
"scripts": {
"build": "remix vite:build",
"db:migrate": "cross-env DB_MIGRATING=true bunx drizzle-kit generate",
"db:push": "bunx drizzle-kit push",
"db:seed": "cross-env DB_SEEDING=true bun run ./db/seed.ts",
"db:migrate": "sst shell cross-env DB_MIGRATING=true drizzle-kit generate",
"db:push": "sst shell bunx drizzle-kit push",
"db:seed": "sst shell cross-env DB_SEEDING=true bun run ./core/drizzle/seed.ts",
"dev": "remix vite:dev",
"format": "biome format --write .",
"format:check": "biome format --error-on-warnings .",
"lint": "biome lint --write .",
"start": "cross-env NODE_ENV=production node ./server.js",
"start": "sst shell remix vite:start",
"test": "vitest",
"typecheck": "tsc"
},
Expand All @@ -33,7 +33,6 @@
"@radix-ui/react-switch": "^1.0.3",
"@react-email/components": "^0.0.7",
"@react-email/render": "^0.0.15",
"@remix-run/express": "^2.9.1",
"@remix-run/node": "^2.9.2",
"@remix-run/react": "^2.9.1",
"@remix-run/router": "^1.16.0",
Expand All @@ -43,7 +42,7 @@
"cross-env": "^7.0.3",
"crypto-js": "^4.2.0",
"drizzle-orm": "^0.33.0",
"helmet": "^7.1.0",
"hono": "^4.6.3",
"i18next": "^23.11.3",
"i18next-browser-languagedetector": "^8.0.0",
"intl-parse-accept-language": "^1.0.0",
Expand All @@ -59,7 +58,7 @@
"remix-i18next": "^6.1.0",
"remix-utils": "^7.6.0",
"sonner": "^1.4.41",
"sst": "3.1.38",
"sst": "^3.2.38",
"stripe": "^15.5.0",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
Expand All @@ -70,6 +69,7 @@
"devDependencies": {
"@biomejs/biome": "^1.9.0",
"@remix-run/dev": "^2.9.2",
"@types/aws-lambda": "^8.10.145",
"@types/cookie": "^0.6.0",
"@types/react": "^18.3.2",
"@types/react-dom": "^18.3.0",
Expand Down
16 changes: 13 additions & 3 deletions sst-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ import "sst"
export {}
declare module "sst" {
export interface Resource {
"Api": {
"name": string
"type": "sst.aws.Function"
"url": string
}
"ApiRouter": {
"type": "sst.aws.Router"
"url": string
}
"DATABASE_URL": {
"type": "sst.sst.Secret"
"value": string
Expand Down Expand Up @@ -37,9 +46,10 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"StripeWebhookEndpoint": {
"type": "sst.sst.Secret"
"value": string
"StripeWebhook": {
"id": string
"secret": string
"type": "stripe.index/webhookEndpoint.WebhookEndpoint"
}
}
}
4 changes: 2 additions & 2 deletions sst.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/// <reference path="./.sst/platform/config.d.ts" />
import { readdirSync } from 'node:fs'

export default $config({
app(input) {
return {
Expand All @@ -9,9 +8,10 @@ export default $config({
home: 'aws',
providers: {
aws: {
version: '6.52.0',
region: 'eu-central-1',
},
'pulumi-stripe': true,
cloudflare: true,
},
}
},
Expand Down

0 comments on commit 7456a7f

Please sign in to comment.