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>
This commit is contained in:
224
booth/server.js
Normal file
224
booth/server.js
Normal file
@@ -0,0 +1,224 @@
|
||||
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}`);
|
||||
});
|
||||
Reference in New Issue
Block a user