feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
|
|
@ -7,10 +7,24 @@ import { requireUser, authErrorResponse, type AuthError } from '../_shared/auth.
|
|||
import { createServiceRoleClient } from '../_shared/quota.ts'
|
||||
|
||||
interface PortalRequest {
|
||||
return_url: string
|
||||
return_url?: unknown
|
||||
}
|
||||
|
||||
function validReturnUrl(value: unknown): string | null {
|
||||
if (typeof value !== 'string') return null
|
||||
try {
|
||||
const url = new URL(value)
|
||||
if (url.username || url.password) return null
|
||||
if (url.protocol === 'https:') return url.toString()
|
||||
if (url.protocol === 'http:' && ['localhost', '127.0.0.1'].includes(url.hostname)) {
|
||||
return url.toString()
|
||||
}
|
||||
return null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error — Deno 런타임 전역
|
||||
Deno.serve(async (req: Request) => {
|
||||
const preflight = handleCorsPreflightRequest(req)
|
||||
if (preflight) return preflight
|
||||
|
|
@ -25,8 +39,14 @@ Deno.serve(async (req: Request) => {
|
|||
try {
|
||||
const user = await requireUser(req)
|
||||
const body = (await req.json()) as PortalRequest
|
||||
const returnUrl = validReturnUrl(body.return_url)
|
||||
if (!returnUrl) {
|
||||
return new Response(JSON.stringify({ error: 'invalid_return_url' }), {
|
||||
status: 400,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
|
||||
// @ts-expect-error — Deno.env
|
||||
const stripeKey = Deno.env.get('STRIPE_SECRET_KEY') ?? ''
|
||||
if (!stripeKey) {
|
||||
return new Response(
|
||||
|
|
@ -44,7 +64,7 @@ Deno.serve(async (req: Request) => {
|
|||
.maybeSingle()
|
||||
|
||||
const customerId = (sub?.stripe_customer_id as string | null | undefined) ?? null
|
||||
if (!customerId) {
|
||||
if (!customerId?.startsWith('cus_')) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'no_customer',
|
||||
|
|
@ -63,16 +83,20 @@ Deno.serve(async (req: Request) => {
|
|||
},
|
||||
body: new URLSearchParams({
|
||||
customer: customerId,
|
||||
return_url: body.return_url
|
||||
return_url: returnUrl
|
||||
})
|
||||
})
|
||||
|
||||
if (!portalResp.ok) {
|
||||
const errText = await portalResp.text()
|
||||
throw new Error(`Portal 세션 생성 실패: ${errText}`)
|
||||
throw new Error('stripe_portal_creation_failed')
|
||||
}
|
||||
|
||||
const data = (await portalResp.json()) as { url: string }
|
||||
const data = (await portalResp.json()) as { url?: unknown }
|
||||
if (typeof data.url !== 'string') throw new Error('stripe_portal_response_invalid')
|
||||
const portalUrl = new URL(data.url)
|
||||
if (portalUrl.protocol !== 'https:' || !portalUrl.hostname.endsWith('.stripe.com')) {
|
||||
throw new Error('stripe_portal_response_invalid')
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ url: data.url }), {
|
||||
status: 200,
|
||||
|
|
@ -82,9 +106,8 @@ Deno.serve(async (req: Request) => {
|
|||
if (err && typeof err === 'object' && 'status' in err && 'message' in err) {
|
||||
return authErrorResponse(err as AuthError, corsHeaders)
|
||||
}
|
||||
const message = err instanceof Error ? err.message : 'Unknown error'
|
||||
return new Response(JSON.stringify({ error: message }), {
|
||||
status: 500,
|
||||
return new Response(JSON.stringify({ error: 'stripe_portal_failed' }), {
|
||||
status: 502,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue