// server.js const express = require('express'); const path = require('path'); const rateLimit = require('express-rate-limit'); const auth = require('./auth'); const app = express(); const PORT = process.env.PORT || 7860; // --- REQUEST LOGGER (MORGAN-LIKE) --- app.use((req, res, next) => { const start = Date.now(); const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress; res.on('finish', () => { const dur = Date.now() - start; const log = { ts: new Date().toISOString(), level: 'INFO', type: 'HTTP', method: req.method, url: req.originalUrl, status: res.statusCode, durationMs: dur, ip, ua: req.headers['user-agent']?.substring(0,50) }; // Colorize status for console readability const color = res.statusCode >= 500 ? '\x1b[31m' : res.statusCode >= 400 ? '\x1b[33m' : '\x1b[32m'; console.log(`${color}${log.method} ${log.url} ${log.status}\x1b[0m ${log.durationMs}ms | IP: ${log.ip}`); // Structured JSON for HF Logs parsing console.log(JSON.stringify(log)); }); next(); }); app.use(express.json({ limit: '10mb' })); app.use(express.static(path.join(__dirname, 'public'))); app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 300, message: 'Trop de requêtes', standardHeaders: true, legacyHeaders: false })); const authMiddleware = (req, res, next) => { const h = req.headers.authorization; if (!h?.startsWith('Bearer ')) return res.status(401).json({ error: 'Token manquant' }); const payload = auth.verifyToken(h.split(' ')[1]); if (!payload) return res.status(401).json({ error: 'Token invalide/expiré' }); req.user = payload; next(); }; // PUBLIC app.get('/api/captcha', (req, res) => res.json(auth.generateCaptcha())); app.get('/api/tos/version', (req, res) => res.json({ version: auth.TOS_VERSION })); app.post('/api/register', (req, res) => { try { res.json(auth.registerUser(req.body.email, req.body.password, req.body.captchaToken, req.body.captchaAnswer, req.body.acceptTos)); } catch (e) { res.status(400).json({ error: e.message }); } }); app.post('/api/login', (req, res) => { try { const r = auth.loginUser(req.body.email, req.body.password); if (r.tosRequired) return res.status(403).json({ tosRequired: true, tempUser: r.tempUser }); res.json(r); } catch (e) { res.status(401).json({ error: e.message }); } }); app.post('/api/tos/accept', (req, res) => { try { res.json(auth.acceptTos(req.body.userId)); } catch (e) { res.status(400).json({ error: e.message }); } }); // PROTECTED app.get('/api/me', authMiddleware, (req, res) => { const s = auth.getUserStats(req.user.uid); const g = auth.getGlobalUsed(); res.json({ user: req.user, stats: s, globalUsed: g, globalLimit: auth.GLOBAL_QUOTA_BYTES }); }); app.get('/api/databases', authMiddleware, (req, res) => res.json(auth.listDatabases(req.user.uid))); app.post('/api/databases', authMiddleware, (req, res) => { try { res.status(201).json(auth.createDatabase(req.user.uid, req.body.name, req.body.captchaToken, req.body.captchaAnswer)); } catch (e) { res.status(400).json({ error: e.message }); } }); app.delete('/api/databases/:id', authMiddleware, (req, res) => { auth.deleteDatabase(req.user.uid, req.params.id); res.json({ success: true }); }); app.post('/api/databases/:id/query', authMiddleware, (req, res) => { const { sql, params } = req.body; if (!sql) return res.status(400).json({ error: 'SQL requis' }); if (/^\s*(PRAGMA|ATTACH|DETACH|VACUUM|BACKUP)/i.test(sql)) return res.status(403).json({ error: 'Commande interdite' }); res.json(auth.executeSql(req.user.uid, req.params.id, sql, params || [])); }); app.get('/api/databases/:id/download', authMiddleware, (req, res) => { const fp = auth.getDatabaseFilePath(req.user.uid, req.params.id); if (!fp || !require('fs').existsSync(fp)) return res.status(404).send('Not found'); res.download(fp, `db_${req.params.id}.sqlite`); }); app.get('*', (req, res) => res.sendFile(path.join(__dirname, 'public', 'index.html'))); app.listen(PORT, () => console.log(`\x1b[32m🚀 Server ready on port ${PORT} | Data: /data\x1b[0m`));