// server.js require('dotenv').config(); const fs = require('fs'); const fsp = require('fs/promises'); const path = require('path'); const jsonServer = require('json-server'); const jwt = require('jsonwebtoken'); const bcrypt = require('bcrypt'); const scanPhoto = require('./scanner/scanPhoto.js'); const { WEB_ROOT, INDEX_PATH } = require('./config'); 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, pathFull: process.env.PATH_FULL, }); }); // ----------------------------------------------------- // 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(jsonServer.bodyParser); // ----------------------------------------------------- // JWT HELPERS // ----------------------------------------------------- function createToken(payload) { return jwt.sign(payload, SECRET_KEY, { expiresIn: EXPIRES_IN }); } const denylist = new Map(); function addToDenylist(token) { try { const decoded = jwt.decode(token); const exp = decoded?.exp || Math.floor(Date.now() / 1000) + 60; 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; if (exp * 1000 < Date.now()) { denylist.delete(token); return false; } return true; } 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 (utility interna usata da /initDB) // ----------------------------------------------------- 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'); } // ----------------------------------------------------- // Rimuove tutte le directories thumbs se user = Admin altrimenti solo quella dello user (Public/photos//thumbs) // ----------------------------------------------------- async function removeAllThumbs(user) { const photosRoot = path.resolve(__dirname, '../public/photos'); if (!fs.existsSync(photosRoot)) { console.log('Nessuna cartella photos trovata, niente thumbs da cancellare'); return; } // Se NON Γ¨ Admin β†’ cancella solo la sua cartella if (user !== 'Admin') { const thumbsDir = path.join(photosRoot, user, 'thumbs'); try { await fsp.rm(thumbsDir, { recursive: true, force: true }); console.log(`βœ” thumbs rimosse per utente "${user}": ${thumbsDir}`); } catch (err) { console.error(`βœ– errore rimuovendo thumbs per "${user}":`, err); } console.log(`πŸŽ‰ Cancellazione thumbs completata per utente "${user}"`); return; } // Se Γ¨ Admin β†’ cancella TUTTE le thumbs const entries = await fsp.readdir(photosRoot, { withFileTypes: true }); let removed = 0; let total = 0; for (const entry of entries) { if (!entry.isDirectory()) continue; const userDir = path.join(photosRoot, entry.name); const thumbsDir = path.join(userDir, 'thumbs'); total++; try { await fsp.rm(thumbsDir, { recursive: true, force: true }); removed++; console.log(`βœ” thumbs rimossa: ${thumbsDir}`); } catch (err) { console.error(`βœ– errore rimuovendo ${thumbsDir}:`, err); } } // LOG FINALE ADMIN if (removed === total) { console.log(`πŸŽ‰ Tutte le cartelle thumbs (${removed}) sono state cancellate`); } else { console.log(`⚠ Cancellate ${removed} cartelle thumbs su ${total}`); } } // ----------------------------------------------------- // Cancella una foto da index.json usando l'id // ----------------------------------------------------- async function deleteFromIndexById(id) { const indexPath = path.resolve(__dirname, '..', WEB_ROOT, INDEX_PATH); if (!fs.existsSync(indexPath)) { console.log("index.json non trovato"); return false; } const raw = await fsp.readFile(indexPath, 'utf8'); const index = JSON.parse(raw); let deleted = false; for (const user of Object.keys(index)) { const userObj = index[user]; if (!userObj || typeof userObj !== 'object') continue; for (const cartella of Object.keys(userObj)) { const folder = userObj[cartella]; if (!folder || typeof folder !== 'object') continue; if (folder[id]) { delete folder[id]; deleted = true; console.log(`βœ” Eliminato ID ${id} da ${user}/${cartella}`); } } } if (deleted) { await fsp.writeFile(indexPath, JSON.stringify(index, null, 2)); } return deleted; } // ----------------------------------------------------- // Cancella i thumbs usando l'id // ----------------------------------------------------- async function deleteThumbsById(id) { console.log(`\n=== DELETE THUMBS FOR ID: ${id} ===`); const db = router.db; const col = db.get('photos'); const rec = col.find({ id }).value(); if (!rec) { console.log("Record non trovato nel DB β†’ impossibile cancellare thumbs"); return false; } const thumb1 = rec.thub1; const thumb2 = rec.thub2; if (!thumb1 && !thumb2) { console.log("Nessun thumb registrato nel DB"); return false; } // Costruzione corretta del percorso assoluto const absThumb1 = thumb1 ? path.resolve(__dirname, '../public' + thumb1) : null; const absThumb2 = thumb2 ? path.resolve(__dirname, '../public' + thumb2) : null; console.log(`Thumb1 path: ${absThumb1}`); console.log(`Thumb2 path: ${absThumb2}`); let deleted = false; if (absThumb1) { const exists1 = fs.existsSync(absThumb1); console.log(`Thumb1 exists: ${exists1}`); if (exists1) { await fsp.rm(absThumb1, { force: true }); console.log("βœ” Eliminato thumb1"); deleted = true; } else { console.log("βœ– thumb1 NON trovato"); } } if (absThumb2) { const exists2 = fs.existsSync(absThumb2); console.log(`Thumb2 exists: ${exists2}`); if (exists2) { await fsp.rm(absThumb2, { force: true }); console.log("βœ” Eliminato thumb2"); deleted = true; } else { console.log("βœ– thumb2 NON trovato"); } } console.log(`=== FINE DELETE THUMBS ID: ${id} ===\n`); return deleted; } // ----------------------------------------------------- // Elimina uno user dal DB e lo salva // ----------------------------------------------------- function deleteUserRecords(username) { const db = router.db; // lowdb instance const col = db.get('photos'); // Rimuove i record e salva su disco const removed = col.remove({ user: username }).write(); console.log(`Eliminati ${removed.length} record per user "${username}"`); return removed.length; } // ----------------------------------------------------- // 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 // ----------------------------------------------------- server.post('/auth/logout', (req, res) => { const auth = req.headers.authorization || ''; const [scheme, token] = auth.split(' '); if (scheme === 'Bearer' && token) { addToDenylist(token); } return res.status(204).end(); }); // ----------------------------------------------------- // JWT MIDDLEWARE (tutte le rotte tranne /auth/*) // ----------------------------------------------------- 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' }); } if (isRevoked(token)) { return res.status(401).json({ status: 401, message: 'Token revoked' }); } try { const decoded = verifyToken(token); req.user = decoded; next(); } catch (err) { return res .status(401) .json({ status: 401, message: 'Error: access_token is not valid' }); } }); // ----------------------------------------------------- // FILTRO AUTOMATICO PER USER (GET) // - Non-Admin: forzo user=[, 'Common'] cosΓ¬ vedono anche la Common // - Admin: vede tutto senza forzature // ----------------------------------------------------- server.use((req, res, next) => { if (req.method === 'GET' && req.user && req.user.name !== 'Admin') { const u = req.user.name; const q = req.query.user; const base = q ? (Array.isArray(q) ? q : [q]) : []; req.query.user = Array.from(new Set([...base, u, 'Common'])); } next(); }); // ----------------------------------------------------- // SCAN FOTO // - Admin: scansiona tutti gli utenti + Common // - Non-Admin: scansiona solo la propria area (NO Common) // ----------------------------------------------------- server.get('/scanold', async (req, res) => { try { if (req.user && req.user.name === 'Admin') { await scanPhoto(undefined, 'Admin'); return res.send({ status: 'Scansione completata', user: 'Admin', scope: 'tutti gli utenti + Common', }); } // Non-Admin β†’ solo la sua area (niente Common) await scanPhoto(undefined, req.user.name); res.send({ status: 'Scansione completata', user: req.user.name, scope: 'utente corrente', }); } catch (err) { console.error('Errore scan:', err); res.status(500).json({ error: 'Errore durante lo scan', details: err.message }); } }); server.get('/scan', async (req, res) => { try { if (req.user && req.user.name === 'Admin') { await scanPhoto(undefined, 'Admin', router.db); return res.send({ status: 'Scansione completata', user: 'Admin', scope: 'tutti gli utenti + Common', }); } // Non-Admin β†’ solo la sua area (niente Common) await scanPhoto(undefined, req.user.name, router.db); res.send({ status: 'Scansione completata', user: req.user.name, scope: 'utente corrente', }); } catch (err) { console.error('Errore scan:', err); res.status(500).json({ error: 'Errore durante lo scan', details: err.message }); } }); // ----------------------------------------------------- // FILE STATICI // ----------------------------------------------------- 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 + rimozione index.json // ----------------------------------------------------- server.get('/initDB', async (req, res) => { try { resetDB(); // Rimuove index.json const absIndexPath = path.resolve(__dirname, '..', WEB_ROOT, INDEX_PATH); try { await fsp.unlink(absIndexPath); console.log('initDB: index.json rimosso ->', absIndexPath); } catch (err) { if (err.code === 'ENOENT') { console.log('initDB: index.json non trovato:', absIndexPath); } else { console.error('initDB: errore cancellando index.json:', err); } } // rimuove tutte le cartelle thumbs await removeAllThumbs('Admin'); res.json({ status: 'DB resettato', indexRemoved: true, thumbsRemoved: true, indexPath: absIndexPath }); } catch (err) { console.error('initDB: errore generale:', err); res.status(500).json({ status: 'errore', message: err.message }); } }); // ----------------------------------------------------- // DELETE FOTO da DB + index.json + thumbs // ----------------------------------------------------- server.delete('/delphoto/:id', async (req, res) => { const { id } = req.params; try { const db = router.db; const col = db.get('photos'); const existing = col.find({ id }).value(); // 1) Cancella thumbs PRIMA DI TUTTO const deletedThumbs = await deleteThumbsById(id); let deletedDB = false; // 2) Cancella dal DB if (existing) { col.remove({ id }).write(); deletedDB = true; console.log(`DELPHOTO β†’ foto cancellata dal DB: ${id}`); } else { console.log(`DELPHOTO β†’ foto NON trovata nel DB: ${id}`); } // 3) Cancella da index.json const deletedIndex = await deleteFromIndexById(id); return res.json({ ok: true, id, deletedThumbs, deletedDB, deletedIndex }); } catch (err) { console.error('DELPHOTO errore:', err); return res.status(500).json({ ok: false, error: err.message }); } }); // ----------------------------------------------------- // RESET DB SOLO PER UN UTENTE // - Admin: deve specificare ?user= // - Non-Admin: cancella solo i propri dati // ----------------------------------------------------- server.get('/initDBuser', async (req, res) => { try { let targetUser = req.user.name; // Admin puΓ² specificare chi cancellare if (req.user.name === 'Admin') { targetUser = req.query.user; if (!targetUser) { return res.status(400).json({ error: "Admin deve specificare ?user=" }); } } // 1) Cancella record DB const deleted = deleteUserRecords(targetUser); // 2) Cancella thumbs await removeAllThumbs(targetUser); res.json({ status: "OK", user: targetUser, deletedRecords: deleted, thumbsRemoved: true }); } catch (err) { console.error("initDBuser errore:", err); res.status(500).json({ error: "Errore durante initDBuser", details: err.message }); } }); // ----------------------------------------------------- // FIND ID IN INDEX.JSON + RETURN RECORD (SOLO LETTURA) // ----------------------------------------------------- server.get('/findIdIndex/:id', async (req, res) => { const { id } = req.params; try { const indexPath = path.resolve(__dirname, '..', WEB_ROOT, INDEX_PATH); if (!fs.existsSync(indexPath)) { return res.json({ ok: false, found: false, message: "index.json non trovato" }); } const raw = await fsp.readFile(indexPath, 'utf8'); const index = JSON.parse(raw); for (const user of Object.keys(index)) { const userObj = index[user]; if (!userObj || typeof userObj !== 'object') continue; for (const cartella of Object.keys(userObj)) { const folder = userObj[cartella]; if (!folder || typeof folder !== 'object') continue; // dentro la cartella: chiavi = id, piΓΉ _folderHash if (folder[id]) { return res.json({ ok: true, found: true, user, cartella, record: folder[id] }); } } } return res.json({ ok: true, found: false }); } catch (err) { console.error("Errore findIdIndex:", err); return res.status(500).json({ ok: false, error: err.message }); } }); // ----------------------------------------------------- // UPSERT anti-duplicato per /photos (prima del router) // Se id esiste -> aggiorna; altrimenti crea // ----------------------------------------------------- server.post('/photos', (req, res, next) => { try { const id = req.body && req.body.id; if (!id) return next(); const db = router.db; // lowdb instance const col = db.get('photos'); const existing = col.find({ id }).value(); if (existing) { col.find({ id }).assign(req.body).write(); return res.status(200).json(req.body); } return next(); // non esiste: crea con il router } catch (e) { console.error('UPSERT /photos error:', e); return res.status(500).json({ error: 'upsert failed' }); } }); // ----------------------------------------------------- // ROUTER JSON-SERVER // ----------------------------------------------------- server.use(router); // ----------------------------------------------------- // START SERVER // ----------------------------------------------------- server.listen(PORT, () => { console.log(`Auth API server running on port ${PORT} ...`); }); // Pulizia denylist setInterval(() => { const nowSec = Math.floor(Date.now() / 1000); for (const [tok, exp] of denylist.entries()) { if (exp < nowSec) denylist.delete(tok); } }, 60 * 1000);