// server.js require('dotenv').config(); const fs = require('fs'); const path = require('path'); const express = require('express'); const jwt = require('jsonwebtoken'); const bcrypt = require('bcrypt'); const scanPhoto = require('./api_v1/scanner/scanPhoto.js'); const { handleAddFile, handleDeleteFile, handleAddDir, handleDeleteDir } = require('./api_v1/scanner/scanAuto.js'); const { WEB_ROOT } = require('./api_v1/config'); const config = require('./api_v1/config'); const db = require('./db/knex'); // Funzioni di cleanup (deleteThumbsById, deleteFromDB) const createCleanupFunctions = require('./api_v1/scanner/orphanCleanup'); const { deleteThumbsById, deleteFromDB } = createCleanupFunctions(db); // recordChange serve per registrare "removed" const recordChange = require('./api_v1/scanner/recordChange'); const photosRouter = require('./routes/photos'); const wss = require('./ws-server'); const scanFile = require('./api_v1/scanner/scanFileEntry'); const scanPhotoSingle = require('./api_v1/scanner/scanPhotoSingle'); const scanPhotosUser = require('./api_v1/scanner/scanPhotosUser'); const createDeleteFolderFunctions = require('./api_v1/scanner/deleteFolder'); const { deleteFolderForUser } = createDeleteFolderFunctions(db); 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 = express(); // STATIC server.use(express.static(path.join(__dirname, 'public'))); // BODY PARSER server.use(express.json()); server.use(express.urlencoded({ extended: true })); // CONFIG PUBBLICO server.get('/config', (req, res) => { res.json({ baseUrl: config.BASE_URL, pathFull: config.PATH_FULL, galleryRefreshSeconds: config.GALLERY_REFRESH_SECONDS }); }); // USERS DB const userdb = JSON.parse(fs.readFileSync('./api_v1/users.json', 'utf-8')); // JWT 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 relPath2Cartella(p) { const parts = p.split("/").filter(Boolean); return parts.slice(3).join("/"); } // HOME server.get('/', (req, res) => { res.sendFile(path.resolve('public/index.html')); }); // LOGIN server.post('/auth/login', async (req, res) => { const { email, password } = req.body; const user = userdb.users.find((u) => u.email === email); if (!user) { return res.status(401).json({ status: 401, message: 'Incorrect email or password' }); } const ok = await bcrypt.compare(password, user.password); if (!ok) { 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 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' }); } }); // ⭐ MIDDLEWARE GET — versione modulare server.use((req, res, next) => { if (req.method === 'GET' && req.user) { // Admin non viene toccato if (req.user.name === 'Admin') { return next(); } // NON applicare a /scan if (req.path.startsWith('/scan')) { req.query.user = [req.user.name]; return next(); } // Per tutte le altre GET (lettura) req.query.user = [req.user.name, 'Common']; } next(); }); // ⭐ SCAN FOTO — Admin scansiona tutti server.get('/scan', async (req, res) => { try { if (req.user && req.user.name === 'Admin') { // Admin scansiona Common await scanPhotosUser('Common', db); // Admin scansiona tutti gli utenti del file users.json for (const u of userdb.users) { await scanPhotosUser(u.name, db); } return res.send({ status: 'Scansione completata', user: 'Admin', scope: 'tutti gli utenti + Common', }); } // Utente normale → solo se stesso await scanPhotosUser(req.user.name, 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 }); } }); // scan_auto new server.post('/api_v1/auto_scan', async (req, res) => { const { type, file, path: relPath, user } = req.body; const cart = relPath2Cartella(relPath); const absFile = path.join(__dirname, WEB_ROOT, relPath, file); /*console.log("type: ", type); console.log("file: ", file); console.log("relPath: ", relPath); console.log("user: ", user); console.log("cart: ", cart); console.log("absFile: ", absFile); return res.json({ status: 'OK', action: 'DEL_DIR', folder: "prova" }); */ try { switch (type) { // ------------------------- // ADD_DIR // ------------------------- case 'ADD_DIR': { const { scanNewCartella } = require('./api_v1/scanner/scanNewCartella'); const result = await scanNewCartella(user, file, db); wss.broadcastToUser(user, { type: "add_dir", folder: file }); return res.json({ status: 'OK', action: 'ADD_DIR', result }); } // ------------------------- // DEL (file) // ------------------------- case 'DEL': { const e = await scanFile(user, cart, absFile); await recordChange(db, e.id, user, "removed"); await deleteThumbsById(e.id); await deleteFromDB(e.id, user); wss.broadcastToUser(user, { type: "removed", id: e.id }); return res.json({ status: 'OK', action: 'DEL', id: e.id }); } // ------------------------- // ADD (file) // ------------------------- case 'ADD': { const f = await scanFile(user, cart, absFile); const newFiles = []; await scanPhotoSingle(db, user, cart, f, newFiles); wss.broadcastToUser(user, { type: "added", id: f.id }); return res.json({ status: 'OK', action: 'ADD', id: f.id }); } // ------------------------- // DEL_DIR // ------------------------- case 'DEL_DIR': { await deleteFolderForUser(user, file); wss.broadcastToUser(user, { type: "del_dir", folder: file }); return res.json({ status: 'OK', action: 'DEL_DIR', folder: cart }); } default: return res.status(400).json({ error: 'Tipo non valido' }); } } catch (err) { console.error('Errore scan_auto:', err); res.status(500).json({ error: 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 server.get('/initDB', async (req, res) => { try { await db('photos').del(); res.json({ status: 'DB resettato', indexRemoved: false }); } catch (err) { console.error('initDB: errore generale:', err); res.status(500).json({ status: 'errore', message: err.message }); } }); // DELETE FOTO da DB + thumbs server.delete('/delphoto/:id', async (req, res) => { const { id } = req.params; const trx = await db.transaction(); try { const createCleanupFunctions = require('./api_v1/scanner/orphanCleanup'); const { deleteThumbsById } = createCleanupFunctions(db); const deletedThumbs = await deleteThumbsById(id); const deletedDB = await trx('photos').where({ id }).del(); await trx.commit(); return res.json({ ok: true, id, deletedThumbs, deletedDB: deletedDB > 0, deletedIndex: false }); } catch (err) { await trx.rollback(); console.error('DELPHOTO errore:', err); return res.status(500).json({ ok: false, error: err.message }); } }); // RESET DB SOLO PER UN UTENTE server.get('/initDBuser', async (req, res) => { try { let targetUser = req.user.name; if (req.user.name === 'Admin') { targetUser = req.query.user; if (!targetUser) { return res.status(400).json({ error: "Admin deve specificare ?user=" }); } } const trx = await db.transaction(); const deleted = await trx('photos').where({ user: targetUser }).del(); await trx.commit(); res.json({ status: "OK", user: targetUser, deletedRecords: deleted }); } catch (err) { console.error("initDBuser errore:", err); res.status(500).json({ error: "Errore durante initDBuser", details: err.message }); } }); // ROUTER PHOTOS (SQLite) server.use('/photos', photosRouter); // 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);