182 lines
6.9 KiB
TypeScript
182 lines
6.9 KiB
TypeScript
import { Elysia, t } from 'elysia'
|
|
import { cors } from '@elysiajs/cors'
|
|
import Stripe from 'stripe'
|
|
import { readFileSync, existsSync, statSync } from 'fs'
|
|
import { join, extname } from 'path'
|
|
|
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY ?? '', {
|
|
apiVersion: '2025-01-27.acacia',
|
|
})
|
|
|
|
const DOMAIN = process.env.DOMAIN ?? 'http://localhost:3000'
|
|
const PUBLIC_DIR = join(import.meta.dir, 'public')
|
|
|
|
const PRODUCTS = {
|
|
lumiere_orbitale: {
|
|
name: 'LUMIÈRE_ORBITALE — REBOUR',
|
|
description: 'Lampe de table unique. Béton texturé coulé à la main + dôme céramique laqué. Collection 001.',
|
|
amount: 180000,
|
|
currency: 'eur',
|
|
},
|
|
}
|
|
|
|
// Map extensions → MIME types
|
|
const MIME: Record<string, string> = {
|
|
'.html': 'text/html; charset=utf-8',
|
|
'.css': 'text/css',
|
|
'.js': 'application/javascript',
|
|
'.json': 'application/json',
|
|
'.jpg': 'image/jpeg',
|
|
'.jpeg': 'image/jpeg',
|
|
'.png': 'image/png',
|
|
'.webp': 'image/webp',
|
|
'.svg': 'image/svg+xml',
|
|
'.ico': 'image/x-icon',
|
|
'.woff2':'font/woff2',
|
|
'.txt': 'text/plain',
|
|
'.xml': 'application/xml',
|
|
}
|
|
|
|
const HTML_HEADERS = {
|
|
'Content-Type': 'text/html; charset=utf-8',
|
|
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
|
|
'X-Content-Type-Options': 'nosniff',
|
|
'X-Frame-Options': 'SAMEORIGIN',
|
|
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
|
}
|
|
|
|
// Sert un fichier statique depuis public/
|
|
function serveStatic(relativePath: string): Response {
|
|
const filePath = join(PUBLIC_DIR, relativePath)
|
|
if (!existsSync(filePath) || statSync(filePath).isDirectory()) {
|
|
return new Response('Not Found', { status: 404 })
|
|
}
|
|
const ext = extname(filePath).toLowerCase()
|
|
const mime = MIME[ext] ?? 'application/octet-stream'
|
|
const isAsset = ['.jpg', '.jpeg', '.png', '.webp', '.svg', '.ico', '.woff2', '.css', '.js'].includes(ext)
|
|
|
|
return new Response(Bun.file(filePath), {
|
|
headers: {
|
|
'Content-Type': mime,
|
|
'Cache-Control': isAsset
|
|
? 'public, max-age=31536000, immutable'
|
|
: 'public, max-age=3600',
|
|
},
|
|
})
|
|
}
|
|
|
|
const app = new Elysia()
|
|
.use(cors({ origin: '*', methods: ['GET', 'POST'] }))
|
|
|
|
// ── Pages HTML ─────────────────────────────────────────────────────────────
|
|
.get('/', () =>
|
|
new Response(readFileSync(join(PUBLIC_DIR, 'index.html'), 'utf-8'), { headers: HTML_HEADERS })
|
|
)
|
|
.get('/success', () =>
|
|
new Response(readFileSync(join(PUBLIC_DIR, 'success.html'), 'utf-8'), { headers: HTML_HEADERS })
|
|
)
|
|
|
|
// ── SEO : robots + sitemap ─────────────────────────────────────────────────
|
|
.get('/robots.txt', () =>
|
|
new Response(`User-agent: *\nAllow: /\nSitemap: ${DOMAIN}/sitemap.xml\n`, {
|
|
headers: { 'Content-Type': 'text/plain', 'Cache-Control': 'public, max-age=86400' },
|
|
})
|
|
)
|
|
.get('/sitemap.xml', () => {
|
|
const today = new Date().toISOString().split('T')[0]
|
|
return new Response(
|
|
`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n <url><loc>${DOMAIN}/</loc><lastmod>${today}</lastmod><changefreq>weekly</changefreq><priority>1.0</priority></url>\n</urlset>`,
|
|
{ headers: { 'Content-Type': 'application/xml', 'Cache-Control': 'public, max-age=86400' } }
|
|
)
|
|
})
|
|
|
|
// ── Fichiers statiques : /assets/*, /style.css, /main.js, etc. ────────────
|
|
.get('/assets/*', ({ params }) => serveStatic(`assets/${(params as any)['*']}`))
|
|
.get('/style.css', () => serveStatic('style.css'))
|
|
.get('/main.js', () => serveStatic('main.js'))
|
|
|
|
// ── API Stripe : créer session checkout ───────────────────────────────────
|
|
.post(
|
|
'/api/checkout',
|
|
async ({ body }) => {
|
|
const product = PRODUCTS[body.product as keyof typeof PRODUCTS]
|
|
if (!product) return new Response('Produit inconnu', { status: 404 })
|
|
|
|
const session = await stripe.checkout.sessions.create({
|
|
mode: 'payment',
|
|
payment_method_types: ['card'],
|
|
line_items: [{
|
|
price_data: {
|
|
currency: product.currency,
|
|
unit_amount: product.amount,
|
|
product_data: { name: product.name, description: product.description },
|
|
},
|
|
quantity: 1,
|
|
}],
|
|
success_url: `${DOMAIN}/success?session_id={CHECKOUT_SESSION_ID}`,
|
|
cancel_url: `${DOMAIN}/#collection`,
|
|
locale: 'fr',
|
|
customer_email: body.email ?? undefined,
|
|
custom_text: {
|
|
submit: { message: 'Pièce unique — fabriquée à Paris. Délai : 6 à 8 semaines.' },
|
|
},
|
|
})
|
|
|
|
return { url: session.url }
|
|
},
|
|
{
|
|
body: t.Object({
|
|
product: t.String(),
|
|
email: t.Optional(t.String()),
|
|
}),
|
|
}
|
|
)
|
|
|
|
// ── API : vérifier session après paiement ─────────────────────────────────
|
|
.get(
|
|
'/api/session/:id',
|
|
async ({ params }) => {
|
|
const session = await stripe.checkout.sessions.retrieve(params.id)
|
|
return {
|
|
status: session.payment_status,
|
|
amount: session.amount_total,
|
|
currency: session.currency,
|
|
customer_email: session.customer_details?.email ?? null,
|
|
}
|
|
},
|
|
{ params: t.Object({ id: t.String() }) }
|
|
)
|
|
|
|
// ── Webhook Stripe ─────────────────────────────────────────────────────────
|
|
.post('/api/webhook', async ({ request, headers }) => {
|
|
const sig = headers['stripe-signature']
|
|
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET
|
|
if (!sig || !webhookSecret) return new Response('Missing signature', { status: 400 })
|
|
|
|
let event: Stripe.Event
|
|
try {
|
|
event = stripe.webhooks.constructEvent(
|
|
Buffer.from(await request.arrayBuffer()), sig, webhookSecret
|
|
)
|
|
} catch {
|
|
return new Response('Webhook Error', { status: 400 })
|
|
}
|
|
|
|
if (event.type === 'checkout.session.completed') {
|
|
const session = event.data.object as Stripe.Checkout.Session
|
|
if (session.payment_status === 'paid') {
|
|
console.log(`✓ Paiement — ${session.id} — ${session.customer_details?.email}`)
|
|
}
|
|
}
|
|
return { received: true }
|
|
})
|
|
|
|
.listen(3000)
|
|
|
|
console.log(`
|
|
┌──────────────────────────────────────┐
|
|
│ REBOUR — http://localhost:3000 │
|
|
│ NODE_ENV: ${process.env.NODE_ENV ?? 'development'}
|
|
└──────────────────────────────────────┘
|
|
`)
|