const express = require('express'); const http = require('http'); const WebSocket = require('ws'); const multer = require('multer'); const sharp = require('sharp'); const path = require('path'); const fs = require('fs'); const crypto = require('crypto'); const app = express(); const server = http.createServer(app); const wss = new WebSocket.Server({ server }); // --- Configuration --- const PORT = process.env.PORT || 3000; const PHOTOS_DIR = path.join(__dirname, 'data', 'photos'); const THUMBS_DIR = path.join(__dirname, 'data', 'thumbs'); // Creer les dossiers fs.mkdirSync(PHOTOS_DIR, { recursive: true }); fs.mkdirSync(THUMBS_DIR, { recursive: true }); // --- Sessions evenement --- // { eventId: { password, photos: [{name, timestamp}], createdAt } } const sessions = new Map(); function genererPassword() { return crypto.randomInt(1000, 9999).toString(); } function getSession(eventId) { if (!sessions.has(eventId)) { sessions.set(eventId, { password: genererPassword(), photos: [], createdAt: Date.now(), }); // Creer le dossier fs.mkdirSync(path.join(PHOTOS_DIR, eventId), { recursive: true }); fs.mkdirSync(path.join(THUMBS_DIR, eventId), { recursive: true }); } return sessions.get(eventId); } // --- Upload photos depuis la borne --- const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 20 * 1024 * 1024 }, // 20 Mo max }); // API Key pour l'upload (simple) const API_KEY = process.env.API_KEY || 'booth-secret-key'; function verifierApiKey(req, res, next) { const key = req.headers['x-api-key'] || req.query.key; if (key !== API_KEY) { return res.status(401).json({ error: 'Cle API invalide' }); } next(); } // --- Routes API --- // Upload photo depuis la borne app.post('/api/:eventId/upload', verifierApiKey, upload.single('photo'), async (req, res) => { try { const { eventId } = req.params; const session = getSession(eventId); const timestamp = Date.now(); const filename = `${timestamp}.jpg`; // Sauvegarder la photo originale const photoPath = path.join(PHOTOS_DIR, eventId, filename); await sharp(req.file.buffer) .jpeg({ quality: 90 }) .toFile(photoPath); // Creer la miniature const thumbPath = path.join(THUMBS_DIR, eventId, filename); await sharp(req.file.buffer) .resize(400, 400, { fit: 'cover' }) .jpeg({ quality: 75 }) .toFile(thumbPath); // Ajouter a la session const photoInfo = { name: filename, timestamp }; session.photos.push(photoInfo); // Notifier les clients WebSocket broadcast(eventId, { type: 'new_photo', photo: photoInfo, }); res.json({ ok: true, filename }); } catch (err) { console.error('Erreur upload:', err); res.status(500).json({ error: 'Erreur upload' }); } }); // Info session (mot de passe pour la borne) app.get('/api/:eventId/info', verifierApiKey, (req, res) => { const session = getSession(req.params.eventId); res.json({ password: session.password, photoCount: session.photos.length, }); }); // Regenerer le mot de passe app.post('/api/:eventId/reset-password', verifierApiKey, (req, res) => { const session = getSession(req.params.eventId); session.password = genererPassword(); res.json({ password: session.password }); }); // --- Routes publiques (invites) --- // Verifier le mot de passe app.post('/api/:eventId/auth', express.json(), (req, res) => { const session = sessions.get(req.params.eventId); if (!session) { return res.status(404).json({ error: 'Evenement introuvable' }); } if (req.body.password !== session.password) { return res.status(403).json({ error: 'Mot de passe incorrect' }); } // Generer un token simple const token = crypto.randomBytes(16).toString('hex'); session.token = session.token || new Set(); session.token.add(token); res.json({ ok: true, token, photoCount: session.photos.length }); }); // Middleware pour verifier le token invite function verifierToken(req, res, next) { const token = req.headers['x-token'] || req.query.token; const session = sessions.get(req.params.eventId); if (!session || !session.token || !session.token.has(token)) { return res.status(403).json({ error: 'Acces refuse' }); } req.session = session; next(); } // Liste des photos app.get('/api/:eventId/photos', verifierToken, (req, res) => { res.json({ photos: req.session.photos }); }); // Servir les photos app.get('/photos/:eventId/:filename', (req, res) => { const token = req.query.token; const session = sessions.get(req.params.eventId); if (!session || !session.token || !session.token.has(token)) { return res.status(403).send('Acces refuse'); } const filePath = path.join(PHOTOS_DIR, req.params.eventId, req.params.filename); if (!fs.existsSync(filePath)) return res.status(404).send('Not found'); res.sendFile(filePath); }); // Servir les miniatures app.get('/thumbs/:eventId/:filename', (req, res) => { const token = req.query.token; const session = sessions.get(req.params.eventId); if (!session || !session.token || !session.token.has(token)) { return res.status(403).send('Acces refuse'); } const filePath = path.join(THUMBS_DIR, req.params.eventId, req.params.filename); if (!fs.existsSync(filePath)) return res.status(404).send('Not found'); res.sendFile(filePath); }); // --- WebSocket --- wss.on('connection', (ws, req) => { const url = new URL(req.url, `http://localhost:${PORT}`); const eventId = url.searchParams.get('event'); const token = url.searchParams.get('token'); const session = sessions.get(eventId); if (!session || !session.token || !session.token.has(token)) { ws.close(4001, 'Acces refuse'); return; } ws.eventId = eventId; ws.isAlive = true; ws.on('pong', () => { ws.isAlive = true; }); ws.on('error', () => {}); }); function broadcast(eventId, data) { const msg = JSON.stringify(data); wss.clients.forEach(client => { if (client.eventId === eventId && client.readyState === WebSocket.OPEN) { client.send(msg); } }); } // Ping pour garder les connexions setInterval(() => { wss.clients.forEach(ws => { if (!ws.isAlive) return ws.terminate(); ws.isAlive = false; ws.ping(); }); }, 30000); // --- Frontend statique --- app.use(express.static(path.join(__dirname, 'public'))); // SPA fallback app.get('/:eventId', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); // --- Demarrage --- server.listen(PORT, () => { console.log(`Booth gallery running on port ${PORT}`); });