server.js 11 KB

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