// server.js - Fichier principal du serveur const express = require('express'); const sqlite3 = require('sqlite3').verbose(); const bcrypt = require('bcrypt'); const cors = require('cors'); const path = require('path'); const http = require('http'); const WebSocket = require('ws'); const app = express(); const server = http.createServer(app); const port = process.env.PORT || 3000; // Create a WebSocket server const wss = new WebSocket.Server({ server }); // Store connected WebSocket clients const clients = new Set(); // WebSocket connection handler wss.on('connection', (ws) => { console.log('Client connected'); clients.add(ws); // Send a welcome message ws.send(JSON.stringify({ type: 'connection', message: 'Connected to WebSocket server' })); // Handle client disconnection ws.on('close', () => { console.log('Client disconnected'); clients.delete(ws); }); // Handle messages from clients (not used in this implementation, but available for future use) ws.on('message', (message) => { console.log('Received message:', message); }); }); // Helper function to broadcast messages to all connected clients function broadcastMessage(type, data) { const message = JSON.stringify({ type, data }); clients.forEach(client => { if (client.readyState === WebSocket.OPEN) { client.send(message); } }); } // Middleware app.use(express.json()); app.use(cors({ origin: ['http://localhost:3001', 'http://localhost:3000', 'http://localhost'], credentials: true })); app.use(express.static(path.join(__dirname, 'public'))); // Connexion à la base de données SQLite const DB_PATH = process.env.DB_PATH || '/app/data/framed.db'; console.log(`Tentative de connexion à la BD: ${DB_PATH}`); const db = new sqlite3.Database(DB_PATH, (err) => { if (err) { console.error('Erreur de connexion à la base de données:', err.message); } else { console.log('Connecté à la base de données SQLite'); initDatabase(); } }); // Initialiser la base de données function initDatabase() { // Activer les clés étrangères db.run('PRAGMA foreign_keys = ON'); // Créer la table des utilisateurs db.run(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); // Créer la table des scores db.run(` CREATE TABLE IF NOT EXISTS scores ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, date TEXT NOT NULL, game_type TEXT NOT NULL, game_number INTEGER, score INTEGER NOT NULL, comment TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ) `); console.log('Tables vérifiées/créées'); } // Route de test simple app.get('/api/test', (req, res) => { res.json({ message: 'Le serveur fonctionne correctement!' }); }); // Routes d'API // Inscription app.post('/api/register', async (req, res) => { const { username, password } = req.body; if (!username || !password) { return res.status(400).json({ error: 'Nom d\'utilisateur et mot de passe requis' }); } try { // Vérifier si l'utilisateur existe déjà db.get('SELECT * FROM users WHERE username = ?', [username], async (err, user) => { if (err) { return res.status(500).json({ error: err.message }); } if (user) { return res.status(400).json({ error: 'Cet utilisateur existe déjà' }); } // Hasher le mot de passe const hashedPassword = await bcrypt.hash(password, 10); // Insérer le nouvel utilisateur db.run('INSERT INTO users (username, password) VALUES (?, ?)', [username, hashedPassword], function(err) { if (err) { return res.status(500).json({ error: err.message }); } res.status(201).json({ message: 'Utilisateur créé avec succès', userId: this.lastID, username }); } ); }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Connexion app.post('/api/login', (req, res) => { const { username, password } = req.body; if (!username || !password) { return res.status(400).json({ error: 'Nom d\'utilisateur et mot de passe requis' }); } db.get('SELECT * FROM users WHERE username = ?', [username], async (err, user) => { if (err) { return res.status(500).json({ error: err.message }); } if (!user) { return res.status(401).json({ error: 'Identifiants invalides' }); } // Vérifier le mot de passe const validPassword = await bcrypt.compare(password, user.password); if (!validPassword) { return res.status(401).json({ error: 'Identifiants invalides' }); } res.json({ userId: user.id, username: user.username }); }); }); // Ajouter un score app.post('/api/scores', (req, res) => { const { userId, date, gameType, gameNumber, score, comment } = req.body; if (!userId || !date || !gameType || !score) { return res.status(400).json({ error: 'Données incomplètes' }); } db.run( 'INSERT INTO scores (user_id, date, game_type, game_number, score, comment) VALUES (?, ?, ?, ?, ?, ?)', [userId, date, gameType, gameNumber, score, comment || null], function(err) { if (err) { return res.status(500).json({ error: err.message }); } const newScore = { id: this.lastID, userId, date, gameType, gameNumber, score, comment }; // Get username for the new score db.get('SELECT username FROM users WHERE id = ?', [userId], (err, user) => { if (!err && user) { newScore.username = user.username; // Broadcast the new score to all connected clients broadcastMessage('new-score', newScore); } }); res.status(201).json(newScore); } ); }); // Récupérer les scores d'un utilisateur app.get('/api/scores/user/:userId', (req, res) => { const userId = req.params.userId; db.all('SELECT * FROM scores WHERE user_id = ? ORDER BY date DESC', [userId], (err, scores) => { if (err) { return res.status(500).json({ error: err.message }); } res.json(scores); }); }); // Récupérer tous les scores (pour le classement) app.get('/api/scores', (req, res) => { const gameType = req.query.gameType || 'all'; let query = ` SELECT s.*, u.username FROM scores s JOIN users u ON s.user_id = u.id `; if (gameType !== 'all') { query += ` WHERE s.game_type = '${gameType}'`; } db.all(query, (err, scores) => { if (err) { return res.status(500).json({ error: err.message }); } res.json(scores); }); }); // Supprimer un score app.delete('/api/scores/:id', (req, res) => { const scoreId = req.params.id; const userId = req.query.userId; if (!userId) { return res.status(400).json({ error: 'Utilisateur non spécifié' }); } db.run('DELETE FROM scores WHERE id = ? AND user_id = ?', [scoreId, userId], function(err) { if (err) { return res.status(500).json({ error: err.message }); } if (this.changes === 0) { return res.status(404).json({ error: 'Score non trouvé ou non autorisé' }); } // Broadcast the deleted score to all connected clients broadcastMessage('delete-score', { id: scoreId, userId }); res.json({ message: 'Score supprimé avec succès' }); }); }); // Récupérer les statistiques d'un utilisateur app.get('/api/stats/:userId', (req, res) => { const userId = req.params.userId; const query = ` SELECT game_type, COUNT(*) as total_games, AVG(score) as average_score, MIN(score) as best_score FROM scores WHERE user_id = ? GROUP BY game_type `; db.all(query, [userId], (err, stats) => { if (err) { return res.status(500).json({ error: err.message }); } res.json(stats); }); }); // Récupérer le classement app.get('/api/leaderboard', (req, res) => { const gameType = req.query.gameType || 'all'; let query = ` SELECT u.id, u.username, COUNT(*) as total_games, AVG(s.score) as average_score, MIN(s.score) as best_score FROM scores s JOIN users u ON s.user_id = u.id `; if (gameType !== 'all') { query += ` WHERE s.game_type = '${gameType}'`; } query += ` GROUP BY u.id ORDER BY average_score ASC, best_score ASC `; db.all(query, (err, leaderboard) => { if (err) { return res.status(500).json({ error: err.message }); } res.json(leaderboard); }); }); // Gestion du mode de production ou développement if (process.env.NODE_ENV === 'production') { // En production, servir les fichiers statiques app.use(express.static(path.join(__dirname, 'public'))); // Route par défaut pour servir l'application React app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); } else { // En développement, juste répondre à l'API app.get('/', (req, res) => { res.json({ message: 'API Framed Tracker fonctionne correctement. Utilisez le port 3001 pour accéder au frontend.' }); }); } // Démarrer le serveur HTTP avec WebSocket server.listen(port, () => { console.log(`Serveur HTTP en écoute sur le port ${port}`); console.log(`Serveur WebSocket en écoute sur le port ${port}`); }); // Gestion des erreurs app.use((err, req, res, next) => { console.error('Erreur API:', err); res.status(500).json({ error: 'Erreur serveur' }); }); // Fermer proprement la connexion à la base de données lors de l'arrêt du serveur process.on('SIGINT', () => { db.close((err) => { if (err) { console.error(err.message); } console.log('Connexion à la base de données fermée'); process.exit(0); }); }); // Gestion des exceptions non capturées process.on('uncaughtException', (error) => { console.error('ERREUR NON CAPTURÉE:', error); }); process.on('unhandledRejection', (reason, promise) => { console.error('PROMESSE REJETÉE NON GÉRÉE:', reason); });