import Fastify from 'fastify' import cors from '@fastify/cors' import fastifyStatic from '@fastify/static' import Stripe from 'stripe' import dotenv from 'dotenv' import path from 'path' import { setupAdmin } from './admin.mjs' import { prisma } from './src/lib/db.mjs' dotenv.config() const stripe = new Stripe(process.env.STRIPE_SECRET_KEY ?? '') const DOMAIN = process.env.DOMAIN ?? 'http://localhost:4321' const app = Fastify({ logger: true, trustProxy: true }) await app.register(cors, { origin: '*', methods: ['GET', 'POST'] }) // Serve uploaded images await app.register(fastifyStatic, { root: path.resolve('uploads'), prefix: '/uploads/', decorateReply: false, }) // ── Webhook Stripe (AVANT AdminJS pour éviter les conflits de body parsing) ─ 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 — ${session.id} — ${session.customer_details?.email}`) // Find product by stripeKey const stripeKey = session.metadata?.product const product = stripeKey ? await prisma.product.findFirst({ where: { stripeKey } }) : null await prisma.order.upsert({ where: { stripeSessionId: session.id }, create: { stripeSessionId: session.id, stripePaymentIntent: typeof session.payment_intent === 'string' ? session.payment_intent : null, status: 'paid', amount: session.amount_total ?? 0, currency: session.currency ?? 'eur', customerEmail: session.customer_details?.email ?? null, productId: product?.id ?? null, productSlug: product?.slug ?? stripeKey ?? null, metadata: session.metadata ?? null, }, update: { status: 'paid' }, }) } } return { received: true } }) // ── AdminJS ───────────────────────────────────────────────────────────────── await setupAdmin(app) // ── SEO ───────────────────────────────────────────────────────────────────── app.get('/robots.txt', (_, reply) => { reply .type('text/plain') .header('Cache-Control', 'public, max-age=86400') .send(`User-agent: *\nAllow: /\nSitemap: ${DOMAIN}/sitemap.xml\n`) }) app.get('/sitemap.xml', async (_, reply) => { const today = new Date().toISOString().split('T')[0] const products = await prisma.product.findMany({ where: { isPublished: true }, select: { slug: true }, }) const productUrls = products .map(p => ` ${DOMAIN}/collection/${p.slug}${today}weekly0.8`) .join('\n') reply .type('application/xml') .header('Cache-Control', 'public, max-age=86400') .send( `\n\n ${DOMAIN}/${today}weekly1.0\n${productUrls}\n` ) }) // ── Health check ──────────────────────────────────────────────────────────── app.get('/api/health', async () => ({ status: 'ok' })) // ── Checkout Stripe ───────────────────────────────────────────────────────── app.post('/api/checkout', async (request, reply) => { const { product, email } = request.body ?? {} // Lookup by stripeKey (compat frontend: "lumiere_orbitale") const p = await prisma.product.findFirst({ where: { stripeKey: product, isPublished: true }, select: { stripePriceId: true, stripeKey: true }, }) if (!p || !p.stripePriceId) return reply.code(404).send({ error: 'Produit inconnu' }) let session try { session = await stripe.checkout.sessions.create({ mode: 'payment', payment_method_types: ['card', 'link'], line_items: [{ price: p.stripePriceId, quantity: 1, }], metadata: { product }, 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.' }, }, }) } catch (err) { app.log.error(err) return reply.code(500).send({ error: err.message }) } return { url: session.url } }) // ── 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.PORT ?? 3000, host: '0.0.0.0' }) } catch (err) { app.log.error(err) process.exit(1) }