Compare commits
6 Commits
9e6bb26a2c
...
fix-enchan
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0dbec2cbdb | ||
|
|
69012029ad | ||
|
|
5433529ffd | ||
|
|
e43c330594 | ||
|
|
974cbe93cd | ||
|
|
c8668920a6 |
8
.claude/settings.local.json
Normal file
8
.claude/settings.local.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(find /Users/sese/Local/dev/MEB/meb-custom-server/copernicus/routers -type f -name \"*.py\" -exec basename {} \\\\;)",
|
||||
"Bash(:)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
DOMAIN=
|
||||
|
||||
#production= mebboat.it
|
||||
#development= localhost
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -16,4 +16,6 @@ Thumbs.db
|
||||
.vscode/
|
||||
.idea/
|
||||
**/tsconfig.tsbuildinfo
|
||||
.eslintcache
|
||||
.eslintcache
|
||||
|
||||
.venv/
|
||||
@@ -12,9 +12,7 @@ const config = {
|
||||
|
||||
const pools = {
|
||||
data: new Pool({ ...config, database: process.env.DATA_DB }),
|
||||
users: new Pool({ ...config, database: process.env.USERS_DB }),
|
||||
sensors: new Pool({ ...config, database: process.env.SENSOR_DB || 'users' }),
|
||||
rules: new Pool({ ...config, database: process.env.RULES_DB || 'rules' }),
|
||||
sensors: new Pool({ ...config, database: process.env.SENSOR_DB || 'sensors' }),
|
||||
}
|
||||
|
||||
Object.entries(pools).forEach(([name, pool]) => {
|
||||
@@ -25,7 +23,7 @@ Object.entries(pools).forEach(([name, pool]) => {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {'users' | 'references'} db - the name of the database
|
||||
* @param {'data' | 'sensors'} db - the name of the database
|
||||
* @returns {Promise<import('pg').PoolClient>}
|
||||
*/
|
||||
async function getClient(db) {
|
||||
@@ -39,9 +37,9 @@ async function getClient(db) {
|
||||
* Esegue una query sul database specificato
|
||||
* @param {string} text - Query SQL
|
||||
* @param {any[]} params - Parametri
|
||||
* @param {'users' | 'references'} name - Quale DB usare
|
||||
* @param {'data' | 'sensors'} name - Quale DB usare
|
||||
*/
|
||||
async function query(text, params, name = 'users') {
|
||||
async function query(text, params, name = 'data') {
|
||||
const client = await getClient(name);
|
||||
try {
|
||||
return await client.query(text, params);
|
||||
@@ -56,7 +54,7 @@ async function query(text, params, name = 'users') {
|
||||
/**
|
||||
* Inserisce una riga in una tabella
|
||||
*/
|
||||
async function append(table, data, type = 'users') {
|
||||
async function append(table, data, type = 'data') {
|
||||
const keys = Object.keys(data);
|
||||
const values = Object.values(data);
|
||||
const placeholders = keys.map((_, i) => `$${i + 1}`).join(', ');
|
||||
@@ -68,7 +66,7 @@ async function append(table, data, type = 'users') {
|
||||
/**
|
||||
* Rimuove una riga
|
||||
*/
|
||||
async function remove(table, condition, params, type = 'users') {
|
||||
async function remove(table, condition, params, type = 'data') {
|
||||
const sql = `DELETE FROM ${table} WHERE ${condition}`;
|
||||
return await query(sql, params, type);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
const query = require('../storage/database').query;
|
||||
const track = require('../tools/tracking')
|
||||
const track = require('../tools/tracking');
|
||||
const { v4: uuid } = require('uuid');
|
||||
const security = require('../tools/security')
|
||||
const security = require('../tools/security');
|
||||
|
||||
|
||||
/**
|
||||
* Registra un nuovo utente
|
||||
*/
|
||||
async function register(username, password) {
|
||||
const userExists = await query('SELECT id FROM users WHERE username = $1', [username]);
|
||||
|
||||
@@ -14,60 +10,55 @@ async function register(username, password) {
|
||||
throw new Error('User already exists');
|
||||
}
|
||||
|
||||
const hashedPassword = security.hashPassword(password);
|
||||
const hashedPassword = await security.hashPassword(password);
|
||||
const id = uuid();
|
||||
|
||||
await query('INSERT INTO users (id, username, password_hash) VALUES ($1, $2, $3)', [id, username, hashedPassword]);
|
||||
await query(
|
||||
'INSERT INTO users (id, username, password_hash) VALUES ($1, $2, $3)',
|
||||
[id, username, hashedPassword]
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
user: {
|
||||
id,
|
||||
username
|
||||
}
|
||||
};
|
||||
return { success: true, user: { id, username } };
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Esegue il login di un utente
|
||||
*/
|
||||
async function login(username, password) {
|
||||
const result = await query('SELECT id, username, password_hash, active, created_at FROM users WHERE username = $1', [username]);
|
||||
const result = await query(
|
||||
'SELECT id, username, password_hash, is_active, created_at FROM users WHERE username = $1',
|
||||
[username]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
throw new Error('No user matched')
|
||||
throw new Error('No user matched');
|
||||
}
|
||||
|
||||
const user = result.rows[0];
|
||||
|
||||
if (!user.is_active) {
|
||||
throw new Error('User account is not active');
|
||||
}
|
||||
|
||||
const isValid = await security.verifyPassword(password, user.password_hash);
|
||||
|
||||
if (!isValid) {
|
||||
throw new Error('Password mismatch')
|
||||
throw new Error('Password mismatch');
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
created: user.created_at
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Esegue il logout di un utente
|
||||
*
|
||||
*/
|
||||
async function logout(sessionID) {
|
||||
if (!sessionID) {
|
||||
throw new Error('no sessio id passed');
|
||||
throw new Error('No session ID provided');
|
||||
}
|
||||
|
||||
const result = await query('UPDATE sessions SET is_revoked = TRUE WHERE id = $1', [sessionID]);
|
||||
return result.rowCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea una nuova sessione per un utente che ha appaena eseguito il login
|
||||
*/
|
||||
async function newSession(userId, userAgent, ip) {
|
||||
const id = uuid();
|
||||
const sessionCode = security.generateSessionCode();
|
||||
@@ -82,39 +73,29 @@ async function newSession(userId, userAgent, ip) {
|
||||
return { id, sessionCode };
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida una sessione
|
||||
*/
|
||||
async function validateSession(token) {
|
||||
const parsed = security.parseSessionToken(token);
|
||||
|
||||
if (!parsed) {
|
||||
throw new Error('Invalid token format');
|
||||
async function validateSession(sessionId) {
|
||||
if (!sessionId || typeof sessionId !== 'string') {
|
||||
throw new Error('Invalid session ID');
|
||||
}
|
||||
|
||||
const { code, username } = parsed;
|
||||
const result = await query(
|
||||
'SELECT s.id, u.is_active FROM sessions s JOIN users u ON s.user_id = u.id WHERE s.id = $1 AND s.is_revoked = FALSE',
|
||||
[sessionId]
|
||||
);
|
||||
|
||||
const result = await query('SELECT s.id as session_id, s.user_id, u.username, u.is_active, u.created_at FROM sessions s JOIN users u ON s.user_id = u.id WHERE s.session_code = $1 AND s.is_revoked = FALSE', [code]);
|
||||
if (result.rows.length === 0) {
|
||||
throw new Error('Session not found or revoked')
|
||||
throw new Error('Session not found or revoked');
|
||||
}
|
||||
|
||||
const session = result.rows[0];
|
||||
|
||||
if (session.username !== username) {
|
||||
throw new Error('Session user mismatch');
|
||||
}
|
||||
|
||||
if (!session.active) {
|
||||
throw new Error('Session is not active');
|
||||
if (!result.rows[0].is_active) {
|
||||
throw new Error('User account is not active');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
register,
|
||||
login,
|
||||
logout,
|
||||
newSession,
|
||||
validateSession
|
||||
}
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ async function getCurrentSessionID(token) {
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
|
||||
const result = await query('SELECT id FROM sessions WHERE session_code = $1', [parsed.code]);
|
||||
const result = await query('SELECT id FROM sessions WHERE session_code = $1', [parsed]);
|
||||
return result.rows[0]?.id || null;
|
||||
}
|
||||
|
||||
|
||||
@@ -61,10 +61,13 @@ const authRateLimit = createRateLimiter(RATE_LIMIT_AUTH_MAX);
|
||||
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('X-XSS-Protection', '0');
|
||||
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
||||
// Rimuovi header che rivelano info sul server
|
||||
res.setHeader(
|
||||
'Content-Security-Policy',
|
||||
"default-src 'self'; style-src 'self'; script-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'"
|
||||
);
|
||||
res.removeHeader('X-Powered-By');
|
||||
next();
|
||||
});
|
||||
@@ -80,6 +83,7 @@ app.use(parser());
|
||||
// ─── STATIC FILES ───────────────────────────────────────────────────
|
||||
const staticFolder = path.join(__dirname, 'static');
|
||||
app.use('/static', express.static(staticFolder));
|
||||
app.use('/api/static', express.static(staticFolder));
|
||||
|
||||
// ─── NUNJUCKS TEMPLATES ─────────────────────────────────────────────
|
||||
const templatesFolder = path.join(__dirname, 'templates');
|
||||
@@ -133,7 +137,7 @@ app.use((req, res) => {
|
||||
|
||||
// ─── ERROR HANDLER GLOBALE ──────────────────────────────────────────
|
||||
app.use((err, req, res, _next) => {
|
||||
console.error('[AUTH] Errore non gestito:', err);
|
||||
console.error('[ERROR]', err.message, '| code:', err.code);
|
||||
res.status(500).json({ error: 'Errore interno del server' });
|
||||
});
|
||||
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
const jwt = require('../tools/jwt');
|
||||
const { validateSession } = require('../core/auth.core');
|
||||
|
||||
/**
|
||||
* 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 userAuth = async (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' });
|
||||
}
|
||||
const unauthorized = (reason) => {
|
||||
if (req.accepts('html')) {
|
||||
const redirect = encodeURIComponent(req.originalUrl);
|
||||
return res.redirect(`/login?redirect=${redirect}`);
|
||||
}
|
||||
return res.status(401).json({ error: reason || 'Non autorizzato' });
|
||||
};
|
||||
|
||||
// Limite ragionevole sulla lunghezza del token per evitare abusi
|
||||
if (token.length > 2048) {
|
||||
return res.status(400).json({ error: 'Token non valido' });
|
||||
if (!token || typeof token !== 'string' || token.length > 2048) {
|
||||
return unauthorized('Token mancante o non valido');
|
||||
}
|
||||
|
||||
const verified = jwt.verifyToken(token);
|
||||
if (!verified.valid) {
|
||||
return res.status(401).json({
|
||||
error: 'Sessione non valida o scaduta',
|
||||
reason: verified.reason
|
||||
});
|
||||
return unauthorized(`Sessione non valida (${verified.reason})`);
|
||||
}
|
||||
|
||||
try {
|
||||
await validateSession(verified.payload.session_id);
|
||||
} catch (err) {
|
||||
return unauthorized('Sessione non valida o revocata');
|
||||
}
|
||||
|
||||
req.user = verified.payload;
|
||||
|
||||
@@ -5,70 +5,88 @@ const jwt = require('../tools/jwt');
|
||||
const CONSOLE_URL = process.env.CONSOLE_URL || 'http://localhost:3004';
|
||||
const COOKIE_DOMAIN = process.env.COOKIE_DOMAIN || undefined;
|
||||
|
||||
// Validazione input
|
||||
const USERNAME_REGEX = /^[a-zA-Z0-9_.\-]{3,50}$/;
|
||||
const PASSWORD_MIN_LENGTH = 8;
|
||||
const PASSWORD_MAX_LENGTH = 128;
|
||||
|
||||
const ERROR_RESPONSES = {
|
||||
csrf: { success: false, error: 'csrf', message: 'Richiesta non valida, riprova' },
|
||||
invalid_credentials: { success: false, error: 'invalid_credentials', message: 'Credenziali non valide' },
|
||||
internal: { success: false, error: 'internal', message: 'Errore interno, riprova più tardi' }
|
||||
};
|
||||
|
||||
/**
|
||||
* Restituisce un redirect sicuro, scartando URL che puntano ad API
|
||||
* o ad host diversi da CONSOLE_URL.
|
||||
*/
|
||||
function resolveSafeRedirect(redirect) {
|
||||
if (!redirect || typeof redirect !== 'string') return CONSOLE_URL;
|
||||
|
||||
try {
|
||||
const redirectUrl = new URL(redirect);
|
||||
const consoleUrl = new URL(CONSOLE_URL);
|
||||
|
||||
const sameHost = redirectUrl.hostname === consoleUrl.hostname;
|
||||
const notApi = !redirectUrl.pathname.startsWith('/api/');
|
||||
|
||||
if (sameHost && notApi) return redirect;
|
||||
} catch {
|
||||
// URL non valido / relativo: fallback a CONSOLE_URL
|
||||
}
|
||||
|
||||
return CONSOLE_URL;
|
||||
}
|
||||
|
||||
router.post('/register', async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
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 || !password || typeof username !== 'string' || typeof password !== 'string') {
|
||||
return res.status(400).json({ success: false, error: 'Username e password richiesti' });
|
||||
}
|
||||
|
||||
if (!USERNAME_REGEX.test(username)) {
|
||||
return res.status(400).json({
|
||||
error: 'Username non valido. 3-50 caratteri alfanumerici, underscore, punto o trattino.'
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
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`
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: `Password deve essere tra ${PASSWORD_MIN_LENGTH} e ${PASSWORD_MAX_LENGTH} caratteri`
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await auth.register(username, password);
|
||||
res.status(201).end();
|
||||
return res.status(201).json({ success: true });
|
||||
} catch (err) {
|
||||
if (err.message === 'User already exists') {
|
||||
return res.status(409).json({ success: false, error: 'User already exists' });
|
||||
}
|
||||
console.error('[AUTH] Register failed:', err.message);
|
||||
const status = err.message === 'User already exists' ? 409 : 500;
|
||||
res.status(status).json({ error: err.message === 'User already exists' ? err.message : 'Errore interno' });
|
||||
return res.status(500).json({ success: false, error: 'Errore interno' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/login', async (req, res) => {
|
||||
const { username, password, redirect } = req.body;
|
||||
const { username, password, redirect, _csrf } = req.body;
|
||||
|
||||
const csrfCookie = req.cookies && req.cookies._csrf;
|
||||
if (!_csrf || !csrfCookie || _csrf !== csrfCookie) {
|
||||
return res.status(400).json(ERROR_RESPONSES.csrf);
|
||||
}
|
||||
|
||||
// Validazione base
|
||||
if (!username || !password || typeof username !== 'string' || typeof password !== 'string') {
|
||||
return res.render('loginpage', { error: 'Credenziali non valide', redirect: redirect || '' });
|
||||
return res.status(400).json(ERROR_RESPONSES.invalid_credentials);
|
||||
}
|
||||
|
||||
// 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 || '' });
|
||||
return res.status(400).json(ERROR_RESPONSES.invalid_credentials);
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
const safeRedirect = resolveSafeRedirect(redirect);
|
||||
|
||||
try {
|
||||
const user = await auth.login(username, password);
|
||||
@@ -81,19 +99,29 @@ router.post('/login', async (req, res) => {
|
||||
sameSite: 'lax',
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000
|
||||
};
|
||||
|
||||
if (COOKIE_DOMAIN) {
|
||||
cookieOptions.domain = COOKIE_DOMAIN;
|
||||
}
|
||||
if (COOKIE_DOMAIN) cookieOptions.domain = COOKIE_DOMAIN;
|
||||
|
||||
res.cookie('auth_token', token, cookieOptions);
|
||||
res.clearCookie('_csrf');
|
||||
|
||||
const destination = redirect || CONSOLE_URL;
|
||||
res.redirect(destination);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
redirect_url: safeRedirect,
|
||||
message: 'Login effettuato con successo'
|
||||
});
|
||||
} catch (err) {
|
||||
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 || '' });
|
||||
if (err.message === 'No user matched' || err.message === 'Password mismatch') {
|
||||
return res.status(401).json(ERROR_RESPONSES.invalid_credentials);
|
||||
}
|
||||
if (err.message === 'User account is not active') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'account_inactive',
|
||||
message: 'Account disattivato'
|
||||
});
|
||||
}
|
||||
console.error('[AUTH] Login error:', err.message);
|
||||
return res.status(500).json(ERROR_RESPONSES.internal);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -112,12 +140,10 @@ router.post('/logout', async (req, res) => {
|
||||
}
|
||||
|
||||
const clearOptions = { httpOnly: true, sameSite: 'lax' };
|
||||
if (COOKIE_DOMAIN) {
|
||||
clearOptions.domain = COOKIE_DOMAIN;
|
||||
}
|
||||
if (COOKIE_DOMAIN) clearOptions.domain = COOKIE_DOMAIN;
|
||||
|
||||
res.clearCookie('auth_token', clearOptions);
|
||||
res.redirect('/login');
|
||||
return res.status(200).json({ success: true, redirect_url: '/login' });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -5,11 +5,9 @@ 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 {
|
||||
|
||||
@@ -1,8 +1,25 @@
|
||||
const router = require('express').Router();
|
||||
const crypto = require('crypto');
|
||||
|
||||
const ERROR_MESSAGES = {
|
||||
invalid_credentials: 'Credenziali non valide',
|
||||
invalid_redirect: 'Redirect non autorizzato',
|
||||
csrf: 'Richiesta non valida, riprova'
|
||||
};
|
||||
|
||||
router.get('/login', (req, res) => {
|
||||
const redirect = req.query.redirect || '';
|
||||
res.render('loginpage', { error: null, redirect });
|
||||
const errorKey = req.query.error;
|
||||
const error = ERROR_MESSAGES[errorKey] || null;
|
||||
|
||||
const csrfToken = crypto.randomBytes(32).toString('hex');
|
||||
res.cookie('_csrf', csrfToken, {
|
||||
httpOnly: true,
|
||||
sameSite: 'strict',
|
||||
secure: process.env.NODE_ENV === 'production'
|
||||
});
|
||||
|
||||
res.render('loginpage', { error, redirect, csrf_token: csrfToken });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router;
|
||||
|
||||
@@ -4,7 +4,7 @@ const userAuth = require('../../middlewares/user.security');
|
||||
router.use(userAuth);
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
res.render('sessions');
|
||||
res.render('sessions', { user: req.user });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,7 +1,10 @@
|
||||
const router = require('express').Router();
|
||||
const userAuth = require('../../middlewares/user.security');
|
||||
|
||||
router.use(userAuth);
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
res.render('user');
|
||||
res.render('user', { user: req.user });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router;
|
||||
|
||||
BIN
auth/src/static/font/sans-flex.ttf
Normal file
BIN
auth/src/static/font/sans-flex.ttf
Normal file
Binary file not shown.
@@ -15,7 +15,8 @@
|
||||
--header-border: #e2e8f0;
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-md:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
}
|
||||
@@ -26,21 +27,21 @@
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Normal';
|
||||
src: url('../font/Quicksand-VariableFont_wght.ttf');
|
||||
font-family: "Normal";
|
||||
src: url("../font/sans-flex.ttf");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Bold';
|
||||
src: url('../font/Quicksand-VariableFont_wght.ttf');
|
||||
font-family: "Bold";
|
||||
src: url("../font/sans-flex.ttf");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Normal', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
font-family: "Normal", Arial;
|
||||
color: var(--text-primary);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
@@ -53,7 +54,7 @@ button {
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-family: 'Bold', inherit;
|
||||
font-family: "Bold", inherit;
|
||||
cursor: pointer;
|
||||
transition: all 0.5s cubic-bezier(0.8, 0, 0.2, 1);
|
||||
white-space: nowrap;
|
||||
@@ -79,13 +80,11 @@ button.prominent:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
|
||||
button.prominent:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
|
||||
/* INFO PANEL */
|
||||
|
||||
.info-panel {
|
||||
@@ -111,9 +110,6 @@ button.prominent:active {
|
||||
transition: transform 0.12s ease;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* GRID & CARD ITEMS */
|
||||
|
||||
.grid {
|
||||
@@ -132,7 +128,9 @@ button.prominent:active {
|
||||
border-radius: 20px;
|
||||
text-decoration: none;
|
||||
color: var(--text-primary);
|
||||
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
||||
transition:
|
||||
transform 0.12s ease,
|
||||
box-shadow 0.12s ease;
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
@@ -158,10 +156,6 @@ button.prominent:active {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* HEADER */
|
||||
|
||||
.header {
|
||||
@@ -179,7 +173,6 @@ button.prominent:active {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
|
||||
.header h1 {
|
||||
color: var(--text-primary);
|
||||
font-size: 1.25rem;
|
||||
@@ -198,4 +191,4 @@ button.prominent:active {
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
padding-inline: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,103 +5,46 @@ const config = {
|
||||
password: process.env.DB_PASSWORD,
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT,
|
||||
database: 'data',
|
||||
max: 10,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 5000
|
||||
}
|
||||
};
|
||||
|
||||
const pool = new Pool({ ...config, database: process.env.USERS_DB });
|
||||
const pool = new Pool(config);
|
||||
|
||||
pool.on('error', (err) => {
|
||||
console.error('Error in database', err);
|
||||
console.error('[DB] Pool error:', err.message);
|
||||
});
|
||||
|
||||
/**
|
||||
* Execute a query with parameters
|
||||
* @param {string} text - SQL query
|
||||
* @param {Array} params - Query parameters
|
||||
* @returns {Promise<Object>} Query result
|
||||
*/
|
||||
async function query(text, params) {
|
||||
const start = Date.now();
|
||||
const result = await pool.query(text, params);
|
||||
const duration = Date.now() - start;
|
||||
|
||||
if (duration > 100) {
|
||||
console.warn(`[DB] Slow query (${duration}ms):`, text.substring(0, 80));
|
||||
try {
|
||||
const result = await pool.query(text, params);
|
||||
const duration = Date.now() - start;
|
||||
if (duration > 100) {
|
||||
console.warn(`[DB] Slow query (${duration}ms):`, text.substring(0, 80));
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.error('[DB] Query failed:', err.message, '| code:', err.code);
|
||||
throw err;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a client from pool for transactions
|
||||
* @returns {Promise<Object>} Pool client
|
||||
*/
|
||||
async function getClient() {
|
||||
return await pool.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize database and ensure tables exist
|
||||
*/
|
||||
async function initDb() {
|
||||
// Test connection
|
||||
await pool.query('SELECT NOW()');
|
||||
// Ensure pgcrypto extension (provides gen_random_uuid)
|
||||
// Note: creating extensions requires proper DB permissions (usually superuser in PG)
|
||||
try {
|
||||
await pool.query(`CREATE EXTENSION IF NOT EXISTS pgcrypto;`);
|
||||
} catch (err) {
|
||||
console.warn('[DB] Could not create pgcrypto extension (may require superuser):', err.message);
|
||||
}
|
||||
|
||||
// Ensure tables exist (UUID default generated by DB)
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
telegram_id VARCHAR(50) UNIQUE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_telegram_id ON users(telegram_id);
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
session_code VARCHAR(64) NOT NULL,
|
||||
encoded_username TEXT NOT NULL,
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
browser VARCHAR(100),
|
||||
os VARCHAR(100),
|
||||
device_type VARCHAR(50),
|
||||
location_country VARCHAR(100),
|
||||
location_city VARCHAR(100),
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
last_active TIMESTAMP DEFAULT NOW(),
|
||||
is_revoked BOOLEAN DEFAULT FALSE
|
||||
);
|
||||
|
||||
-- Altera colonna in base al nuovo standard token 32 byte - 64 url chars
|
||||
ALTER TABLE sessions ALTER COLUMN session_code TYPE VARCHAR(64);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_code ON sessions(session_code);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
|
||||
`);
|
||||
}
|
||||
|
||||
async function checkPostgres() {
|
||||
try {
|
||||
await pool.query('SELECT NOW()');
|
||||
return true;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,108 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
|
||||
<link rel="stylesheet" href="../static/style/style.css">
|
||||
<link rel="stylesheet" href="../static/style/login.css" </head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login — Console MEB</title>
|
||||
<link rel="stylesheet" href="/static/style/style.css">
|
||||
<link rel="stylesheet" href="/static/style/login.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="login">
|
||||
<div class="header">
|
||||
<h1>Accedi alla <span class="prominent-title">Console MEB</span></h1>
|
||||
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<p class="error">{{ error }}</p>
|
||||
<p class="error" id="errorMessage">{{ error }}</p>
|
||||
{% endif %}
|
||||
|
||||
<form action="/api/auth/login" method="post">
|
||||
<input type="hidden" name="redirect" value="{{ redirect }}">
|
||||
<form id="loginForm">
|
||||
<input type="hidden" id="redirect" name="redirect" value="{{ redirect }}">
|
||||
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
|
||||
<div class="group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
<input type="text" id="username" name="username" required autocomplete="username">
|
||||
</div>
|
||||
<div class="group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
<input type="password" id="password" name="password" required autocomplete="current-password">
|
||||
</div>
|
||||
|
||||
<button type="submit">Login</button>
|
||||
<button type="submit" class="prominent" id="submitBtn">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const form = document.getElementById('loginForm');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const errorMessage = document.getElementById('errorMessage');
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'Accesso in corso...';
|
||||
if (errorMessage) errorMessage.style.display = 'none';
|
||||
|
||||
const formData = new FormData(form);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
username: formData.get('username'),
|
||||
password: formData.get('password'),
|
||||
redirect: formData.get('redirect'),
|
||||
_csrf: formData.get('_csrf')
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json().catch(() => ({}));
|
||||
|
||||
if (response.ok && data.success && data.redirect_url) {
|
||||
window.location.href = data.redirect_url;
|
||||
} else {
|
||||
const errorMsg = data.message || 'Errore durante il login';
|
||||
|
||||
if (errorMessage) {
|
||||
errorMessage.textContent = errorMsg;
|
||||
errorMessage.style.display = 'block';
|
||||
} else {
|
||||
alert(errorMsg);
|
||||
}
|
||||
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = 'Login';
|
||||
|
||||
// Ricarica la pagina per ottenere un nuovo CSRF token
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMsg = 'Errore di connessione. Riprova più tardi.';
|
||||
|
||||
if (errorMessage) {
|
||||
errorMessage.textContent = errorMsg;
|
||||
errorMessage.style.display = 'block';
|
||||
} else {
|
||||
alert(errorMsg);
|
||||
}
|
||||
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = 'Login';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -1 +1,109 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sessioni — Console MEB</title>
|
||||
<link rel="stylesheet" href="/static/style/style.css">
|
||||
<style>
|
||||
main { padding: 24px 30px; }
|
||||
main h2 { font-size: 1.1rem; margin-bottom: 20px; color: var(--text-secondary); font-weight: 500; }
|
||||
.session-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border: 1px solid var(--header-border);
|
||||
border-radius: var(--radius-lg);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.session-card .info h3 { font-size: 0.95rem; margin-bottom: 4px; }
|
||||
.session-card .info p { font-size: 0.8rem; color: var(--text-secondary); margin: 2px 0; }
|
||||
.session-card button { font-size: 0.8rem; padding: 6px 14px; color: #dc2626; border-color: #fca5a5; }
|
||||
.session-card button:hover { background-color: #fef2f2; border-color: #dc2626; color: #dc2626; }
|
||||
#loading { color: var(--text-tertiary); font-size: 0.9rem; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>Console MEB</h1>
|
||||
<div class="profile">
|
||||
<p>{{ user.username }}</p>
|
||||
<form action="/api/auth/logout" method="post">
|
||||
<button type="submit">Logout</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<h2>Sessioni attive</h2>
|
||||
<div id="sessions-container">
|
||||
<p id="loading">Caricamento...</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
function escapeHtml(str) {
|
||||
const d = document.createElement('div');
|
||||
d.appendChild(document.createTextNode(str || ''));
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function formatDate(iso) {
|
||||
if (!iso) return 'N/D';
|
||||
return new Date(iso).toLocaleString('it-IT', {
|
||||
day: 'numeric', month: 'short', year: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
async function loadSessions() {
|
||||
try {
|
||||
const res = await fetch('/api/sessions');
|
||||
if (res.status === 401) { window.location.href = '/login'; return; }
|
||||
if (!res.ok) throw new Error('Network error');
|
||||
|
||||
const sessions = await res.json();
|
||||
const container = document.getElementById('sessions-container');
|
||||
|
||||
if (sessions.length === 0) {
|
||||
container.innerHTML = '<p style="color:var(--text-tertiary)">Nessuna sessione attiva.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = sessions.map(s => `
|
||||
<div class="session-card" id="session-${escapeHtml(s.id)}">
|
||||
<div class="info">
|
||||
<h3>${escapeHtml(s.browser || 'Browser sconosciuto')} su ${escapeHtml(s.os || 'OS sconosciuto')}</h3>
|
||||
<p>${escapeHtml(s.device_type || '')}${s.ip_address ? ' — ' + escapeHtml(s.ip_address) : ''}</p>
|
||||
<p>Ultima attività: ${formatDate(s.last_active)}</p>
|
||||
</div>
|
||||
<button onclick="revokeSession('${escapeHtml(s.id)}')">Revoca</button>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch {
|
||||
document.getElementById('sessions-container').innerHTML =
|
||||
'<p style="color:#dc2626">Errore nel caricamento delle sessioni.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
async function revokeSession(id) {
|
||||
if (!confirm('Revocare questa sessione?')) return;
|
||||
try {
|
||||
const res = await fetch('/api/sessions/' + encodeURIComponent(id), { method: 'DELETE' });
|
||||
if (res.status === 401) { window.location.href = '/login'; return; }
|
||||
if (!res.ok) throw new Error();
|
||||
const el = document.getElementById('session-' + id);
|
||||
if (el) el.remove();
|
||||
} catch {
|
||||
alert('Errore durante la revoca della sessione.');
|
||||
}
|
||||
}
|
||||
|
||||
loadSessions();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -1 +1,87 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Profilo — Console MEB</title>
|
||||
<link rel="stylesheet" href="/static/style/style.css">
|
||||
<style>
|
||||
main { padding: 24px 30px; max-width: 600px; }
|
||||
main h2 { font-size: 1.1rem; margin-bottom: 20px; color: var(--text-secondary); font-weight: 500; }
|
||||
.field { margin-bottom: 20px; }
|
||||
.field label { display: block; font-size: 0.75rem; font-weight: 600; color: var(--text-secondary); margin-bottom: 4px; }
|
||||
.field p { font-size: 0.95rem; padding: 10px 14px; border: 1px solid var(--header-border); border-radius: var(--radius-md); }
|
||||
.field-empty { color: var(--text-tertiary); font-style: italic; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>Console MEB</h1>
|
||||
<div class="profile">
|
||||
<p id="username-label">{{ user.username }}</p>
|
||||
<form action="/api/auth/logout" method="post">
|
||||
<button type="submit">Logout</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<h2>Profilo utente</h2>
|
||||
<div id="user-info">
|
||||
<p style="color:var(--text-tertiary)">Caricamento...</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
function escapeHtml(str) {
|
||||
const d = document.createElement('div');
|
||||
d.appendChild(document.createTextNode(str || ''));
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function formatDate(iso) {
|
||||
if (!iso) return 'N/D';
|
||||
return new Date(iso).toLocaleDateString('it-IT', {
|
||||
day: 'numeric', month: 'long', year: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
async function loadUser() {
|
||||
try {
|
||||
const res = await fetch('/api/users/me');
|
||||
if (res.status === 401) { window.location.href = '/login'; return; }
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
const user = await res.json();
|
||||
document.getElementById('username-label').textContent = user.username;
|
||||
document.getElementById('user-info').innerHTML = `
|
||||
<div class="field">
|
||||
<label>Username</label>
|
||||
<p>${escapeHtml(user.username)}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Account creato il</label>
|
||||
<p>${formatDate(user.created_at)}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Telegram ID</label>
|
||||
<p>${user.telegram_id ? escapeHtml(user.telegram_id) : '<span class="field-empty">Non configurato</span>'}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Sessioni</label>
|
||||
<p><a href="/sessions">Gestisci sessioni →</a></p>
|
||||
</div>
|
||||
`;
|
||||
} catch {
|
||||
document.getElementById('user-info').innerHTML =
|
||||
'<p style="color:#dc2626">Errore nel caricamento del profilo.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
loadUser();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -63,8 +63,7 @@ function getToken(header) {
|
||||
return parts[1];
|
||||
}
|
||||
|
||||
//TODO: valutare se modificare in return null per accettare solo metodo Bearer Authorization Token,
|
||||
return header;
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = { generateToken, verifyToken, getToken };
|
||||
@@ -8,8 +8,8 @@ const saltRounds = 12;
|
||||
* @param {string} password - Password da hashare
|
||||
* @returns {string} - Hash della password
|
||||
*/
|
||||
function hashPassword(password) {
|
||||
return bcrypt.hashSync(password, saltRounds);
|
||||
async function hashPassword(password) {
|
||||
return bcrypt.hash(password, saltRounds);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -18,8 +18,8 @@ function hashPassword(password) {
|
||||
* @param {string} hash - Hash della password
|
||||
* @returns {boolean} - True se la password è corretta, false altrimenti
|
||||
*/
|
||||
function verifyPassword(password, hash) {
|
||||
return bcrypt.compareSync(password, hash);
|
||||
async function verifyPassword(password, hash) {
|
||||
return bcrypt.compare(password, hash);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,7 +23,7 @@ services:
|
||||
- "3006:3006"
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.auth.rule=Host(`auth.mebboat.it`)"
|
||||
- "traefik.http.routers.auth.rule=Host(`auth.${DOMAIN:-mebboat.it}`)"
|
||||
- "traefik.http.routers.auth.entrypoints=websecure"
|
||||
- "traefik.http.services.auth.loadbalancer.server.port=3006"
|
||||
- "traefik.docker.network=meb-public"
|
||||
@@ -46,7 +46,7 @@ services:
|
||||
- meb-private
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.api.rule=Host(`api.mebboat.it`)"
|
||||
- "traefik.http.routers.api.rule=Host(`api.${DOMAIN:-mebboat.it}`)"
|
||||
- "traefik.http.routers.api.entrypoints=websecure"
|
||||
- "traefik.http.services.api.loadbalancer.server.port=3003"
|
||||
- "traefik.http.routers.api.tls.certresolver=letsencrypt"
|
||||
@@ -68,7 +68,7 @@ services:
|
||||
- meb-private
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.console.rule=Host(`console.mebboat.it`)"
|
||||
- "traefik.http.routers.console.rule=Host(`console.${DOMAIN:-mebboat.it}`)"
|
||||
- "traefik.http.routers.console.entrypoints=websecure"
|
||||
- "traefik.http.services.console.loadbalancer.server.port=3004"
|
||||
- "traefik.http.routers.console.tls.certresolver=letsencrypt"
|
||||
@@ -90,7 +90,7 @@ services:
|
||||
- meb-public
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.realtime.rule=Host(`realtime.mebboat.it`)"
|
||||
- "traefik.http.routers.realtime.rule=Host(`realtime.${DOMAIN:-mebboat.it}`)"
|
||||
- "traefik.http.routers.realtime.entrypoints=websecure"
|
||||
- "traefik.http.services.realtime.loadbalancer.server.port=3000"
|
||||
- "traefik.http.routers.realtime.tls.certresolver=letsencrypt"
|
||||
@@ -115,7 +115,7 @@ services:
|
||||
- meb-public
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.ml.rule=Host(`ml.mebboat.it`)"
|
||||
- "traefik.http.routers.ml.rule=Host(`ml.${DOMAIN:-mebboat.it}`)"
|
||||
- "traefik.http.routers.ml.entrypoints=websecure"
|
||||
- "traefik.http.services.ml.loadbalancer.server.port=8000"
|
||||
- "traefik.http.routers.ml.tls.certresolver=letsencrypt"
|
||||
@@ -139,7 +139,7 @@ services:
|
||||
# - meb-internal
|
||||
# labels:
|
||||
# - "traefik.enable=true"
|
||||
# - "traefik.http.routers.marine.rule=Host(`api.${URL_DOMAIN}`) && PathPrefix(`/marine`)"
|
||||
# - "traefik.http.routers.marine.rule=Host(`api.${DOMAIN:-mebboat.it}`) && PathPrefix(`/marine`)"
|
||||
# - "traefik.http.routers.marine.entrypoints=web"
|
||||
# - "traefik.http.services.marine.loadbalancer.server.port=8001"
|
||||
# - "traefik.docker.network=meb-proxy-net"
|
||||
@@ -155,8 +155,8 @@ services:
|
||||
# environment:
|
||||
# - DATABASE_URL=postgresql://meb:meb@meb-postgres:5432/circuits
|
||||
# - AUTH_SERVICE_URL=http://auth:3001
|
||||
# - AUTH_URL=http://auth.${URL_DOMAIN:-localhost}
|
||||
# - API_URL=http://api.${URL_DOMAIN:-localhost}
|
||||
# - AUTH_URL=http://auth.${DOMAIN:-localhost}
|
||||
# - API_URL=http://api.${DOMAIN:-localhost}
|
||||
# - NODE_ENV=${NODE_ENV:-development}
|
||||
# volumes:
|
||||
# - ./circuits/src:/app/src
|
||||
@@ -173,7 +173,7 @@ services:
|
||||
# - meb-internal
|
||||
# labels:
|
||||
# - "traefik.enable=true"
|
||||
# - "traefik.http.routers.circuits.rule=Host(`circuits.${URL_DOMAIN}`)"
|
||||
# - "traefik.http.routers.circuits.rule=Host(`circuits.${DOMAIN:-mebboat.it}`)"
|
||||
# - "traefik.http.routers.circuits.entrypoints=web"
|
||||
# - "traefik.http.services.circuits.loadbalancer.server.port=3005"
|
||||
# - "traefik.docker.network=meb-proxy-net"
|
||||
|
||||
Reference in New Issue
Block a user