From fd05422059553072f7c888a437c6b37c5f3e2da5 Mon Sep 17 00:00:00 2001 From: ordinarthur Date: Wed, 25 Feb 2026 15:26:18 +0100 Subject: [PATCH] correct all + stripe ok --- .dockerignore | 25 --------- Dockerfile | 23 -------- Dockerfile.dev | 13 ----- docker-compose.dev.yml | 21 -------- docker-compose.yml | 49 ----------------- package.json | 17 ++---- server.mjs | 116 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 121 insertions(+), 143 deletions(-) delete mode 100644 .dockerignore delete mode 100644 Dockerfile delete mode 100644 Dockerfile.dev delete mode 100644 docker-compose.dev.yml delete mode 100644 docker-compose.yml create mode 100644 server.mjs diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 915f7e1..0000000 --- a/.dockerignore +++ /dev/null @@ -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 diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 58304cb..0000000 --- a/Dockerfile +++ /dev/null @@ -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"] diff --git a/Dockerfile.dev b/Dockerfile.dev deleted file mode 100644 index c2f754e..0000000 --- a/Dockerfile.dev +++ /dev/null @@ -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"] diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml deleted file mode 100644 index 6420bd4..0000000 --- a/docker-compose.dev.yml +++ /dev/null @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index e487eeb..0000000 --- a/docker-compose.yml +++ /dev/null @@ -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: diff --git a/package.json b/package.json index 26085fd..4617c7d 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/server.mjs b/server.mjs new file mode 100644 index 0000000..0b41ed4 --- /dev/null +++ b/server.mjs @@ -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( + `\n\n ${DOMAIN}/${today}weekly1.0\n` + ) +}) + +// ── 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) +}