correct all + stripe ok
This commit is contained in:
parent
b5a273ee2b
commit
fd05422059
@ -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
|
||||
23
Dockerfile
23
Dockerfile
@ -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"]
|
||||
@ -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"]
|
||||
@ -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
|
||||
@ -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:
|
||||
17
package.json
17
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"
|
||||
}
|
||||
}
|
||||
|
||||
116
server.mjs
Normal file
116
server.mjs
Normal 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)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user