You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
export async function initDB() {
const db = await open({ filename: DB_PATH, driver: sqlite3.Database });
await db.exec('PRAGMA foreign_keys = ON;');
await db.exec(CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT DEFAULT 'user', points INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ););
await db.exec(CREATE TABLE IF NOT EXISTS orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, meal TEXT NOT NULL, side TEXT NOT NULL, total REAL NOT NULL, status TEXT NOT NULL DEFAULT 'pending', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ););
await db.exec(CREATE TABLE IF NOT EXISTS order_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER NOT NULL, status TEXT NOT NULL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE ););
return db;
}import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import dotenv from 'dotenv';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
import { initDB } from './db.js';
import { createServer } from 'http';
import { Server as SocketIOServer } from 'socket.io';
import path from 'path';
import { fileURLToPath } from 'url';
// --- Socket.io ---
io.on('connection', (socket) => {
// Le client peut joindre la "room" de sa commande pour recevoir les updates ciblées
socket.on('order:join', (orderId) => {
socket.join(order:${orderId});
});
});
// Points fidélité : 1 point par $ dépensé (arrondi)
const addPoints = Math.round(total);
await db.run('UPDATE users SET points = points + ? WHERE id = ?', [addPoints, req.user.id]);
const order = await db.get('SELECT * FROM orders WHERE id = ?', [orderId]);
io.emit('order:created', { order }); // event global (ex: écran cuisine)
io.to(order:${orderId}).emit('order:update', { orderId, status: order.status });
// Récupérer une commande (propriétaire ou admin)
app.get('/api/orders/:id', auth(), async (req, res) => {
const order = await db.get('SELECT * FROM orders WHERE id = ?', [req.params.id]);
if (!order) return res.status(404).json({ error: 'Commande introuvable' });
if (req.user.role !== 'admin' && order.user_id !== req.user.id) {
return res.status(403).json({ error: 'Accès refusé' });
}
const history = await db.all('SELECT status, timestamp FROM order_events WHERE order_id = ? ORDER BY id ASC', [order.id]);
res.json({ order, history });
});
// Mettre à jour le statut (admin)
app.patch('/api/orders/:id/status', auth('admin'), async (req, res) => {
const { status } = req.body || {};
if (!VALID_STATUSES.includes(status)) return res.status(400).json({ error: 'Statut invalide' });
const order = await db.get('SELECT * FROM orders WHERE id = ?', [req.params.id]);
if (!order) return res.status(404).json({ error: 'Commande introuvable' });
await db.run('UPDATE orders SET status = ? WHERE id = ?', [status, order.id]);
await db.run('INSERT INTO order_events (order_id, status) VALUES (?,?)', [order.id, status]);
io.emit('order:board', { orderId: order.id, status }); // pour l’écran staff
io.to(order:${order.id}).emit('order:update', { orderId: order.id, status }); // pour le client
res.json({ ok: true, orderId: order.id, status });
});
// Fallback SPA simple: retourne index.html pour les routes inconnues côté client
app.get('*', (_, res) => {
res.sendFile(path.join(PUBLIC_DIR, 'index.html'));
});
{
"name": "brochette-ny-backend",
"version": "1.0.0",
"main": "server.js",
"type": "module",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"helmet": "^7.1.0",
"jsonwebtoken": "^9.0.2",
"socket.io": "^4.7.5",
"sqlite3": "^5.1.7"
}
}PORT=3000
JWT_SECRET=change_me_super_secret_please
ADMIN_EMAIL=admin@brochette.ny
ADMIN_PASSWORD=admin123import sqlite3 from 'sqlite3';
import { open } from 'sqlite';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const DB_PATH = path.join(__dirname, 'data.sqlite');
export async function initDB() {
const db = await open({ filename: DB_PATH, driver: sqlite3.Database });
await db.exec('PRAGMA foreign_keys = ON;');
await db.exec(
CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT DEFAULT 'user', points INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ););await db.exec(
CREATE TABLE IF NOT EXISTS orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, meal TEXT NOT NULL, side TEXT NOT NULL, total REAL NOT NULL, status TEXT NOT NULL DEFAULT 'pending', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ););await db.exec(
CREATE TABLE IF NOT EXISTS order_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER NOT NULL, status TEXT NOT NULL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE ););return db;
}import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import dotenv from 'dotenv';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
import { initDB } from './db.js';
import { createServer } from 'http';
import { Server as SocketIOServer } from 'socket.io';
import path from 'path';
import { fileURLToPath } from 'url';
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const PUBLIC_DIR = path.join(__dirname, '..', 'public');
const PORT = process.env.PORT || 3000;
const JWT_SECRET = process.env.JWT_SECRET || 'dev_secret';
const app = express();
const httpServer = createServer(app);
const io = new SocketIOServer(httpServer, {
cors: { origin: '*' }
});
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use(express.static(PUBLIC_DIR)); // sert /public (frontend)
const PRICE_MAP = {
'Brochette Poulet': 12,
'Brochette Boeuf' : 14,
'Dibi Poulet' : 13,
'Dibi Mouton' : 15,
'Frites' : 5,
'Djollof Rice' : 6,
'Smashed Potatoes': 6,
'Salade' : 4
};
const VALID_MEALS = ['Brochette Poulet','Brochette Boeuf','Dibi Poulet','Dibi Mouton'];
const VALID_SIDES = ['Frites','Djollof Rice','Smashed Potatoes','Salade'];
const VALID_STATUSES = ['pending','confirmed','preparing','ready','out_for_delivery','completed','cancelled'];
let db;
// --- Utils Auth ---
function signToken(user) {
return jwt.sign({ id: user.id, role: user.role }, JWT_SECRET, { expiresIn: '7d' });
}
function auth(requiredRole) {
return async (req, res, next) => {
const hdr = req.headers.authorization || '';
const token = hdr.startsWith('Bearer ') ? hdr.slice(7) : null;
if (!token) return res.status(401).json({ error: 'Token manquant' });
try {
const payload = jwt.verify(token, JWT_SECRET);
req.user = payload;
if (requiredRole && payload.role !== requiredRole) {
return res.status(403).json({ error: 'Accès refusé' });
}
next();
} catch {
return res.status(401).json({ error: 'Token invalide' });
}
};
}
// --- Socket.io ---
io.on('connection', (socket) => {
// Le client peut joindre la "room" de sa commande pour recevoir les updates ciblées
socket.on('order:join', (orderId) => {
socket.join(
order:${orderId});});
});
// --- Routes ---
app.get('/api/health', (_, res) => res.json({ ok: true }));
// Inscription
app.post('/api/auth/register', async (req, res) => {
const { name, email, password } = req.body || {};
if (!name || !email || !password) return res.status(400).json({ error: 'Champs requis' });
const hash = await bcrypt.hash(password, 10);
try {
const result = await db.run(
'INSERT INTO users (name, email, password_hash) VALUES (?,?,?)',
[name, email.toLowerCase(), hash]
);
const user = { id: result.lastID, name, email: email.toLowerCase(), role: 'user', points: 0 };
const token = signToken(user);
res.json({ token, user });
} catch (e) {
if (e.message.includes('UNIQUE')) return res.status(409).json({ error: 'Email déjà utilisé' });
res.status(500).json({ error: 'Erreur serveur' });
}
});
// Connexion
app.post('/api/auth/login', async (req, res) => {
const { email, password } = req.body || {};
if (!email || !password) return res.status(400).json({ error: 'Champs requis' });
const user = await db.get('SELECT * FROM users WHERE email = ?', [email.toLowerCase()]);
if (!user) return res.status(401).json({ error: 'Identifiants invalides' });
const ok = await bcrypt.compare(password, user.password_hash);
if (!ok) return res.status(401).json({ error: 'Identifiants invalides' });
const token = signToken(user);
res.json({ token, user: { id: user.id, name: user.name, email: user.email, role: user.role, points: user.points } });
});
// Profil & points
app.get('/api/me', auth(), async (req, res) => {
const me = await db.get('SELECT id, name, email, role, points FROM users WHERE id = ?', [req.user.id]);
res.json(me);
});
app.get('/api/loyalty', auth(), async (req, res) => {
const me = await db.get('SELECT points FROM users WHERE id = ?', [req.user.id]);
res.json({ points: me?.points ?? 0 });
});
// Créer une commande
app.post('/api/orders', auth(), async (req, res) => {
const { meal, side } = req.body || {};
if (!VALID_MEALS.includes(meal) || !VALID_SIDES.includes(side)) {
return res.status(400).json({ error: 'Articles invalides' });
}
const total = (PRICE_MAP[meal] ?? 0) + (PRICE_MAP[side] ?? 0);
const result = await db.run(
'INSERT INTO orders (user_id, meal, side, total, status) VALUES (?,?,?,?,?)',
[req.user.id, meal, side, total, 'pending']
);
const orderId = result.lastID;
await db.run('INSERT INTO order_events (order_id, status) VALUES (?,?)', [orderId, 'pending']);
// Points fidélité : 1 point par $ dépensé (arrondi)
const addPoints = Math.round(total);
await db.run('UPDATE users SET points = points + ? WHERE id = ?', [addPoints, req.user.id]);
const order = await db.get('SELECT * FROM orders WHERE id = ?', [orderId]);
io.emit('order:created', { order }); // event global (ex: écran cuisine)
io.to(
order:${orderId}).emit('order:update', { orderId, status: order.status });res.json({ orderId, status: order.status, total, points_added: addPoints });
});
// Récupérer une commande (propriétaire ou admin)
app.get('/api/orders/:id', auth(), async (req, res) => {
const order = await db.get('SELECT * FROM orders WHERE id = ?', [req.params.id]);
if (!order) return res.status(404).json({ error: 'Commande introuvable' });
if (req.user.role !== 'admin' && order.user_id !== req.user.id) {
return res.status(403).json({ error: 'Accès refusé' });
}
const history = await db.all('SELECT status, timestamp FROM order_events WHERE order_id = ? ORDER BY id ASC', [order.id]);
res.json({ order, history });
});
// Mettre à jour le statut (admin)
app.patch('/api/orders/:id/status', auth('admin'), async (req, res) => {
const { status } = req.body || {};
if (!VALID_STATUSES.includes(status)) return res.status(400).json({ error: 'Statut invalide' });
const order = await db.get('SELECT * FROM orders WHERE id = ?', [req.params.id]);
if (!order) return res.status(404).json({ error: 'Commande introuvable' });
await db.run('UPDATE orders SET status = ? WHERE id = ?', [status, order.id]);
await db.run('INSERT INTO order_events (order_id, status) VALUES (?,?)', [order.id, status]);
io.emit('order:board', { orderId: order.id, status }); // pour l’écran staff
io.to(
order:${order.id}).emit('order:update', { orderId: order.id, status }); // pour le clientres.json({ ok: true, orderId: order.id, status });
});
// Fallback SPA simple: retourne index.html pour les routes inconnues côté client
app.get('*', (_, res) => {
res.sendFile(path.join(PUBLIC_DIR, 'index.html'));
});
// --- Boot ---
(async () => {
db = await initDB();
// Crée un admin par défaut si nécessaire
const admin = await db.get('SELECT id FROM users WHERE email = ?', [process.env.ADMIN_EMAIL?.toLowerCase()]);
if (!admin) {
const hash = await bcrypt.hash(process.env.ADMIN_PASSWORD || 'admin123', 10);
await db.run(
'INSERT INTO users (name, email, password_hash, role, points) VALUES (?,?,?,?,?)',
['Admin', (process.env.ADMIN_EMAIL || 'admin@brochette.ny').toLowerCase(), hash, 'admin', 0]
);
}
httpServer.listen(PORT, () => console.log(
✅ Server running on http://localhost:${PORT}));})();