import fs from 'node:fs' import path from 'node:path' import http from 'node:http' import { fileURLToPath } from 'node:url' import express from 'express' import compression from 'compression' import { JSDOM } from 'jsdom'; import DOMPurify from 'dompurify'; import { Server as SocketIOServer } from 'socket.io' import { server as serverConfig, headerBarUrl, logoUrl, database } from './config.js' import * as roomsManager from './lib/rooms.js' // Assuming rooms.js uses ES module exports // Database - This will be initialized later. Needs to export `db` as an ES module. // Example: export let db; export function initializeDb(callback) { db = new TheActualDb(callback); } // For now, assuming it exports an object with a `db` property that gets assigned. import * as dbModule from './lib/data/redis.js' // ESM equivalent of __dirname and __filename const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) // Helper for consistent logging const log = (level, ...args) => { const timestamp = new Date().toISOString() if (level === 'error') { console.error(`[${timestamp}] ERROR:`, ...args) } else if (level === 'warn') { console.warn(`[${timestamp}] WARN:`, ...args) } else { console.log(`[${timestamp}] INFO:`, ...args) } } // --- Global Error Handlers --- process.on('uncaughtException', (err, origin) => { const message = `UNCAUGHT EXCEPTION: ${err.stack || err}\nException origin: ${origin}` log('error', message) try { fs.writeFileSync(path.join(__dirname, 'crash_uncaught.log'), `${message}\nPID: ${process.pid}\n`) } catch (e) { log('error', 'Failed to write crash_uncaught.log:', e) } process.exit(1) }) process.on('unhandledRejection', (reason, promise) => { const reasonStack = reason instanceof Error && reason.stack ? reason.stack : reason const message = `UNHANDLED REJECTION: ${reasonStack}\nAt promise: ${JSON.stringify(promise)}` log('error', message) try { fs.writeFileSync(path.join(__dirname, 'crash_unhandled.log'), `${message}\nPID: ${process.pid}\n`) } catch (e) { log('error', 'Failed to write crash_unhandled.log:', e) } process.exit(1) }) let dbInstance // Will hold the initialized DB instance // --- Globals (Minimize these if possible) --- const sidsToUsernames = new Map() // Using a Map is cleaner for sids_to_user_names async function main() { try { log('info', 'Inside main function.') const app = express() const router = express.Router() app.use(compression()) app.set('views', path.join(__dirname, 'views')) app.set('view engine', 'pug') app.use(serverConfig.baseurl, router) router.use(express.static(path.join(__dirname, 'node_modules'))) router.use(express.static(path.join(__dirname, 'client'))) const httpServer = http.createServer(app) const io = new SocketIOServer(httpServer, { path: serverConfig.baseurl === '/' ? '/socket.io' : `${serverConfig.baseurl}/socket.io`, cookie: false }) const window = new JSDOM('').window; const purify = DOMPurify(window); // --- Database Initialization --- // This assumes your db module exports a class or function that can be instantiated // and takes a callback for when it's ready. // Modify this according to how your actual db module works. log('info', `Initializing database type: ${database.type}`) dbInstance = new dbModule.db(() => { // This `db` should be the actual constructor/class from your module log('info', 'Database initialized.') cleanAndInitializeDemoRoom() startServer() }) function startServer() { app.use((err, req, res, next) => { log('error', `EXPRESS GLOBAL ERROR HANDLER triggered for ${req.originalUrl}:`, err.stack || err) if (!res.headersSent) { res.status(500).send('Something broke on the server!') } else { next(err) } }) httpServer.listen(serverConfig.port, () => { log('info', `Web server available on http://127.0.0.1:${serverConfig.port}`) log('info', `Base URL: http://127.0.0.1:${serverConfig.port}${serverConfig.baseurl}`) }) } // --- HTTP Routes (defined on the router) --- router.get('/', (req, res) => { try { log('info', `Route / requested by ${req.ip}`) const fullUrl = `${req.protocol}://${req.get('host')}${req.baseUrl}` const connectedSockets = io.sockets.sockets const clientsCount = connectedSockets.size log('info', `Socket.IO 1.7.4 clientsCount: ${clientsCount}`) log('info', { url: fullUrl, headerBarUrl, logoUrl, connected: clientsCount, home: true }) res.render('home', { url: fullUrl, headerBarUrl, logoUrl, connected: clientsCount, home: true }) } catch (renderError) { log('error', 'Error rendering / route:', renderError) if (!res.headersSent) { res.status(500).send('Error rendering page.') } } }) router.get('/demo', (req, res) => { try { log('info', `Route /demo requested by ${req.ip}`) const fullUrl = `${req.protocol}://${req.get('host')}${req.baseUrl}` res.render('index', { // Assumes index.jade pageTitle: 'Memo - demo', headerBarUrl, logoUrl, url: fullUrl, demo: true }) } catch (renderError) { log('error', 'Error rendering /demo route:', renderError) if (!res.headersSent) { res.status(500).send('Error rendering page.') } } }) router.get('/:id', (req, res) => { try { const { id } = req.params log('info', `Route /:id (${id}) requested by ${req.ip}`) const fullUrl = `${req.protocol}://${req.get('host')}${req.baseUrl}` res.render('index', { // Assumes index.jade pageTitle: `Memo - ${id}`, headerBarUrl, logoUrl, url: fullUrl }) } catch (renderError) { log('error', `Error rendering /:id (${req.params.id}) route:`, renderError) if (!res.headersSent) { res.status(500).send('Error rendering page.') } } }) router.get('/stats', (req, res) => { log('info', 'TODO: /stats route hit') res.status(501).send('Stats page not implemented yet.') }) // --- Socket.IO Event Handlers --- io.on('connection', (client) => { log('info', `Socket connected: ${client.id}`) client.on('message', async(message) => { // Made async if db ops become async try { // log('info', `Socket message from ${client.id}: Action - ${message.action}`); if (!message || typeof message.action !== 'string') { log('warn', `Invalid message format from ${client.id}:`, message) return } // It's better to break this switch into separate functions for clarity // For now, just ensuring basic safety and async potential // All db operations should ideally be promisified and awaited let responseAction; let responseData; let broadcastData const currentRoom = roomsManager.get_room(client) // Assuming this is synchronous switch (message.action) { case 'initializeMe': await initClient(client, currentRoom) // Make initClient async break case 'joinRoom': // roomName should be in message.data const roomToJoin = scrub(message.data) // Assuming message.data is the room name if (roomToJoin) { await joinRoom(client, roomToJoin) // Make joinRoom async client.emit('message', { action: 'roomAccept', data: { room: roomToJoin } }) } else { log('warn', 'JoinRoom: Room name not provided or invalid.') } break case 'editBoardMetas': const { id: boardId, prop, value: boardValue } = message.data || {} if (!boardId || !prop || typeof boardValue === 'undefined') break const cleanBoardValue = scrub(boardValue) dbInstance.editBoardMetas(currentRoom, boardId, prop, cleanBoardValue) // Assuming sync for now broadcastData = { id: boardId, prop, value: cleanBoardValue } broadcastToRoom(client, currentRoom, { action: 'editBoardMetas', data: broadcastData }) break case 'moveCard': const { id: cardIdMove, position } = message.data || {} if (!cardIdMove || !position || typeof position.left === 'undefined' || typeof position.top === 'undefined') break const cleanPos = { left: Number(scrub(position.left)), top: Number(scrub(position.top)) } dbInstance.cardSetXY(currentRoom, cardIdMove, cleanPos.left, cleanPos.top) // Assuming sync broadcastData = { id: scrub(cardIdMove), position: cleanPos } broadcastToRoom(client, currentRoom, { action: 'moveCard', data: broadcastData }) break case 'createCard': const cardData = message.data || {} // Add proper validation for all fields const newCard = { text: scrub(cardData.text), id: scrub(cardData.id), x: Number(scrub(cardData.x)), y: Number(scrub(cardData.y)), rot: Number(scrub(cardData.rot)), colour: scrub(cardData.colour), sticker: null // from original createCard function } if (!newCard.id) { log('warn', 'CreateCard: ID is missing.'); break } dbInstance.createCard(currentRoom, newCard.id, newCard) // Assuming sync broadcastToRoom(client, currentRoom, { action: 'createCard', data: newCard }) break // ... (Continue refactoring other cases similarly) // Ensure all data is scrubbed, validated, and db operations are clear. // Use `await` if db operations become async. case 'editCard': const { id: cardIdEdit, value: cardValueEdit } = message.data || {} if (!cardIdEdit || typeof cardValueEdit === 'undefined') break const cleanCardValue = scrub(cardValueEdit) dbInstance.cardEdit(currentRoom, cardIdEdit, cleanCardValue) // Assuming sync broadcastData = { id: cardIdEdit, value: cleanCardValue } broadcastToRoom(client, currentRoom, { action: 'editCard', data: broadcastData }) break case 'deleteCard': const { id: cardIdDelete } = message.data || {} if (!cardIdDelete) break const cleanCardIdDelete = scrub(cardIdDelete) dbInstance.deleteCard(currentRoom, cleanCardIdDelete) // Assuming sync broadcastToRoom(client, currentRoom, { action: 'deleteCard', data: { id: cleanCardIdDelete } }) break // Add other cases here, applying similar logic: // - Validate input from message.data // - Scrub data // - Call dbInstance methods (await if they become async) // - broadcastToRoom with structured action and data case 'setUserName': const newUsername = scrub(message.data) if (newUsername) { setUserName(client, newUsername) broadcastToRoom(client, currentRoom, { action: 'nameChangeAnnounce', data: { sid: client.id, user_name: newUsername } }) } break // ... other cases like 'createColumn', 'addSticker', etc. default: log('info', `Unknown socket action from ${client.id}: ${message.action}`) break } } catch (socketError) { log('error', `Error in socket message handler (client: ${client.id}, action: ${message?.action}):`, socketError) // Optionally emit an error back to the client client.emit('message', { action: 'serverError', data: { message: 'An internal server error occurred.' } }) } }) client.on('disconnect', (reason) => { log('info', `Socket disconnected: ${client.id}, Reason: ${reason}`) leaveRoom(client) }) }) // end io.on('connection') // --- Helper Functions (Socket & Business Logic) --- // (Refactored with async/await where db calls are made, assuming db calls will be promisified) async function initClient(client, room) { log('info', `Initializing client ${client.id} for room ${room}`) try { // These db calls should ideally return promises const cards = await new Promise((resolve, reject) => dbInstance.getAllCards(room, resolve)) client.emit('message', { action: 'initCards', data: cards || [] }) const columns = await new Promise((resolve, reject) => dbInstance.getAllColumns(room, resolve)) client.emit('message', { action: 'initColumns', data: columns || [] }) const revisionsData = await new Promise((resolve, reject) => dbInstance.getRevisions(room, resolve)) client.emit('message', { action: 'initRevisions', data: revisionsData ? Object.keys(revisionsData) : [] }) let theme = await new Promise((resolve, reject) => dbInstance.getTheme(room, resolve)) theme = theme === null ? 'bigcards' : theme client.emit('message', { action: 'changeTheme', data: theme }) const boardSize = await new Promise((resolve, reject) => dbInstance.getBoardSize(room, resolve)) if (boardSize) { client.emit('message', { action: 'setBoardSize', data: boardSize }) } const roommateClients = roomsManager.room_clients(room) || [] const roommates = roommateClients .filter((rc) => rc.id !== client.id) .map((rc) => ({ sid: rc.id, user_name: sidsToUsernames.get(rc.id) || 'Anonymous' })) client.emit('message', { action: 'initialUsers', data: roommates }) } catch (error) { log('error', `Error initializing client ${client.id}:`, error) client.emit('message', { action: 'initError', data: { message: 'Failed to initialize client data.' } }) } } async function joinRoom(client, roomName) { // roomName is the string name log('info', `Client ${client.id} attempting to join room: ${roomName}`) const announceMsg = { action: 'join-announce', data: { sid: client.id, user_name: sidsToUsernames.get(client.id) || 'Anonymous' } } roomsManager.add_to_room_and_announce(client, roomName, announceMsg) // Assuming this updates client's room internally // No explicit successFunction needed if client.emit is handled after this call } function leaveRoom(client) { const currentRoom = roomsManager.get_room(client) if (currentRoom) { log('info', `Client ${client.id} leaving room: ${currentRoom}`) const announceMsg = { action: 'leave-announce', data: { sid: client.id } } roomsManager.remove_from_all_rooms_and_announce(client, announceMsg) // This should handle removal from its current room } sidsToUsernames.delete(client.id) } function broadcastToRoom(client, room, message) { // Pass explicit room // Filter clients in the specific room, excluding the sender client const targetClients = (roomsManager.room_clients(room) || []) .filter((rc) => rc.id !== client.id) targetClients.forEach((rc) => rc.emit('message', message)) } function scrub(text) { if (text === null || typeof text === 'undefined') return null let strText = String(text) if (strText.length > 65535) { strText = strText.substring(0, 65535) } return sanitizer.sanitize(strText) // Ensure sanitizer is effective and secure } function setUserName(client, name) { const cleanName = scrub(name) || 'Anonymous' client.user_name = cleanName // Storing on client object if roomsManager uses it sidsToUsernames.set(client.id, cleanName) log('info', `Username for ${client.id} set to: ${cleanName}`) } function roundRand(max) { return Math.floor(Math.random() * max) } function cleanAndInitializeDemoRoom() { const demoRoom = '/demo' log('info', `Initializing demo room: ${demoRoom}`) dbInstance.clearRoom(demoRoom, () => { // Assuming clearRoom is async via callback dbInstance.createColumn(demoRoom, 'Pas commencé') dbInstance.createColumn(demoRoom, 'Commencé') // ... (add other columns and cards for demo) log('info', 'Demo room columns and cards created.') // Example card creations: const demoCards = [ { id: 'card1', text: "Salut, c'est fun", colour: 'yellow' }, { id: 'card2', text: "Salut, c'est une nouvelle histoire.", colour: 'white' } ] demoCards.forEach((card) => { dbInstance.createCard(demoRoom, card.id, { id: card.id, colour: card.colour, rot: Math.random() * 10 - 5, x: roundRand(600), y: roundRand(300), text: card.text, sticker: null }) }) }) } // ... (Refactor exportBoard, exportJson, importJson, revision functions similarly) // - Use async/await if db calls become promisified. // - Ensure data is scrubbed. // - Use client.emit for sending responses. } catch (startupError) { log('error', 'FATAL STARTUP ERROR in main:', startupError.stack || startupError) process.exit(1) } } log('info', 'Calling main function...') main().catch((mainError) => { log('error', 'FATAL ERROR from main promise:', mainError.stack || mainError) process.exit(1) }) log('info', 'Main function called. Script will continue if server is listening.')