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