correct all + stripe ok

This commit is contained in:
ordinarthur 2026-02-25 15:26:18 +01:00
parent b5a273ee2b
commit fd05422059
7 changed files with 121 additions and 143 deletions

View File

@ -1,25 +0,0 @@
# Git
.git
.gitignore
# Secrets — JAMAIS dans l'image
.env
.env.local
.env.production
# Dev
node_modules
*.log
.cursor
# Assets source bruts (seul public/ va dans l'image Docker)
assets/
# Infra (pas besoin dans le conteneur)
Dockerfile
docker-compose.yml
nginx.conf
ssl/
# Docs
README.md

View File

@ -1,23 +0,0 @@
# ─────────────────────────────────────────────────────────────────────────────
# REBOUR — Dockerfile (API uniquement)
# Le front est servi par nginx directement depuis public/
# ─────────────────────────────────────────────────────────────────────────────
FROM oven/bun:1.3-alpine AS deps
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile --production
FROM oven/bun:1.3-alpine AS runner
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY server.ts ./
USER bun
EXPOSE 3000
ENV NODE_ENV=production
HEALTHCHECK --interval=5s --timeout=3s --start-period=5s --retries=5 \
CMD wget -qO- http://localhost:3000/robots.txt || exit 1
CMD ["bun", "run", "server.ts"]

View File

@ -1,13 +0,0 @@
# ─────────────────────────────────────────────────────────────────────────────
# REBOUR — Dockerfile.dev (API avec hot reload)
# Le front est servi par nginx ou accédé directement via bun dev
# ─────────────────────────────────────────────────────────────────────────────
FROM oven/bun:1.3-alpine
WORKDIR /app
COPY package.json bun.lock* ./
RUN bun install
COPY server.ts ./
EXPOSE 3000
ENV NODE_ENV=development
CMD ["bun", "--watch", "server.ts"]

View File

@ -1,21 +0,0 @@
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
# Monte tout le projet pour le hot reload
- .:/app
# Évite d'écraser node_modules de l'image par le dossier local (s'il est vide)
- /app/node_modules
environment:
NODE_ENV: development
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY:-}
STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET:-}
DOMAIN: ${DOMAIN:-http://localhost:3000}
# Force bun --watch à utiliser le polling sur Docker Desktop Mac
# (les événements inotify ne sont pas propagés depuis macOS)
CHOKIDAR_USEPOLLING: "true"
restart: unless-stopped

View File

@ -1,49 +0,0 @@
services:
# ── API Bun/Elysia — uniquement les routes /api/ ──────────────────────────
app:
build:
context: .
dockerfile: Dockerfile
target: runner
restart: unless-stopped
expose:
- "3000"
environment:
NODE_ENV: production
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY}
STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET}
DOMAIN: ${DOMAIN:-http://localhost}
networks:
- rebour-net
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:3000/robots.txt || exit 1"]
interval: 5s
timeout: 3s
retries: 5
start_period: 5s
# ── Nginx : sert public/ + proxifie /api/ vers app ────────────────────────
nginx:
image: nginx:1.27-alpine
restart: unless-stopped
ports:
- "0.0.0.0:80:80"
- "0.0.0.0:443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./public:/srv/public:ro
# En prod : décommenter + monter les certificats Let's Encrypt
# - /etc/letsencrypt:/etc/letsencrypt:ro
- nginx-logs:/var/log/nginx
depends_on:
- app
networks:
- rebour-net
networks:
rebour-net:
driver: bridge
volumes:
nginx-logs:

View File

@ -1,21 +1,14 @@
{
"name": "rebours",
"private": true,
"type": "module",
"scripts": {
"dev": "bun --watch server.ts",
"dev:docker": "docker compose -f docker-compose.dev.yml up --build",
"start": "bun run server.ts"
},
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
"dev": "node --watch server.mjs",
"start": "node server.mjs"
},
"dependencies": {
"@elysiajs/cors": "^1.4.1",
"@elysiajs/static": "^1.4.7",
"elysia": "^1.4.25",
"@fastify/cors": "^10.0.2",
"fastify": "^5.3.2",
"stripe": "^20.3.1"
}
}

116
server.mjs Normal file
View File

@ -0,0 +1,116 @@
import Fastify from 'fastify'
import cors from '@fastify/cors'
import Stripe from 'stripe'
import { readFileSync } from 'node:fs'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY ?? '')
const DOMAIN = process.env.DOMAIN ?? 'http://localhost:3000'
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',
},
}
const app = Fastify({ logger: true })
await app.register(cors, { origin: '*', methods: ['GET', 'POST'] })
// ── 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', (_, reply) => {
const today = new Date().toISOString().split('T')[0]
reply
.type('application/xml')
.header('Cache-Control', 'public, max-age=86400')
.send(
`<?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>`
)
})
// ── Checkout Stripe ───────────────────────────────────────────────────────────
app.post('/api/checkout', async (request, reply) => {
const { product, email } = request.body ?? {}
const p = PRODUCTS[product]
if (!p) return reply.code(404).send({ error: 'Produit inconnu' })
const session = await stripe.checkout.sessions.create({
mode: 'payment',
payment_method_types: ['card'],
line_items: [{
price_data: {
currency: p.currency,
unit_amount: p.amount,
product_data: { name: p.name, description: p.description },
},
quantity: 1,
}],
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 }
})
// ── Vérification session ──────────────────────────────────────────────────────
app.get('/api/session/:id', async (request) => {
const session = await stripe.checkout.sessions.retrieve(request.params.id)
return {
status: session.payment_status,
amount: session.amount_total,
currency: session.currency,
customer_email: session.customer_details?.email ?? null,
}
})
// ── 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 — ${session.id}${session.customer_details?.email}`)
}
}
return { received: true }
})
// ── Start ─────────────────────────────────────────────────────────────────────
try {
await app.listen({ port: 3000, host: '127.0.0.1' })
} catch (err) {
app.log.error(err)
process.exit(1)
}