Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions apps/supabase/src/edge-functions/stripe-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,112 @@ const MGMT_API_BASE = MGMT_API_BASE_RAW.match(/^https?:\/\//)
? MGMT_API_BASE_RAW
: `https://${MGMT_API_BASE_RAW}`

// Returns true if the projectRef passed is a non-branch project, false otherwise
async function canAccessProject(projectRef: string, accessToken: string): Promise<boolean> {
const url = `${MGMT_API_BASE}/v1/projects/${projectRef}`
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
})
return response.ok
}

// Returns all projects refs
async function getAllProjects(accessToken: string): Promise<string[]> {
const url = `${MGMT_API_BASE}/v1/projects`
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
})
if (response.ok) {
const projects = (await response.json()) as Array<{ ref?: string }>
return Array.isArray(projects)
? projects
.map((project) => project.ref)
.filter((projectRef): projectRef is string => Boolean(projectRef))
: []
} else {
return []
}
}

// Returns all branch project refs of a give projectRef
async function getAllBranches(projectRef: string, accessToken: string): Promise<string[]> {
const url = `${MGMT_API_BASE}/v1/projects/${projectRef}/branches`
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
})
if (response.ok) {
const branches = (await response.json()) as Array<{ project_ref?: string }>
return Array.isArray(branches)
? branches
.map((branch) => branch.project_ref)
.filter((projectRef): projectRef is string => Boolean(projectRef))
: []
} else {
return []
}
}

// Authenticates with Supabase management API. Returns null if the accessToken has
// access to the projectRef project, other returns an error string
async function authenticateWithSupabase(
projectRef: string,
accessToken: string | null
): Promise<string | null> {
if (!accessToken) {
return 'Unauthorized: Invalid credentials'
}

// First we check if the token can return the project directly
// If it does then the token is valid for this project and
// we authenticate the request
const tokenCanAccesProject = await canAccessProject(projectRef, accessToken)
if (tokenCanAccesProject) {
return null
}

// If the token does not return a project then projectRef could be a branch
// project, in which case we enumerate branch projects of all projects
const allProjectRefs = await getAllProjects(accessToken)
for (const ref of allProjectRefs) {
const branches = await getAllBranches(ref, accessToken)
if (branches.includes(projectRef)) {
return null
}
}

// It's not even a branch project
return 'Unauthorized: Invalid credentials'
}

// Helper to validate accessToken against Management API
async function validateAccessToken(projectRef: string, accessToken: string): Promise<boolean> {
// Try to fetch project details using the access token
// This validates that the token is valid for the management API
const url = `${MGMT_API_BASE}/v1/projects/${projectRef}`
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
})

// If we can successfully get the project, the token is valid
return response.ok
}

const jsonResponse = (body: unknown, status = 200) =>
new Response(JSON.stringify(body), {
status,
Expand Down Expand Up @@ -162,6 +268,11 @@ async function handleSetupPost(req: Request): Promise<Response> {
if (ctx instanceof Response) return ctx
const { supabaseUrl, projectRef, accessToken } = ctx

const supabaseAuthError = await authenticateWithSupabase(projectRef, accessToken)
if (supabaseAuthError) {
return new Response(supabaseAuthError, { status: 401 })
}

let pool: pg.Pool | null = null
try {
const dbUrl = Deno.env.get('SUPABASE_DB_URL')
Expand Down Expand Up @@ -275,6 +386,15 @@ async function handleSetupPost(req: Request): Promise<Response> {
// ---------------------------------------------------------------------------

async function handleSetupGet(_req: Request): Promise<Response> {
const ctx = extractAuthContext(req)
if (ctx instanceof Response) return ctx
const { projectRef, accessToken } = ctx

const supabaseAuthError = await authenticateWithSupabase(projectRef, accessToken)
if (supabaseAuthError) {
return new Response(supabaseAuthError, { status: 401 })
}

const dbUrl = Deno.env.get('SUPABASE_DB_URL')
if (!dbUrl) {
return jsonResponse({ error: 'SUPABASE_DB_URL not set' }, 500)
Expand Down Expand Up @@ -353,6 +473,11 @@ async function handleSetupDelete(req: Request): Promise<Response> {
if (ctx instanceof Response) return ctx
const { projectRef, accessToken } = ctx

const isValid = await validateAccessToken(projectRef, accessToken)
if (!isValid) {
return new Response('Forbidden: Invalid access token for this project', { status: 403 })
}

let pool: pg.Pool | null = null
try {
const dbUrl = Deno.env.get('SUPABASE_DB_URL')
Expand Down
Loading