647 lines
19 KiB
JavaScript
647 lines
19 KiB
JavaScript
// 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/<user>/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=[<nome>, '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=<nome>
|
|
// - 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=<nome>"
|
|
});
|
|
}
|
|
}
|
|
|
|
// 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);
|