require('dotenv').config(); const fs = require('fs'); //const bodyParser = require('body-parser'); const jsonServer = require('json-server'); const jwt = require('jsonwebtoken'); const bcrypt = require('bcrypt'); const path = require('path'); const scanPhoto = require('./scanphoto.js'); const SECRET_KEY = process.env.JWT_SECRET || '123456789'; const EXPIRES_IN = process.env.JWT_EXPIRES || '1h'; const PORT = process.env.SERVER_PORT || 4000; const server = jsonServer.create(); // ----------------------------------------------------- // STATIC FILES // ----------------------------------------------------- server.use(jsonServer.defaults({ static: path.join(__dirname, '../public') })); // ----------------------------------------------------- // CONFIG ENDPOINT (PUBBLICO) // ----------------------------------------------------- server.get('/config', (req, res) => { res.json({ baseUrl: process.env.BASE_URL }); }); // ----------------------------------------------------- // ROUTER DB // ----------------------------------------------------- let router; if (fs.existsSync('./api_v1/db.json')) { router = jsonServer.router('./api_v1/db.json'); } else { const initialData = fs.readFileSync('api_v1/initialDB.json', 'utf8'); fs.writeFileSync('api_v1/db.json', initialData); router = jsonServer.router('./api_v1/db.json'); } // ----------------------------------------------------- // USERS DB // ----------------------------------------------------- const userdb = JSON.parse(fs.readFileSync('./api_v1/users.json', 'UTF-8')); //server.use(bodyParser.urlencoded({ extended: true })); //server.use(bodyParser.json()); server.use(jsonServer.bodyParser); // ----------------------------------------------------- // JWT HELPERS // ----------------------------------------------------- function createToken(payload) { return jwt.sign(payload, SECRET_KEY, { expiresIn: EXPIRES_IN }); } // Denylist per token revocati fino a scadenza // token(string) -> exp(secondi epoch) const denylist = new Map(); function addToDenylist(token) { try { const decoded = jwt.decode(token); const exp = decoded?.exp || Math.floor(Date.now() / 1000) + 60; // fallback 60s denylist.set(token, exp); } catch { denylist.set(token, Math.floor(Date.now() / 1000) + 60); } } function isRevoked(token) { const exp = denylist.get(token); if (!exp) return false; // pulizia automatica entry scadute if (exp * 1000 < Date.now()) { denylist.delete(token); return false; } return true; } // Versione SINCRONA: lancia errore se il token non è valido function verifyToken(token) { return jwt.verify(token, SECRET_KEY); } function isAuthenticated({ email, password }) { return userdb.users.findIndex( user => user.email === email && bcrypt.compareSync(password, user.password) ) !== -1; } // ----------------------------------------------------- // RESET DB // ----------------------------------------------------- function resetDB() { const initialData = fs.readFileSync('api_v1/initialDB.json', 'utf8'); fs.writeFileSync('api_v1/db.json', initialData); router.db.setState(JSON.parse(initialData)); console.log('DB resettato'); } // ----------------------------------------------------- // HOME // ----------------------------------------------------- server.get('/', (req, res) => { res.sendFile(path.resolve('public/index.html')); }); // ----------------------------------------------------- // LOGIN (PUBBLICO) // ----------------------------------------------------- server.post('/auth/login', (req, res) => { const { email, password } = req.body; const user = userdb.users.find(u => u.email === email); if (!user || !bcrypt.compareSync(password, user.password)) { return res.status(401).json({ status: 401, message: 'Incorrect email or password' }); } const token = createToken({ id: user.id, email: user.email, name: user.name }); res.status(200).json({ token, name: user.name }); }); // ----------------------------------------------------- // LOGOUT (PUBBLICO - usa Authorization se presente) // ----------------------------------------------------- server.post('/auth/logout', (req, res) => { const auth = req.headers.authorization || ''; const [scheme, token] = auth.split(' '); if (scheme === 'Bearer' && token) { // Revoca il token fino alla sua scadenza addToDenylist(token); } // 204: nessun contenuto, logout idempotente return res.status(204).end(); }); // ----------------------------------------------------- // JWT MIDDLEWARE (TUTTO IL RESTO È PROTETTO) // ----------------------------------------------------- server.use(/^(?!\/auth).*$/, (req, res, next) => { const auth = req.headers.authorization || ''; const [scheme, token] = auth.split(' '); if (scheme !== 'Bearer' || !token) { return res.status(401).json({ status: 401, message: 'Bad authorization header' }); } // Blocca token revocati (logout) if (isRevoked(token)) { return res.status(401).json({ status: 401, message: 'Token revoked' }); } try { const decoded = verifyToken(token); req.user = decoded; // { id, email, name, iat, exp } next(); } catch (err) { return res.status(401).json({ status: 401, message: 'Error: access_token is not valid' }); } }); // ----------------------------------------------------- // FILTRO AUTOMATICO PER USER // ----------------------------------------------------- server.use((req, res, next) => { if (req.method === 'GET' && req.user) { req.query.user = req.user.name; } next(); }); // ----------------------------------------------------- // SCAN FOTO (PER-UTENTE) // ----------------------------------------------------- server.get('/scan', async (req, res) => { try { resetDB(); const userFolder = `./public/photos/${req.user.name}/original`; await scanPhoto(userFolder, req.user.name); res.send({ status: 'Ricaricato', user: req.user.name, folder: userFolder }); } catch (err) { console.error('Errore scan:', err); res.status(500).json({ error: 'Errore durante lo scan', details: err.message }); } }); // ----------------------------------------------------- // FILE STATICI (hardening path traversal) // ----------------------------------------------------- server.get('/files', (req, res) => { const requested = req.query.file || ''; const publicDir = path.resolve(path.join(__dirname, '../public')); const resolved = path.resolve(publicDir, requested); if (!resolved.startsWith(publicDir)) { return res.status(400).json({ error: 'Invalid path' }); } if (!fs.existsSync(resolved) || fs.statSync(resolved).isDirectory()) { return res.status(404).json({ error: 'Not found' }); } res.sendFile(resolved); }); // ----------------------------------------------------- // RESET DB MANUALE // ----------------------------------------------------- server.get('/initDB', (req, res) => { resetDB(); res.send({ status: 'DB resettato' }); }); // ----------------------------------------------------- // ROUTER JSON-SERVER // ----------------------------------------------------- server.use(router); // ----------------------------------------------------- // START SERVER // ----------------------------------------------------- server.listen(PORT, () => { console.log(`Auth API server running on port ${PORT} ...`); }); // (Opzionale) pulizia periodica denylist per contenere la memoria setInterval(() => { const nowSec = Math.floor(Date.now() / 1000); for (const [tok, exp] of denylist.entries()) { if (exp < nowSec) denylist.delete(tok); } }, 60 * 1000);