Files
photobooth/booth/server.js
Jules f0584e8fdf Galerie live booth.copydev.fr + icones agrandies + pellicule
- booth/: serveur Node.js galerie photo live avec auth par code 4 chiffres
- Backend: envoi auto des photos vers booth, endpoint QR dynamique
- Frontend: code + QR booth sur ecran accueil, icone pellicule pour multi-shot
- Icones mode agrandies (5rem)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 17:27:05 +02:00

225 lines
6.5 KiB
JavaScript

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}`);
});