255 lines
7.7 KiB
JavaScript
255 lines
7.7 KiB
JavaScript
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);
|