server.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. // server.js - Fichier principal du serveur
  2. const express = require('express');
  3. const sqlite3 = require('sqlite3').verbose();
  4. const bcrypt = require('bcrypt');
  5. const cors = require('cors');
  6. const path = require('path');
  7. const http = require('http');
  8. const WebSocket = require('ws');
  9. const app = express();
  10. const server = http.createServer(app);
  11. const port = process.env.PORT || 3000;
  12. // Create a WebSocket server
  13. const wss = new WebSocket.Server({ server });
  14. // Store connected WebSocket clients
  15. const clients = new Set();
  16. // WebSocket connection handler
  17. wss.on('connection', (ws) => {
  18. console.log('Client connected');
  19. clients.add(ws);
  20. // Send a welcome message
  21. ws.send(JSON.stringify({ type: 'connection', message: 'Connected to WebSocket server' }));
  22. // Handle client disconnection
  23. ws.on('close', () => {
  24. console.log('Client disconnected');
  25. clients.delete(ws);
  26. });
  27. // Handle messages from clients (not used in this implementation, but available for future use)
  28. ws.on('message', (message) => {
  29. console.log('Received message:', message);
  30. });
  31. });
  32. // Helper function to broadcast messages to all connected clients
  33. function broadcastMessage(type, data) {
  34. const message = JSON.stringify({ type, data });
  35. clients.forEach(client => {
  36. if (client.readyState === WebSocket.OPEN) {
  37. client.send(message);
  38. }
  39. });
  40. }
  41. const getAllowedOrigins = () => {
  42. // Récupérer les origines depuis les variables d'environnement ou utiliser des valeurs par défaut
  43. const corsOrigins = process.env.CORS_ORIGINS || 'http://localhost:3000,http://localhost:3001,http://localhost';
  44. // Convertir en tableau et ajouter undefined pour les requêtes sans origine
  45. const origins = corsOrigins.split(',').map(origin => origin.trim());
  46. origins.push(undefined); // Pour les requêtes sans origine (comme curl ou Postman)
  47. console.log('Origines CORS autorisées:', origins.filter(o => o !== undefined));
  48. return origins;
  49. };
  50. const corsOptions = {
  51. origin: function (origin, callback) {
  52. const allowedOrigins = getAllowedOrigins();
  53. if (!origin || allowedOrigins.indexOf(origin) !== -1) {
  54. callback(null, true);
  55. } else {
  56. console.log(`Origine bloquée par CORS: ${origin}`);
  57. callback(null, false);
  58. }
  59. },
  60. credentials: true,
  61. methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  62. allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With']
  63. };
  64. // Log CORS options
  65. app.use((req, res, next) => {
  66. console.log(`${new Date().toISOString()} - ${req.method} ${req.url} - Origin: ${req.headers.origin || 'Aucune'}`);
  67. next();
  68. });
  69. // Middleware
  70. app.use(express.json());
  71. app.use(cors(corsOptions));
  72. app.use(express.static(path.join(__dirname, 'public')));
  73. // Connexion à la base de données SQLite
  74. const DB_PATH = process.env.DB_PATH || '/app/data/framed.db';
  75. console.log(`Tentative de connexion à la BD: ${DB_PATH}`);
  76. console.log(`Origines CORS autorisées: ${getAllowedOrigins().filter(o => o !== undefined).join(', ')}`);
  77. const db = new sqlite3.Database(DB_PATH, (err) => {
  78. if (err) {
  79. console.error('Erreur de connexion à la base de données:', err.message);
  80. } else {
  81. console.log('Connecté à la base de données SQLite');
  82. initDatabase();
  83. }
  84. });
  85. // Initialiser la base de données
  86. function initDatabase() {
  87. // Activer les clés étrangères
  88. db.run('PRAGMA foreign_keys = ON');
  89. // Créer la table des utilisateurs
  90. db.run(`
  91. CREATE TABLE IF NOT EXISTS users (
  92. id INTEGER PRIMARY KEY AUTOINCREMENT,
  93. username TEXT UNIQUE NOT NULL,
  94. password TEXT NOT NULL,
  95. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  96. )
  97. `);
  98. // Créer la table des scores
  99. db.run(`
  100. CREATE TABLE IF NOT EXISTS scores (
  101. id INTEGER PRIMARY KEY AUTOINCREMENT,
  102. user_id INTEGER NOT NULL,
  103. date TEXT NOT NULL,
  104. game_type TEXT NOT NULL,
  105. game_number INTEGER,
  106. score INTEGER NOT NULL,
  107. comment TEXT,
  108. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  109. FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
  110. )
  111. `);
  112. console.log('Tables vérifiées/créées');
  113. }
  114. // Route de test simple
  115. app.get('/api/test', (req, res) => {
  116. res.json({ message: 'Le serveur fonctionne correctement!' });
  117. });
  118. // Routes d'API
  119. // Inscription
  120. app.post('/api/register', async (req, res) => {
  121. const { username, password } = req.body;
  122. if (!username || !password) {
  123. return res.status(400).json({ error: 'Nom d\'utilisateur et mot de passe requis' });
  124. }
  125. try {
  126. // Vérifier si l'utilisateur existe déjà
  127. db.get('SELECT * FROM users WHERE username = ?', [username], async (err, user) => {
  128. if (err) {
  129. return res.status(500).json({ error: err.message });
  130. }
  131. if (user) {
  132. return res.status(400).json({ error: 'Cet utilisateur existe déjà' });
  133. }
  134. // Hasher le mot de passe
  135. const hashedPassword = await bcrypt.hash(password, 10);
  136. // Insérer le nouvel utilisateur
  137. db.run('INSERT INTO users (username, password) VALUES (?, ?)',
  138. [username, hashedPassword],
  139. function (err) {
  140. if (err) {
  141. return res.status(500).json({ error: err.message });
  142. }
  143. res.status(201).json({
  144. message: 'Utilisateur créé avec succès',
  145. userId: this.lastID,
  146. username
  147. });
  148. }
  149. );
  150. });
  151. } catch (error) {
  152. res.status(500).json({ error: error.message });
  153. }
  154. });
  155. // Connexion
  156. app.post('/api/login', (req, res) => {
  157. const { username, password } = req.body;
  158. if (!username || !password) {
  159. return res.status(400).json({ error: 'Nom d\'utilisateur et mot de passe requis' });
  160. }
  161. db.get('SELECT * FROM users WHERE username = ?', [username], async (err, user) => {
  162. if (err) {
  163. return res.status(500).json({ error: err.message });
  164. }
  165. if (!user) {
  166. return res.status(401).json({ error: 'Identifiants invalides' });
  167. }
  168. // Vérifier le mot de passe
  169. const validPassword = await bcrypt.compare(password, user.password);
  170. if (!validPassword) {
  171. return res.status(401).json({ error: 'Identifiants invalides' });
  172. }
  173. res.json({
  174. userId: user.id,
  175. username: user.username
  176. });
  177. });
  178. });
  179. // Ajouter un score
  180. app.post('/api/scores', (req, res) => {
  181. const { userId, date, gameType, gameNumber, score, comment } = req.body;
  182. if (!userId || !date || !gameType || !score) {
  183. return res.status(400).json({ error: 'Données incomplètes' });
  184. }
  185. db.run(
  186. 'INSERT INTO scores (user_id, date, game_type, game_number, score, comment) VALUES (?, ?, ?, ?, ?, ?)',
  187. [userId, date, gameType, gameNumber, score, comment || null],
  188. function (err) {
  189. if (err) {
  190. return res.status(500).json({ error: err.message });
  191. }
  192. const newScore = {
  193. id: this.lastID,
  194. userId,
  195. date,
  196. gameType,
  197. gameNumber,
  198. score,
  199. comment
  200. };
  201. // Get username for the new score
  202. db.get('SELECT username FROM users WHERE id = ?', [userId], (err, user) => {
  203. if (!err && user) {
  204. newScore.username = user.username;
  205. // Broadcast the new score to all connected clients
  206. broadcastMessage('new-score', newScore);
  207. }
  208. });
  209. res.status(201).json(newScore);
  210. }
  211. );
  212. });
  213. // Récupérer les scores d'un utilisateur
  214. app.get('/api/scores/user/:userId', (req, res) => {
  215. const userId = req.params.userId;
  216. db.all('SELECT * FROM scores WHERE user_id = ? ORDER BY date DESC', [userId], (err, scores) => {
  217. if (err) {
  218. return res.status(500).json({ error: err.message });
  219. }
  220. res.json(scores);
  221. });
  222. });
  223. // Récupérer tous les scores (pour le classement)
  224. app.get('/api/scores', (req, res) => {
  225. const gameType = req.query.gameType || 'all';
  226. let query = `
  227. SELECT s.*, u.username
  228. FROM scores s
  229. JOIN users u ON s.user_id = u.id
  230. `;
  231. if (gameType !== 'all') {
  232. query += ` WHERE s.game_type = '${gameType}'`;
  233. }
  234. db.all(query, (err, scores) => {
  235. if (err) {
  236. return res.status(500).json({ error: err.message });
  237. }
  238. res.json(scores);
  239. });
  240. });
  241. // Supprimer un score
  242. app.delete('/api/scores/:id', (req, res) => {
  243. const scoreId = req.params.id;
  244. const userId = req.query.userId;
  245. if (!userId) {
  246. return res.status(400).json({ error: 'Utilisateur non spécifié' });
  247. }
  248. db.run('DELETE FROM scores WHERE id = ? AND user_id = ?', [scoreId, userId], function (err) {
  249. if (err) {
  250. return res.status(500).json({ error: err.message });
  251. }
  252. if (this.changes === 0) {
  253. return res.status(404).json({ error: 'Score non trouvé ou non autorisé' });
  254. }
  255. // Broadcast the deleted score to all connected clients
  256. broadcastMessage('delete-score', { id: scoreId, userId });
  257. res.json({ message: 'Score supprimé avec succès' });
  258. });
  259. });
  260. // Récupérer les statistiques d'un utilisateur
  261. app.get('/api/stats/:userId', (req, res) => {
  262. const userId = req.params.userId;
  263. const query = `
  264. SELECT
  265. game_type,
  266. COUNT(*) as total_games,
  267. AVG(score) as average_score,
  268. MIN(score) as best_score
  269. FROM scores
  270. WHERE user_id = ?
  271. GROUP BY game_type
  272. `;
  273. db.all(query, [userId], (err, stats) => {
  274. if (err) {
  275. return res.status(500).json({ error: err.message });
  276. }
  277. res.json(stats);
  278. });
  279. });
  280. // Récupérer le classement
  281. app.get('/api/leaderboard', (req, res) => {
  282. const gameType = req.query.gameType || 'all';
  283. let query = `
  284. SELECT
  285. u.id,
  286. u.username,
  287. COUNT(*) as total_games,
  288. AVG(s.score) as average_score,
  289. MIN(s.score) as best_score
  290. FROM scores s
  291. JOIN users u ON s.user_id = u.id
  292. `;
  293. if (gameType !== 'all') {
  294. query += ` WHERE s.game_type = '${gameType}'`;
  295. }
  296. query += `
  297. GROUP BY u.id
  298. ORDER BY average_score ASC, best_score ASC
  299. `;
  300. db.all(query, (err, leaderboard) => {
  301. if (err) {
  302. return res.status(500).json({ error: err.message });
  303. }
  304. res.json(leaderboard);
  305. });
  306. });
  307. // Gestion du mode de production ou développement
  308. if (process.env.NODE_ENV === 'production') {
  309. // En production, servir les fichiers statiques
  310. app.use(express.static(path.join(__dirname, 'public')));
  311. // Route par défaut pour servir l'application React
  312. app.get('*', (req, res) => {
  313. res.sendFile(path.join(__dirname, 'public', 'index.html'));
  314. });
  315. } else {
  316. // En développement, juste répondre à l'API
  317. app.get('/', (req, res) => {
  318. res.json({ message: 'API Framed Tracker fonctionne correctement. Utilisez le port 3001 pour accéder au frontend.' });
  319. });
  320. }
  321. // Démarrer le serveur HTTP avec WebSocket
  322. server.listen(port, () => {
  323. console.log(`Serveur HTTP en écoute sur le port ${port}`);
  324. console.log(`Serveur WebSocket en écoute sur le port ${port}`);
  325. });
  326. // Gestion des erreurs
  327. app.use((err, req, res, next) => {
  328. console.error('Erreur API:', err);
  329. res.status(500).json({ error: 'Erreur serveur' });
  330. });
  331. // Fermer proprement la connexion à la base de données lors de l'arrêt du serveur
  332. process.on('SIGINT', () => {
  333. db.close((err) => {
  334. if (err) {
  335. console.error(err.message);
  336. }
  337. console.log('Connexion à la base de données fermée');
  338. process.exit(0);
  339. });
  340. });
  341. // Gestion des exceptions non capturées
  342. process.on('uncaughtException', (error) => {
  343. console.error('ERREUR NON CAPTURÉE:', error);
  344. });
  345. process.on('unhandledRejection', (reason, promise) => {
  346. console.error('PROMESSE REJETÉE NON GÉRÉE:', reason);
  347. });