427 lines
No EOL
10 KiB
JavaScript
427 lines
No EOL
10 KiB
JavaScript
// 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=<nome>"
|
|
});
|
|
}
|
|
}
|
|
|
|
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); |