Compare commits
10 Commits
3efed93cbb
...
14c29b1434
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14c29b1434 | ||
|
|
473eb9015f | ||
|
|
f785fbedca | ||
|
|
e65f2ba3a0 | ||
|
|
98eefcacdc | ||
|
|
a07abbfeea | ||
|
|
07673586c2 | ||
|
|
dd19b33f35 | ||
|
|
3cd5a84cc1 | ||
|
|
0f511c2cf9 |
@@ -76,6 +76,9 @@ app.use('/storage', storageRoutes)
|
|||||||
const paramsRoutes = require('./routes/params')
|
const paramsRoutes = require('./routes/params')
|
||||||
app.use('/params', paramsRoutes)
|
app.use('/params', paramsRoutes)
|
||||||
|
|
||||||
|
const settingsRoutes = require('./routes/settings')
|
||||||
|
app.use('/settings', settingsRoutes)
|
||||||
|
|
||||||
// Avvio del server
|
// Avvio del server
|
||||||
app.listen(PORT, '0.0.0.0', () => {
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
console.log(`Started on port ${PORT}`);
|
console.log(`Started on port ${PORT}`);
|
||||||
|
|||||||
9
api/src/routes/settings.js
Normal file
9
api/src/routes/settings.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// api.mebboat.it/settings
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
24
auth/.env.example
Normal file
24
auth/.env.example
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
DB_USER=
|
||||||
|
DB_HOST=
|
||||||
|
DB_PASSWORD=
|
||||||
|
DB_PORT=
|
||||||
|
|
||||||
|
DATA_DB=
|
||||||
|
|
||||||
|
# In locale: lasciare vuoto (il cookie su localhost funziona su tutte le porte)
|
||||||
|
# In produzione: .mebboat.it (condiviso tra auth.mebboat.it, console.mebboat.it, api.mebboat.it)
|
||||||
|
COOKIE_DOMAIN=
|
||||||
|
COOKIE_NAME=
|
||||||
|
|
||||||
|
DB_NAME=
|
||||||
|
|
||||||
|
PORT=3006
|
||||||
|
|
||||||
|
JWT_SECRET=
|
||||||
|
JWT_EXPIRES_IN=7d
|
||||||
|
|
||||||
|
CONSOLE_URL=
|
||||||
|
|
||||||
|
VERSION=1.4.0
|
||||||
|
VERSION_BUILD=1.0
|
||||||
|
VERSION_STATE=pre-release
|
||||||
@@ -12,15 +12,76 @@ const version = process.env.VERSION;
|
|||||||
const vBuild = process.env.VERSION_BUILD;
|
const vBuild = process.env.VERSION_BUILD;
|
||||||
const vState = process.env.VERSION_STATE;
|
const vState = process.env.VERSION_STATE;
|
||||||
|
|
||||||
app.use(express.json());
|
// ─── SICUREZZA GLOBALE ──────────────────────────────────────────────
|
||||||
app.use(express.urlencoded({ extended: true }));
|
|
||||||
|
// Trust proxy (necessario dietro Nginx/Traefik per avere il vero IP del client)
|
||||||
|
app.set('trust proxy', 1);
|
||||||
|
|
||||||
|
// Rate Limiter in-memory (protezione DoS base, senza dipendenze esterne)
|
||||||
|
const rateLimitStore = new Map();
|
||||||
|
const RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minuto
|
||||||
|
const RATE_LIMIT_MAX = 100; // max richieste per finestra
|
||||||
|
const RATE_LIMIT_AUTH_MAX = 10; // max tentativi login/register per finestra
|
||||||
|
|
||||||
|
// Pulizia periodica dello store
|
||||||
|
setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [key, entry] of rateLimitStore) {
|
||||||
|
if (now - entry.start > RATE_LIMIT_WINDOW_MS) {
|
||||||
|
rateLimitStore.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, RATE_LIMIT_WINDOW_MS);
|
||||||
|
|
||||||
|
function createRateLimiter(maxRequests) {
|
||||||
|
return (req, res, next) => {
|
||||||
|
const key = `${req.ip}:${maxRequests}`;
|
||||||
|
const now = Date.now();
|
||||||
|
const entry = rateLimitStore.get(key);
|
||||||
|
|
||||||
|
if (!entry || now - entry.start > RATE_LIMIT_WINDOW_MS) {
|
||||||
|
rateLimitStore.set(key, { count: 1, start: now });
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.count++;
|
||||||
|
if (entry.count > maxRequests) {
|
||||||
|
res.set('Retry-After', Math.ceil((RATE_LIMIT_WINDOW_MS - (now - entry.start)) / 1000));
|
||||||
|
return res.status(429).json({ error: 'Troppe richieste, riprova più tardi' });
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const globalRateLimit = createRateLimiter(RATE_LIMIT_MAX);
|
||||||
|
const authRateLimit = createRateLimiter(RATE_LIMIT_AUTH_MAX);
|
||||||
|
|
||||||
|
// Security headers (equivalente leggero di Helmet, senza dipendenze)
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||||
|
res.setHeader('X-Frame-Options', 'DENY');
|
||||||
|
res.setHeader('X-XSS-Protection', '0'); // Disabilitato a favore di CSP
|
||||||
|
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||||
|
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
||||||
|
// Rimuovi header che rivelano info sul server
|
||||||
|
res.removeHeader('X-Powered-By');
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rate limit globale
|
||||||
|
app.use(globalRateLimit);
|
||||||
|
|
||||||
|
// Limiti dimensione body per prevenire payload eccessivi
|
||||||
|
app.use(express.json({ limit: '16kb' }));
|
||||||
|
app.use(express.urlencoded({ extended: true, limit: '16kb' }));
|
||||||
app.use(parser());
|
app.use(parser());
|
||||||
|
|
||||||
// Static files
|
// ─── STATIC FILES ───────────────────────────────────────────────────
|
||||||
const staticFolder = path.join(__dirname, 'static');
|
const staticFolder = path.join(__dirname, 'static');
|
||||||
app.use('/static', express.static(staticFolder));
|
app.use('/static', express.static(staticFolder));
|
||||||
|
|
||||||
// Nunjucks templates
|
// ─── NUNJUCKS TEMPLATES ─────────────────────────────────────────────
|
||||||
const templatesFolder = path.join(__dirname, 'templates');
|
const templatesFolder = path.join(__dirname, 'templates');
|
||||||
nunjucks.configure(templatesFolder, {
|
nunjucks.configure(templatesFolder, {
|
||||||
autoescape: true,
|
autoescape: true,
|
||||||
@@ -31,10 +92,24 @@ nunjucks.configure(templatesFolder, {
|
|||||||
app.set('views', templatesFolder);
|
app.set('views', templatesFolder);
|
||||||
app.set('view engine', 'html');
|
app.set('view engine', 'html');
|
||||||
|
|
||||||
// Routes
|
// ─── ROUTES ─────────────────────────────────────────────────────────
|
||||||
const authRoutes = require('./routes/auth');
|
|
||||||
app.use('/', authRoutes);
|
|
||||||
|
|
||||||
|
// Views (pagine HTML)
|
||||||
|
app.use('/', require('./routes/views/auth'));
|
||||||
|
app.use('/sessions', require('./routes/views/sessions'));
|
||||||
|
app.use('/user', require('./routes/views/user'));
|
||||||
|
|
||||||
|
// API - Auth (con rate limit più stretto su login/register)
|
||||||
|
const authRoutes = require('./routes/auth');
|
||||||
|
app.use('/api/auth/login', authRateLimit);
|
||||||
|
app.use('/api/auth/register', authRateLimit);
|
||||||
|
app.use('/api/auth', authRoutes);
|
||||||
|
|
||||||
|
// API - Risorse
|
||||||
|
app.use('/api/users', require('./routes/users'));
|
||||||
|
app.use('/api/sessions', require('./routes/sessions'));
|
||||||
|
|
||||||
|
// ─── HEALTH CHECK ───────────────────────────────────────────────────
|
||||||
app.get('/health', async (req, res) => {
|
app.get('/health', async (req, res) => {
|
||||||
const dbConnected = await database.checkPostgres();
|
const dbConnected = await database.checkPostgres();
|
||||||
const redisHelper = require('./storage/redis');
|
const redisHelper = require('./storage/redis');
|
||||||
@@ -51,7 +126,18 @@ app.get('/health', async (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Startup
|
// ─── 404 HANDLER ────────────────────────────────────────────────────
|
||||||
|
app.use((req, res) => {
|
||||||
|
res.status(404).json({ error: 'Risorsa non trovata' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── ERROR HANDLER GLOBALE ──────────────────────────────────────────
|
||||||
|
app.use((err, req, res, _next) => {
|
||||||
|
console.error('[AUTH] Errore non gestito:', err);
|
||||||
|
res.status(500).json({ error: 'Errore interno del server' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── STARTUP ────────────────────────────────────────────────────────
|
||||||
async function start() {
|
async function start() {
|
||||||
await database.initDb();
|
await database.initDb();
|
||||||
app.listen(PORT, '0.0.0.0', () => {
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
|
|||||||
42
auth/src/middlewares/internal.security.js
Normal file
42
auth/src/middlewares/internal.security.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const API_KEY = process.env.INTERNAL_API_KEY;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware di autenticazione per servizi interni (container-to-container).
|
||||||
|
* Verifica l'header 'x-internal-api-key' contro INTERNAL_API_KEY nell'env.
|
||||||
|
*
|
||||||
|
* SICUREZZA:
|
||||||
|
* - Se INTERNAL_API_KEY non è configurata, TUTTE le richieste vengono rifiutate
|
||||||
|
* - Usa timingSafeEqual per prevenire attacchi timing side-channel
|
||||||
|
*/
|
||||||
|
const internalAuth = (req, res, next) => {
|
||||||
|
// Se la chiave non è configurata nel server, blocca tutto
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error('[SECURITY] INTERNAL_API_KEY absent! All internal requests blocked.');
|
||||||
|
return res.status(503).json({ error: 'Service not configured correctly' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const internalToken = req.headers['x-internal-api-key'];
|
||||||
|
|
||||||
|
if (!internalToken || typeof internalToken !== 'string') {
|
||||||
|
return res.status(403).json({ error: 'unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confronto timing-safe per prevenire timing attacks
|
||||||
|
try {
|
||||||
|
const tokenBuffer = Buffer.from(internalToken, 'utf8');
|
||||||
|
const keyBuffer = Buffer.from(API_KEY, 'utf8');
|
||||||
|
|
||||||
|
if (tokenBuffer.length !== keyBuffer.length || !crypto.timingSafeEqual(tokenBuffer, keyBuffer)) {
|
||||||
|
return res.status(403).json({ error: 'Accesso negato' });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return res.status(403).json({ error: 'Accesso negato' });
|
||||||
|
}
|
||||||
|
|
||||||
|
req.user = { id: 'system', role: 'internal_service' };
|
||||||
|
return next();
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = internalAuth;
|
||||||
33
auth/src/middlewares/user.security.js
Normal file
33
auth/src/middlewares/user.security.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const jwt = require('../tools/jwt');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware di autenticazione per utenti finali.
|
||||||
|
* Verifica il JWT dal cookie 'auth_token' o dall'header 'Authorization: Bearer <token>'.
|
||||||
|
*
|
||||||
|
* Se valido, inietta req.user con { user_id, username, session_id }.
|
||||||
|
*/
|
||||||
|
const userAuth = (req, res, next) => {
|
||||||
|
const token = (req.cookies && req.cookies.auth_token) || jwt.getToken(req.headers['authorization']);
|
||||||
|
|
||||||
|
if (!token || typeof token !== 'string') {
|
||||||
|
return res.status(401).json({ error: 'Accesso negato: token mancante' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limite ragionevole sulla lunghezza del token per evitare abusi
|
||||||
|
if (token.length > 2048) {
|
||||||
|
return res.status(400).json({ error: 'Token non valido' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const verified = jwt.verifyToken(token);
|
||||||
|
if (!verified.valid) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: 'Sessione non valida o scaduta',
|
||||||
|
reason: verified.reason
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
req.user = verified.payload;
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = userAuth;
|
||||||
@@ -2,22 +2,13 @@ const router = require('express').Router();
|
|||||||
const auth = require('../core/auth.core');
|
const auth = require('../core/auth.core');
|
||||||
const jwt = require('../tools/jwt');
|
const jwt = require('../tools/jwt');
|
||||||
|
|
||||||
const version = process.env.VERSION;
|
|
||||||
const vBuild = process.env.VERSION_BUILD;
|
|
||||||
const vState = process.env.VERSION_STATE;
|
|
||||||
|
|
||||||
const CONSOLE_URL = process.env.CONSOLE_URL || 'http://localhost:3004';
|
const CONSOLE_URL = process.env.CONSOLE_URL || 'http://localhost:3004';
|
||||||
const COOKIE_DOMAIN = process.env.COOKIE_DOMAIN || undefined;
|
const COOKIE_DOMAIN = process.env.COOKIE_DOMAIN || undefined;
|
||||||
|
|
||||||
router.get('/health', (req, res) => {
|
// Validazione input
|
||||||
res.json({
|
const USERNAME_REGEX = /^[a-zA-Z0-9_.\-]{3,50}$/;
|
||||||
status: 'ok',
|
const PASSWORD_MIN_LENGTH = 8;
|
||||||
service: 'auth',
|
const PASSWORD_MAX_LENGTH = 128;
|
||||||
version: version,
|
|
||||||
build_number: vBuild,
|
|
||||||
version_state: vState
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/register', async (req, res) => {
|
router.post('/register', async (req, res) => {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
@@ -26,24 +17,59 @@ router.post('/register', async (req, res) => {
|
|||||||
return res.status(400).json({ error: 'Username e password richiesti' });
|
return res.status(400).json({ error: 'Username e password richiesti' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof username !== 'string' || typeof password !== 'string') {
|
||||||
|
return res.status(400).json({ error: 'Formato dati non valido' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!USERNAME_REGEX.test(username)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Username non valido. 3-50 caratteri alfanumerici, underscore, punto o trattino.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < PASSWORD_MIN_LENGTH || password.length > PASSWORD_MAX_LENGTH) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: `Password deve essere tra ${PASSWORD_MIN_LENGTH} e ${PASSWORD_MAX_LENGTH} caratteri`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await auth.register(username, password);
|
await auth.register(username, password);
|
||||||
res.status(201).end();
|
res.status(201).end();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[AUTH] Register failed:', err.message);
|
console.error('[AUTH] Register failed:', err.message);
|
||||||
const status = err.message === 'User already exists' ? 409 : 500;
|
const status = err.message === 'User already exists' ? 409 : 500;
|
||||||
res.status(status).json({ error: err.message });
|
res.status(status).json({ error: err.message === 'User already exists' ? err.message : 'Errore interno' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/login', (req, res) => {
|
|
||||||
const redirect = req.query.redirect || '';
|
|
||||||
res.render('loginpage', { error: null, redirect });
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/login', async (req, res) => {
|
router.post('/login', async (req, res) => {
|
||||||
const { username, password, redirect } = req.body;
|
const { username, password, redirect } = req.body;
|
||||||
|
|
||||||
|
// Validazione base
|
||||||
|
if (!username || !password || typeof username !== 'string' || typeof password !== 'string') {
|
||||||
|
return res.render('loginpage', { error: 'Credenziali non valide', redirect: redirect || '' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limiti di lunghezza per prevenire abuse
|
||||||
|
if (username.length > 50 || password.length > PASSWORD_MAX_LENGTH) {
|
||||||
|
return res.render('loginpage', { error: 'Credenziali non valide', redirect: redirect || '' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validazione redirect URL per prevenire open redirect attacks
|
||||||
|
if (redirect && typeof redirect === 'string') {
|
||||||
|
try {
|
||||||
|
const redirectUrl = new URL(redirect);
|
||||||
|
const consoleUrl = new URL(CONSOLE_URL);
|
||||||
|
// Permetti redirect solo allo stesso dominio del CONSOLE_URL
|
||||||
|
if (redirectUrl.hostname !== consoleUrl.hostname) {
|
||||||
|
return res.render('loginpage', { error: 'Redirect non autorizzato', redirect: '' });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// URL relativo o non valido — ignora il redirect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await auth.login(username, password);
|
const user = await auth.login(username, password);
|
||||||
const session = await auth.newSession(user.id, req.headers['user-agent'], req.ip);
|
const session = await auth.newSession(user.id, req.headers['user-agent'], req.ip);
|
||||||
@@ -53,7 +79,7 @@ router.post('/login', async (req, res) => {
|
|||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: process.env.NODE_ENV === 'production',
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 giorni
|
maxAge: 7 * 24 * 60 * 60 * 1000
|
||||||
};
|
};
|
||||||
|
|
||||||
if (COOKIE_DOMAIN) {
|
if (COOKIE_DOMAIN) {
|
||||||
@@ -62,11 +88,11 @@ router.post('/login', async (req, res) => {
|
|||||||
|
|
||||||
res.cookie('auth_token', token, cookieOptions);
|
res.cookie('auth_token', token, cookieOptions);
|
||||||
|
|
||||||
// Redirect alla pagina da cui l'utente e' arrivato, o alla console
|
|
||||||
const destination = redirect || CONSOLE_URL;
|
const destination = redirect || CONSOLE_URL;
|
||||||
res.redirect(destination);
|
res.redirect(destination);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[AUTH] Login failed:', err.message, err.stack);
|
console.error('[AUTH] Login failed:', err.message);
|
||||||
|
// Mai rivelare se è l'utente o la password ad essere sbagliati
|
||||||
res.render('loginpage', { error: 'Credenziali non valide', redirect: redirect || '' });
|
res.render('loginpage', { error: 'Credenziali non valide', redirect: redirect || '' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// api.mebboat.it/api/sessions
|
||||||
|
|
||||||
|
const router = require('express').Router();
|
||||||
|
const { query } = require('../storage/database');
|
||||||
|
const userAuth = require('../middlewares/user.security');
|
||||||
|
|
||||||
|
router.use(userAuth);
|
||||||
|
|
||||||
|
// Mostra SOLO le sessioni dell'utente autenticato (non di tutti!)
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await query(
|
||||||
|
`SELECT id, ip_address, browser, os, device_type,
|
||||||
|
location_country, location_city, created_at, last_active, is_revoked
|
||||||
|
FROM sessions
|
||||||
|
WHERE user_id = $1
|
||||||
|
ORDER BY last_active DESC`,
|
||||||
|
[req.user.user_id]
|
||||||
|
);
|
||||||
|
res.json(result.rows);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[SESSIONS] Errore recupero sessioni:', err);
|
||||||
|
res.status(500).json({ error: 'Errore interno del server' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Revoca una sessione specifica dell'utente
|
||||||
|
router.delete('/:sessionId', async (req, res) => {
|
||||||
|
const { sessionId } = req.params;
|
||||||
|
|
||||||
|
// Validazione UUID
|
||||||
|
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||||
|
if (!UUID_REGEX.test(sessionId)) {
|
||||||
|
return res.status(400).json({ error: 'ID sessione non valido' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Verifica che la sessione appartenga all'utente autenticato
|
||||||
|
const result = await query(
|
||||||
|
'UPDATE sessions SET is_revoked = TRUE WHERE id = $1 AND user_id = $2 AND is_revoked = FALSE',
|
||||||
|
[sessionId, req.user.user_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rowCount === 0) {
|
||||||
|
return res.status(404).json({ error: 'Sessione non trovata o già revocata' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'Sessione revocata' });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[SESSIONS] Errore revoca sessione:', err);
|
||||||
|
res.status(500).json({ error: 'Errore interno del server' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -1,2 +1,123 @@
|
|||||||
const router = require('express').Router();
|
// api.mebboat.it/api/users
|
||||||
|
|
||||||
|
const router = require('express').Router();
|
||||||
|
const { query } = require('../storage/database');
|
||||||
|
const userAuth = require('../middlewares/user.security');
|
||||||
|
const internalAuth = require('../middlewares/internal.security');
|
||||||
|
|
||||||
|
// ─── VALIDAZIONE INPUT ──────────────────────────────────────────────
|
||||||
|
const USERNAME_REGEX = /^[a-zA-Z0-9_.\-]{3,50}$/;
|
||||||
|
const TELEGRAM_ID_REGEX = /^[0-9]{5,15}$/;
|
||||||
|
|
||||||
|
// ─── ROTTE INTERNAL (prima del router.use userAuth) ─────────────────
|
||||||
|
|
||||||
|
router.get('/', internalAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await query(
|
||||||
|
'SELECT id, username, is_active, created_at, telegram_id FROM users'
|
||||||
|
);
|
||||||
|
res.json(result.rows);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[USERS] Errore lista utenti:', err);
|
||||||
|
res.status(500).json({ error: 'Errore interno del server' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/tonotify', internalAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await query(
|
||||||
|
'SELECT telegram_id FROM users WHERE telegram_id IS NOT NULL'
|
||||||
|
);
|
||||||
|
res.json(result.rows);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[USERS] Errore lista notifiche:', err);
|
||||||
|
res.status(500).json({ error: 'Errore interno del server' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── ROTTE USER (tutte le rotte sotto usano userAuth) ───────────────
|
||||||
|
|
||||||
|
router.use(userAuth);
|
||||||
|
|
||||||
|
router.get('/me', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await query(
|
||||||
|
'SELECT id, username, is_active, created_at, telegram_id FROM users WHERE id = $1',
|
||||||
|
[req.user.user_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'Utente non trovato' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(result.rows[0]);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[USERS] Errore recupero utente:', err);
|
||||||
|
res.status(500).json({ error: 'Errore interno del server' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/me/username', async (req, res) => {
|
||||||
|
const newUsername = req.query.newUsername || req.body?.newUsername;
|
||||||
|
|
||||||
|
if (!newUsername || typeof newUsername !== 'string') {
|
||||||
|
return res.status(400).json({ error: 'Nuovo username richiesto' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validazione formato username
|
||||||
|
if (!USERNAME_REGEX.test(newUsername)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Username non valido. Deve contenere 3-50 caratteri alfanumerici, underscore, punto o trattino.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await query(
|
||||||
|
'UPDATE users SET username = $1 WHERE id = $2 RETURNING username',
|
||||||
|
[newUsername, req.user.user_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rowCount === 0) {
|
||||||
|
return res.status(404).json({ error: 'Utente non trovato' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, username: result.rows[0].username });
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === '23505') {
|
||||||
|
return res.status(409).json({ error: 'Questo username è già in uso' });
|
||||||
|
}
|
||||||
|
console.error('[USERS] Errore aggiornamento username:', err);
|
||||||
|
res.status(500).json({ error: 'Errore interno del server' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/me/telegram', async (req, res) => {
|
||||||
|
const telegramId = req.query.telegramId || req.body?.telegramId;
|
||||||
|
|
||||||
|
if (!telegramId || typeof telegramId !== 'string') {
|
||||||
|
return res.status(400).json({ error: 'Telegram ID richiesto' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validazione formato Telegram ID (solo numeri, 5-15 cifre)
|
||||||
|
if (!TELEGRAM_ID_REGEX.test(telegramId)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Telegram ID non valido. Deve contenere solo numeri (5-15 cifre).'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await query(
|
||||||
|
'UPDATE users SET telegram_id = $1 WHERE id = $2',
|
||||||
|
[telegramId, req.user.user_id]
|
||||||
|
);
|
||||||
|
res.json({ success: true, message: 'Telegram ID aggiornato' });
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === '23505') {
|
||||||
|
return res.status(409).json({ error: 'Questo Telegram ID è già associato a un altro account' });
|
||||||
|
}
|
||||||
|
console.error('[USERS] Errore aggiornamento telegram:', err);
|
||||||
|
res.status(500).json({ error: 'Errore interno del server' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|||||||
8
auth/src/routes/views/auth.js
Normal file
8
auth/src/routes/views/auth.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
const router = require('express').Router();
|
||||||
|
|
||||||
|
router.get('/login', (req, res) => {
|
||||||
|
const redirect = req.query.redirect || '';
|
||||||
|
res.render('loginpage', { error: null, redirect });
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
10
auth/src/routes/views/sessions.js
Normal file
10
auth/src/routes/views/sessions.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const router = require('express').Router();
|
||||||
|
const userAuth = require('../../middlewares/user.security');
|
||||||
|
|
||||||
|
router.use(userAuth);
|
||||||
|
|
||||||
|
router.get('/', (req, res) => {
|
||||||
|
res.render('sessions');
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
7
auth/src/routes/views/user.js
Normal file
7
auth/src/routes/views/user.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const router = require('express').Router();
|
||||||
|
|
||||||
|
router.get('/', (req, res) => {
|
||||||
|
res.render('user');
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
12
console/.env.example
Normal file
12
console/.env.example
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
PORT=3004
|
||||||
|
|
||||||
|
VERSION=1.3.0
|
||||||
|
VERSION_BUILD=1.0
|
||||||
|
VERSION_STATE=pre-release
|
||||||
|
|
||||||
|
REALTIME_URL=
|
||||||
|
REALTIME_WS_URL=
|
||||||
|
|
||||||
|
JWT_SECRET=
|
||||||
|
AUTH_LOGIN_URL=
|
||||||
|
COOKIE_DOMAIN=
|
||||||
@@ -2,12 +2,12 @@ const { Pool } = require('pg');
|
|||||||
const { hash, generateShortId } = require('./cryptoUtils');
|
const { hash, generateShortId } = require('./cryptoUtils');
|
||||||
|
|
||||||
const pool = new Pool({
|
const pool = new Pool({
|
||||||
user: process.env.POSTGRES_USER,
|
user: process.env.DB_USER,
|
||||||
host: process.env.POSTGRES_HOST,
|
host: process.env.DB_HOST,
|
||||||
database: process.env.POSTGRES_NAME,
|
database: process.env.DB_NAME,
|
||||||
password: process.env.POSTGRES_PASSWORD,
|
password: process.env.DB_PASSWORD,
|
||||||
port: process.env.POSTGRES_PORT,
|
port: process.env.DB_PORT,
|
||||||
})
|
});
|
||||||
|
|
||||||
async function checkDB() {
|
async function checkDB() {
|
||||||
try {
|
try {
|
||||||
@@ -15,11 +15,29 @@ async function checkDB() {
|
|||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Database connection failed:', error);
|
console.error('Database connection failed:', error);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function initDB() {
|
||||||
|
try {
|
||||||
|
await pool.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS sensors (
|
||||||
|
id VARCHAR(10) PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
code_hash TEXT NOT NULL UNIQUE,
|
||||||
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
last_seen TIMESTAMP DEFAULT NOW(),
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sensors_code_hash ON sensors(code_hash);
|
||||||
|
`);
|
||||||
|
console.log('[DB] Database schema initialized (sensors table ensured)');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DB] Schema initialization failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restituisce i dati del sensore in base al token ricevuto.
|
* Restituisce i dati del sensore in base al token ricevuto.
|
||||||
* Il token viene hashato prima della comparazione con il database.
|
* Il token viene hashato prima della comparazione con il database.
|
||||||
@@ -78,6 +96,7 @@ async function getSensors() {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
checkDB,
|
checkDB,
|
||||||
|
initDB,
|
||||||
getSensor,
|
getSensor,
|
||||||
updateLastSeen,
|
updateLastSeen,
|
||||||
setSensorActivity,
|
setSensorActivity,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ app.get('/', (req, res) => {
|
|||||||
app.get('/health', async (req, res) => {
|
app.get('/health', async (req, res) => {
|
||||||
const dbConnected = await require('./helper/authdb').checkDB();
|
const dbConnected = await require('./helper/authdb').checkDB();
|
||||||
const redisConnected = await redisHelper.checkRedis();
|
const redisConnected = await redisHelper.checkRedis();
|
||||||
console.log('DATABASE LOGS', process.env.POSTGRES_USER, process.env.POSTGRES_HOST, process.env.POSTGRES_NAME, process.env.POSTGRES_PASSWORD, process.env.POSTGRES_PORT);
|
console.log('DATABASE LOGS', process.env.DB_USER, process.env.DB_HOST, process.env.DB_NAME, process.env.DB_PASSWORD, process.env.DB_PORT);
|
||||||
console.log('REDIS LOGS', process.env.REDIS_HOST, process.env.REDIS_PORT);
|
console.log('REDIS LOGS', process.env.REDIS_HOST, process.env.REDIS_PORT);
|
||||||
|
|
||||||
res.status(200).send({
|
res.status(200).send({
|
||||||
@@ -76,10 +76,11 @@ app.get('/sessions/:sensorId/csv', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- HTTP server + WebSocket per watchers live ---
|
|
||||||
|
|
||||||
const server = app.listen(process.env.PORT, '0.0.0.0', () => {
|
const PORT = process.env.PORT || 3000;
|
||||||
console.log(`Realtime on port ${process.env.PORT}`);
|
const server = app.listen(PORT, '0.0.0.0', async () => {
|
||||||
|
console.log(`Realtime on port ${PORT}`);
|
||||||
|
await require('./helper/authdb').initDB();
|
||||||
});
|
});
|
||||||
|
|
||||||
const wss = new WebSocket.Server({ server, path: '/live' });
|
const wss = new WebSocket.Server({ server, path: '/live' });
|
||||||
|
|||||||
Reference in New Issue
Block a user