rebours/server.mjs
ordinarthur 1b53e04b5d feat: switch to SSR for live Sanity updates
Migrate from SSG to SSR with @astrojs/node adapter so Sanity CMS
changes are reflected immediately without rebuild. Separate ports
for Astro SSR (4321) and Fastify API (3000) in production.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 11:41:34 +02:00

139 lines
5.5 KiB
JavaScript

import Fastify from 'fastify'
import cors from '@fastify/cors'
import Stripe from 'stripe'
import { createClient } from '@sanity/client'
import dotenv from 'dotenv'
dotenv.config()
// ── Sanity client ──────────────────────────────────────────────────────────
const sanity = createClient({
projectId: process.env.SANITY_PROJECT_ID,
dataset: process.env.SANITY_DATASET || 'production',
apiVersion: '2024-01-01',
useCdn: false, // Server-side: always fresh data
token: process.env.SANITY_API_TOKEN, // Optional: for authenticated reads
})
// ── Stripe ─────────────────────────────────────────────────────────────────
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY ?? '')
const DOMAIN = process.env.DOMAIN ?? 'http://localhost:4321'
// ── Fastify ────────────────────────────────────────────────────────────────
const app = Fastify({ logger: true, trustProxy: true })
await app.register(cors, { origin: '*', methods: ['GET', 'POST'] })
// ── Webhook Stripe ─────────────────────────────────────────────────────────
app.post('/api/webhook', {
config: { rawBody: true },
onRequest: (request, reply, done) => {
request.rawBody = ''
request.req.on('data', chunk => { request.rawBody += chunk })
request.req.on('end', done)
},
}, async (request, reply) => {
const sig = request.headers['stripe-signature']
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET
if (!sig || !webhookSecret) return reply.code(400).send('Missing signature')
let event
try {
event = stripe.webhooks.constructEvent(request.rawBody, sig, webhookSecret)
} catch {
return reply.code(400).send('Webhook Error')
}
if (event.type === 'checkout.session.completed') {
const session = event.data.object
if (session.payment_status === 'paid') {
app.log.info(
`✓ Paiement confirmé — ${session.id}${session.customer_details?.email}${session.metadata?.product}`
)
}
}
return { received: true }
})
// ── Health check ────────────────────────────────────────────────────────────
app.get('/api/health', async () => ({ status: 'ok' }))
// ── Checkout Stripe ─────────────────────────────────────────────────────────
app.post('/api/checkout', async (request, reply) => {
const { product: slug, email } = request.body ?? {}
if (!slug) return reply.code(400).send({ error: 'Produit manquant' })
// Fetch product from Sanity
const product = await sanity.fetch(
`*[_type == "product" && slug.current == $slug && isPublished == true][0]{
productDisplayName,
description,
price,
currency,
"imageUrl": image.asset->url,
"slug": slug.current
}`,
{ slug }
)
if (!product || !product.price) {
return reply.code(404).send({ error: 'Produit non disponible' })
}
try {
const session = await stripe.checkout.sessions.create({
mode: 'payment',
payment_method_types: ['card', 'link'],
line_items: [{
price_data: {
currency: (product.currency || 'EUR').toLowerCase(),
product_data: {
name: product.productDisplayName,
description: product.description?.substring(0, 500) || undefined,
images: product.imageUrl ? [product.imageUrl] : [],
},
unit_amount: product.price,
},
quantity: 1,
}],
metadata: { product: slug },
success_url: `${DOMAIN}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${DOMAIN}/#collection`,
locale: 'fr',
customer_email: email ?? undefined,
custom_text: {
submit: { message: 'Pièce unique — fabriquée à Paris. Délai : 6 à 8 semaines.' },
},
})
return { url: session.url }
} catch (err) {
app.log.error(err)
return reply.code(500).send({ error: err.message })
}
})
// ── Vérification session ────────────────────────────────────────────────────
app.get('/api/session/:id', async (request) => {
const session = await stripe.checkout.sessions.retrieve(request.params.id, {
expand: ['payment_intent.latest_charge'],
})
const charge = session.payment_intent?.latest_charge
return {
status: session.payment_status,
amount: session.amount_total,
currency: session.currency,
customer_email: session.customer_details?.email ?? null,
product: session.metadata?.product ?? null,
receipt_url: charge?.receipt_url ?? null,
}
})
// ── Start ───────────────────────────────────────────────────────────────────
try {
await app.listen({ port: process.env.FASTIFY_PORT ?? process.env.PORT ?? 3000, host: '0.0.0.0' })
} catch (err) {
app.log.error(err)
process.exit(1)
}