fix: live Stripe keys + prices, fixed checkout API Node server
This commit is contained in:
parent
7ad7cb6458
commit
ea8d6c7e4d
199
api/checkout.js
199
api/checkout.js
|
|
@ -1,141 +1,98 @@
|
||||||
/**
|
/**
|
||||||
* Stripe Checkout Session API
|
* Stripe Checkout Session API
|
||||||
* Deploy as a Cloudflare Worker, Vercel function, or standalone server.
|
* Deploy as standalone Node.js server.
|
||||||
*
|
*
|
||||||
* Environment variables needed:
|
* Environment: STRIPE_SECRET_KEY, FRONTEND_URL
|
||||||
* - STRIPE_SECRET_KEY: sk_live_... or sk_test_...
|
|
||||||
* - FRONTEND_URL: https://git.fdsa.agency (or your domain)
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* POST /api/checkout
|
|
||||||
* Body: { "price_id": "price_abc123", "mode": "payment", "customer_email": "..." }
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const stripeEndpoint = 'https://api.stripe.com/v1/checkout/sessions'
|
const http = require('http')
|
||||||
|
const https = require('https')
|
||||||
|
|
||||||
async function handleRequest(request) {
|
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY
|
||||||
// CORS for frontend
|
const FRONTEND_URL = process.env.FRONTEND_URL || 'https://git.fdsa.agency'
|
||||||
if (request.method === 'OPTIONS') {
|
const PORT = process.env.PORT || 8787
|
||||||
return new Response(null, {
|
|
||||||
headers: {
|
|
||||||
'Access-Control-Allow-Origin': '*',
|
|
||||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
||||||
'Access-Control-Allow-Headers': 'Content-Type',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.method !== 'POST') {
|
function createCheckoutSession(priceId, mode, email) {
|
||||||
return new Response(JSON.stringify({ error: 'Method not allowed' }), { status: 405 })
|
return new Promise((resolve, reject) => {
|
||||||
}
|
const data = new URLSearchParams({
|
||||||
|
mode: mode || 'payment',
|
||||||
const stripeKey = globalThis.STRIPE_SECRET_KEY || process?.env?.STRIPE_SECRET_KEY
|
success_url: `${FRONTEND_URL}/checkout/success`,
|
||||||
const frontendUrl = globalThis.FRONTEND_URL || process?.env?.FRONTEND_URL || 'https://git.fdsa.agency'
|
cancel_url: `${FRONTEND_URL}/checkout/cancel`,
|
||||||
|
allow_promotion_codes: 'true',
|
||||||
if (!stripeKey) {
|
'line_items[0][price]': priceId,
|
||||||
return new Response(JSON.stringify({ error: 'Stripe not configured' }), {
|
|
||||||
status: 500,
|
|
||||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { price_id, mode, customer_email } = await request.json()
|
|
||||||
|
|
||||||
const body = new URLSearchParams({
|
|
||||||
'mode': mode || 'payment',
|
|
||||||
'success_url': `${frontendUrl}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
|
|
||||||
'cancel_url': `${frontendUrl}/checkout/cancel`,
|
|
||||||
'allow_promotion_codes': 'true',
|
|
||||||
'line_items[0][price]': price_id,
|
|
||||||
'line_items[0][quantity]': '1',
|
'line_items[0][quantity]': '1',
|
||||||
})
|
})
|
||||||
|
if (email) data.set('customer_email', email)
|
||||||
|
|
||||||
if (customer_email) {
|
const options = {
|
||||||
body.set('customer_email', customer_email)
|
hostname: 'api.stripe.com',
|
||||||
}
|
path: '/v1/checkout/sessions',
|
||||||
|
|
||||||
const stripeResp = await fetch(stripeEndpoint, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${stripeKey}`,
|
'Authorization': `Bearer ${STRIPE_SECRET_KEY}`,
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Content-Length': data.toString().length,
|
||||||
},
|
},
|
||||||
body: body.toString(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = await stripeJson(stripeResp)
|
|
||||||
|
|
||||||
if (!stripeResp.ok) {
|
|
||||||
throw new Error(data.error?.message || 'Stripe error')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response(JSON.stringify({ url: data.url, session_id: data.id }), {
|
const req = https.request(options, (res) => {
|
||||||
status: 200,
|
let body = ''
|
||||||
headers: {
|
res.on('data', chunk => body += chunk)
|
||||||
'Content-Type': 'application/json',
|
res.on('end', () => {
|
||||||
'Access-Control-Allow-Origin': '*',
|
try { resolve({ status: res.statusCode, body: JSON.parse(body) }) }
|
||||||
},
|
catch { resolve({ status: res.statusCode, body: { error: { message: body } } }) }
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
return new Response(JSON.stringify({ error: e.message }), {
|
|
||||||
status: 500,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Access-Control-Allow-Origin': '*',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function stripeJson(response) {
|
|
||||||
const text = await response.text()
|
|
||||||
try {
|
|
||||||
return JSON.parse(text)
|
|
||||||
} catch {
|
|
||||||
// Stripe sometimes returns unexpected formats — parse query string
|
|
||||||
const params = new URLSearchParams(text)
|
|
||||||
const obj = {}
|
|
||||||
for (const [k, v] of params) {
|
|
||||||
// Handle nested keys like 'error.message'
|
|
||||||
const parts = k.split('.')
|
|
||||||
if (parts.length > 1) {
|
|
||||||
let cursor = obj
|
|
||||||
for (let i = 0; i < parts.length - 1; i++) {
|
|
||||||
cursor[parts[i]] = cursor[parts[i]] || {}
|
|
||||||
cursor = cursor[parts[i]]
|
|
||||||
}
|
|
||||||
cursor[parts[parts.length - 1]] = v
|
|
||||||
} else {
|
|
||||||
obj[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cloudflare Worker entry
|
|
||||||
export default { fetch: handleRequest }
|
|
||||||
|
|
||||||
// Node.js entry
|
|
||||||
if (typeof module !== 'undefined' && require?.main === module) {
|
|
||||||
const http = require('http')
|
|
||||||
const server = http.createServer((req, res) => {
|
|
||||||
// Minimal Node.js adapter — assumes body is JSON
|
|
||||||
let body = ''
|
|
||||||
req.on('data', chunk => body += chunk)
|
|
||||||
req.on('end', async () => {
|
|
||||||
globalThis.process = process
|
|
||||||
globalThis.request = new Request(`http://localhost${req.url}`, {
|
|
||||||
method: req.method,
|
|
||||||
headers: req.headers,
|
|
||||||
body: body || undefined,
|
|
||||||
})
|
})
|
||||||
const response = await handleRequest(globalThis.request)
|
|
||||||
res.writeHead(response.status, Object.fromEntries(response.headers))
|
|
||||||
res.end(await response.text())
|
|
||||||
})
|
})
|
||||||
|
req.on('error', reject)
|
||||||
|
req.write(data.toString())
|
||||||
|
req.end()
|
||||||
})
|
})
|
||||||
const port = process.env.PORT || 8787
|
|
||||||
server.listen(port, () => console.log(`Checkout API on :${port}`))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const server = http.createServer(async (req, res) => {
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||||
|
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
|
||||||
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
|
||||||
|
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
res.writeHead(204)
|
||||||
|
res.end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method !== 'POST') {
|
||||||
|
res.writeHead(405, { 'Content-Type': 'application/json' })
|
||||||
|
res.end(JSON.stringify({ error: 'Method not allowed' }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!STRIPE_SECRET_KEY) {
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' })
|
||||||
|
res.end(JSON.stringify({ error: 'Stripe not configured' }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = ''
|
||||||
|
req.on('data', chunk => body += chunk)
|
||||||
|
req.on('end', async () => {
|
||||||
|
try {
|
||||||
|
const { price_id, mode, customer_email } = JSON.parse(body)
|
||||||
|
const result = await createCheckoutSession(price_id, mode, customer_email)
|
||||||
|
|
||||||
|
if (result.status === 200) {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' })
|
||||||
|
res.end(JSON.stringify({ url: result.body.url, session_id: result.body.id }))
|
||||||
|
} else {
|
||||||
|
res.writeHead(result.status, { 'Content-Type': 'application/json' })
|
||||||
|
res.end(JSON.stringify({ error: result.body.error?.message || 'Stripe error' }))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
res.writeHead(400, { 'Content-Type': 'application/json' })
|
||||||
|
res.end(JSON.stringify({ error: e.message }))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
server.listen(PORT, () => {
|
||||||
|
console.log(`Checkout API running on :${PORT}`)
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,9 @@ const { site } = useData()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const PRICES = {
|
const PRICES = {
|
||||||
'self-paced': { price_id: 'price_self_paced', name: 'Self-Paced', amount: 9700 },
|
'self-paced': { price_id: 'price_1TeZdgQGFaSjzUrJE4aoSlPb', name: 'Self-Paced', amount: 9700 },
|
||||||
'enterprise': { price_id: 'price_enterprise', name: 'Enterprise', amount: 19900 },
|
'enterprise': { price_id: 'price_1TeZdhQGFaSjzUrJxVhrpvQj', name: 'Enterprise', amount: 19900 },
|
||||||
'cohort': { price_id: 'price_cohort', name: 'Cohort', amount: 24700 },
|
'cohort': { price_id: 'price_1TeZdiQGFaSjzUrJpoX01VPn', name: 'Cohort', amount: 24700 },
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue