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)
}