Skip to content

b #45

Description

@mamadousdiouf40-sketch

{
"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 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'));
});

// --- 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}));
})();

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions