Files
supabase/apps/studio/pages/api/integrations/stripe-sync.ts
T
0e2c31bf60 feat: Stripe Sync Engine integration (#41317)
* add initial installation flow of stripe sync engine

* update docs link

* Add supabase_vault extension dep

* Add stripe logo to sync engine integration

* Move overview content to bottom of integration pages

* Add sync state to stripe sync page

* only check sync state if stripe integration is installed

* Use proper stripe-sync package and setup flows

* Improve sync engine installation ux

* Remove unused hardcoded dep

* Add alpha status to stripe sync engine integration

* fix typo

* run format

* fix types

* Rename the stripe-sync path to remove the 'integration". The path needs to have BASE_PATH to work on prod.

* Design tidy up (#41337)

UI tidy up

* update to latest sync engine package

* Add stripe key verification

* Remove noop try/catch

* Add integration isntallation telelemtry

* Add basic settings page

* Address coderabbit comments

* remove unused dep

* Remove state setting on render

* s/description/comment

* Cleanup settings screen UI

* Improve settings screen design

* update schema test snapshot

* Use latest stripe-sync-engine package

* Update repo url to new official location

* revert marketing change

* Update stripe sync engine package

* Add link to table from overview page

* Add feature flag and improve telemetry

* Fix missing useMemo dep

* add uninstall telemetry note

---------

Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
Co-authored-by: Saxon Fletcher <saxonafletcher@gmail.com>
2025-12-20 00:42:10 +00:00

142 lines
4.3 KiB
TypeScript

import { NextApiRequest, NextApiResponse } from 'next'
import { z } from 'zod'
import { install, uninstall } from 'stripe-experiment-sync/supabase'
import { VERSION } from 'stripe-experiment-sync'
import { waitUntil } from '@vercel/functions'
const ENABLE_FLAG_KEY = 'enableStripeSyncEngineIntegration'
const InstallBodySchema = z.object({
projectRef: z.string().min(1),
stripeSecretKey: z.string().min(1),
})
const UninstallBodySchema = z.object({
projectRef: z.string().min(1),
})
async function isStripeSyncEnabled() {
// The ConfigClient doesn't seem to work properly so we'll just gate access from the frontend
// for now
return true
}
function getBearerToken(req: NextApiRequest) {
const authHeader = req.headers.authorization
if (!authHeader || Array.isArray(authHeader)) return null
const match = authHeader.match(/^Bearer\s+(.+)$/i)
return match?.[1]?.trim() ?? null
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// Hide endpoint if the integration is disabled.
if (!(await isStripeSyncEnabled())) {
return res.status(404).json({ data: null, error: { message: 'Not Found' } })
}
const { method } = req
switch (method) {
case 'POST':
return handleSetupStripeSyncInstall(req, res)
case 'DELETE':
return handleDeleteStripeSyncInstall(req, res)
default:
return res
.status(405)
.json({ data: null, error: { message: `Method ${method} Not Allowed` } })
}
}
async function handleDeleteStripeSyncInstall(req: NextApiRequest, res: NextApiResponse) {
const supabaseToken = getBearerToken(req)
if (!supabaseToken) {
return res
.status(401)
.json({ data: null, error: { message: 'Unauthorized: Invalid Authorization header' } })
}
const parsed = UninstallBodySchema.safeParse(req.body)
if (!parsed.success) {
return res
.status(400)
.json({ data: null, error: { message: 'Bad Request: Invalid request body' } })
}
const { projectRef } = parsed.data
waitUntil(
uninstall({
supabaseAccessToken: supabaseToken,
supabaseProjectRef: projectRef,
baseProjectUrl: process.env.NEXT_PUBLIC_CUSTOMER_DOMAIN,
supabaseManagementUrl: process.env.NEXT_PUBLIC_API_DOMAIN,
}).catch((error) => {
console.error('Stripe Sync Engine uninstallation failed.', error)
throw error
})
)
return res
.status(200)
.json({ data: { message: 'Stripe Sync uninstallation initiated' }, error: null })
}
async function handleSetupStripeSyncInstall(req: NextApiRequest, res: NextApiResponse) {
const supabaseToken = getBearerToken(req)
if (!supabaseToken) {
return res
.status(401)
.json({ data: null, error: { message: 'Unauthorized: Invalid Authorization header' } })
}
const parsed = InstallBodySchema.safeParse(req.body)
if (!parsed.success) {
return res
.status(400)
.json({ data: null, error: { message: 'Bad Request: Invalid request body' } })
}
const { projectRef, stripeSecretKey } = parsed.data
// Validate the Stripe API key before proceeding with installation
try {
const stripeResponse = await fetch('https://api.stripe.com/v1/account', {
method: 'GET',
headers: {
Authorization: `Bearer ${stripeSecretKey}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
})
if (!stripeResponse.ok) {
const errorData = await stripeResponse.json()
const errorMessage =
errorData.error?.message || `Invalid Stripe API key (HTTP ${stripeResponse.status})`
return res.status(400).json({
data: null,
error: { message: `Invalid Stripe API key: ${errorMessage}` },
})
}
} catch (error: any) {
return res.status(400).json({
data: null,
error: { message: `Failed to validate Stripe API key: ${error.message}` },
})
}
waitUntil(
install({
supabaseAccessToken: supabaseToken,
supabaseProjectRef: projectRef,
stripeKey: stripeSecretKey,
baseProjectUrl: process.env.NEXT_PUBLIC_CUSTOMER_DOMAIN,
supabaseManagementUrl: process.env.NEXT_PUBLIC_API_DOMAIN,
packageVersion: VERSION,
}).catch((error) => {
console.error('Stripe Sync Engine installation failed.', error)
throw error
})
)
return res
.status(200)
.json({ data: { message: 'Stripe Sync setup initiated', version: VERSION }, error: null })
}