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
|
||||
* Deploy as a Cloudflare Worker, Vercel function, or standalone server.
|
||||
* Deploy as standalone Node.js server.
|
||||
*
|
||||
* Environment variables needed:
|
||||
* - 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": "..." }
|
||||
* Environment: STRIPE_SECRET_KEY, FRONTEND_URL
|
||||
*/
|
||||
|
||||
const stripeEndpoint = 'https://api.stripe.com/v1/checkout/sessions'
|
||||
const http = require('http')
|
||||
const https = require('https')
|
||||
|
||||
async function handleRequest(request) {
|
||||
// CORS for frontend
|
||||
if (request.method === 'OPTIONS') {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
},
|
||||
})
|
||||
}
|
||||
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY
|
||||
const FRONTEND_URL = process.env.FRONTEND_URL || 'https://git.fdsa.agency'
|
||||
const PORT = process.env.PORT || 8787
|
||||
|
||||
if (request.method !== 'POST') {
|
||||
return new Response(JSON.stringify({ error: 'Method not allowed' }), { status: 405 })
|
||||
}
|
||||
|
||||
const stripeKey = globalThis.STRIPE_SECRET_KEY || process?.env?.STRIPE_SECRET_KEY
|
||||
const frontendUrl = globalThis.FRONTEND_URL || process?.env?.FRONTEND_URL || 'https://git.fdsa.agency'
|
||||
|
||||
if (!stripeKey) {
|
||||
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,
|
||||
function createCheckoutSession(priceId, mode, email) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const data = new URLSearchParams({
|
||||
mode: mode || 'payment',
|
||||
success_url: `${FRONTEND_URL}/checkout/success`,
|
||||
cancel_url: `${FRONTEND_URL}/checkout/cancel`,
|
||||
allow_promotion_codes: 'true',
|
||||
'line_items[0][price]': priceId,
|
||||
'line_items[0][quantity]': '1',
|
||||
})
|
||||
if (email) data.set('customer_email', email)
|
||||
|
||||
if (customer_email) {
|
||||
body.set('customer_email', customer_email)
|
||||
}
|
||||
|
||||
const stripeResp = await fetch(stripeEndpoint, {
|
||||
const options = {
|
||||
hostname: 'api.stripe.com',
|
||||
path: '/v1/checkout/sessions',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${stripeKey}`,
|
||||
'Authorization': `Bearer ${STRIPE_SECRET_KEY}`,
|
||||
'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 }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
})
|
||||
} 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 req = https.request(options, (res) => {
|
||||
let body = ''
|
||||
res.on('data', chunk => body += chunk)
|
||||
res.on('end', () => {
|
||||
try { resolve({ status: res.statusCode, body: JSON.parse(body) }) }
|
||||
catch { resolve({ status: res.statusCode, body: { error: { message: body } } }) }
|
||||
})
|
||||
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 PRICES = {
|
||||
'self-paced': { price_id: 'price_self_paced', name: 'Self-Paced', amount: 9700 },
|
||||
'enterprise': { price_id: 'price_enterprise', name: 'Enterprise', amount: 19900 },
|
||||
'cohort': { price_id: 'price_cohort', name: 'Cohort', amount: 24700 },
|
||||
'self-paced': { price_id: 'price_1TeZdgQGFaSjzUrJE4aoSlPb', name: 'Self-Paced', amount: 9700 },
|
||||
'enterprise': { price_id: 'price_1TeZdhQGFaSjzUrJxVhrpvQj', name: 'Enterprise', amount: 19900 },
|
||||
'cohort': { price_id: 'price_1TeZdiQGFaSjzUrJpoX01VPn', name: 'Cohort', amount: 24700 },
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue