fix: additional fix for auth login flow and auth web pages and database

connection.
This commit is contained in:
Giuseppe Raffa
2026-04-21 20:08:59 +02:00
parent c8668920a6
commit 974cbe93cd
17 changed files with 327 additions and 91 deletions

View File

@@ -0,0 +1,4 @@
DOMAIN=
#production= mebboat.it
#development= localhost

4
.gitignore vendored
View File

@@ -16,4 +16,6 @@ Thumbs.db
.vscode/ .vscode/
.idea/ .idea/
**/tsconfig.tsbuildinfo **/tsconfig.tsbuildinfo
.eslintcache .eslintcache
.venv/

View File

@@ -14,7 +14,7 @@ async function register(username, password) {
throw new Error('User already exists'); throw new Error('User already exists');
} }
const hashedPassword = security.hashPassword(password); const hashedPassword = await security.hashPassword(password);
const id = uuid(); 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]);
@@ -33,7 +33,7 @@ async function register(username, password) {
* Esegue il login di un utente * Esegue il login di un utente
*/ */
async function login(username, password) { 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, created_at FROM users WHERE username = $1', [username]);
if (result.rows.length === 0) { if (result.rows.length === 0) {
throw new Error('No user matched') throw new Error('No user matched')
} }
@@ -83,30 +83,24 @@ async function newSession(userId, userAgent, ip) {
} }
/** /**
* Valida una sessione * Valida una sessione tramite il suo UUID
*/ */
async function validateSession(token) { async function validateSession(sessionId) {
const parsed = security.parseSessionToken(token); if (!sessionId || typeof sessionId !== 'string') {
throw new Error('Invalid session ID');
if (!parsed) {
throw new Error('Invalid token format');
} }
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) { 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 (!result.rows[0].is_active) {
throw new Error('User account is not active');
if (session.username !== username) {
throw new Error('Session user mismatch');
}
if (!session.active) {
throw new Error('Session is not active');
} }
} }

View File

@@ -27,7 +27,7 @@ async function getCurrentSessionID(token) {
throw new Error('Invalid 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; return result.rows[0]?.id || null;
} }

View File

@@ -80,6 +80,7 @@ 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));
app.use('/api/static', express.static(staticFolder));
// ─── NUNJUCKS TEMPLATES ───────────────────────────────────────────── // ─── NUNJUCKS TEMPLATES ─────────────────────────────────────────────
const templatesFolder = path.join(__dirname, 'templates'); const templatesFolder = path.join(__dirname, 'templates');

View File

@@ -1,31 +1,45 @@
const jwt = require('../tools/jwt'); const jwt = require('../tools/jwt');
const { validateSession } = require('../core/auth.core');
/** const userAuth = async (req, res, next) => {
* 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']); const token = (req.cookies && req.cookies.auth_token) || jwt.getToken(req.headers['authorization']);
if (!token || typeof token !== 'string') { const redirectToLogin = () => {
if (req.accepts('html')) {
const redirect = encodeURIComponent(req.originalUrl);
return res.redirect(`/login?redirect=${redirect}`);
}
return res.status(401).json({ error: 'Accesso negato: token mancante' }); return res.status(401).json({ error: 'Accesso negato: token mancante' });
};
if (!token || typeof token !== 'string') {
return redirectToLogin();
} }
// Limite ragionevole sulla lunghezza del token per evitare abusi
if (token.length > 2048) { if (token.length > 2048) {
return res.status(400).json({ error: 'Token non valido' }); return redirectToLogin();
} }
const verified = jwt.verifyToken(token); const verified = jwt.verifyToken(token);
if (!verified.valid) { if (!verified.valid) {
return res.status(401).json({ if (req.accepts('html')) {
error: 'Sessione non valida o scaduta', return res.redirect('/login');
reason: verified.reason }
return res.status(401).json({
error: 'Sessione non valida o scaduta',
reason: verified.reason
}); });
} }
try {
await validateSession(verified.payload.session_id);
} catch {
if (req.accepts('html')) {
return res.redirect('/login');
}
return res.status(401).json({ error: 'Sessione non valida o revocata' });
}
req.user = verified.payload; req.user = verified.payload;
next(); next();
}; };

View File

@@ -44,27 +44,40 @@ router.post('/register', async (req, res) => {
}); });
router.post('/login', async (req, res) => { router.post('/login', async (req, res) => {
const { username, password, redirect } = req.body; const { username, password, redirect, _csrf } = req.body;
const loginRedirect = (errorKey, safeRedirect) => {
const params = new URLSearchParams({ error: errorKey });
if (safeRedirect) params.set('redirect', safeRedirect);
return res.redirect(`/login?${params.toString()}`);
};
// Validazione CSRF (double-submit cookie)
const csrfCookie = req.cookies && req.cookies._csrf;
if (!_csrf || !csrfCookie || _csrf !== csrfCookie) {
return loginRedirect('csrf', '');
}
// Validazione base // Validazione base
if (!username || !password || typeof username !== 'string' || typeof password !== 'string') { if (!username || !password || typeof username !== 'string' || typeof password !== 'string') {
return res.render('loginpage', { error: 'Credenziali non valide', redirect: redirect || '' }); return loginRedirect('invalid_credentials', redirect || '');
} }
// Limiti di lunghezza per prevenire abuse // Limiti di lunghezza per prevenire abuse
if (username.length > 50 || password.length > PASSWORD_MAX_LENGTH) { if (username.length > 50 || password.length > PASSWORD_MAX_LENGTH) {
return res.render('loginpage', { error: 'Credenziali non valide', redirect: redirect || '' }); return loginRedirect('invalid_credentials', redirect || '');
} }
// Validazione redirect URL per prevenire open redirect attacks // Validazione redirect URL per prevenire open redirect attacks
let safeRedirect = '';
if (redirect && typeof redirect === 'string') { if (redirect && typeof redirect === 'string') {
try { try {
const redirectUrl = new URL(redirect); const redirectUrl = new URL(redirect);
const consoleUrl = new URL(CONSOLE_URL); const consoleUrl = new URL(CONSOLE_URL);
// Permetti redirect solo allo stesso dominio del CONSOLE_URL
if (redirectUrl.hostname !== consoleUrl.hostname) { if (redirectUrl.hostname !== consoleUrl.hostname) {
return res.render('loginpage', { error: 'Redirect non autorizzato', redirect: '' }); return loginRedirect('invalid_redirect', '');
} }
safeRedirect = redirect;
} catch { } catch {
// URL relativo o non valido — ignora il redirect // URL relativo o non valido — ignora il redirect
} }
@@ -87,13 +100,13 @@ router.post('/login', async (req, res) => {
} }
res.cookie('auth_token', token, cookieOptions); res.cookie('auth_token', token, cookieOptions);
res.clearCookie('_csrf');
const destination = redirect || CONSOLE_URL; const destination = safeRedirect || CONSOLE_URL;
res.redirect(destination); res.redirect(destination);
} catch (err) { } catch (err) {
console.error('[AUTH] Login failed:', err.message); console.error('[AUTH] Login failed:', err.message);
// Mai rivelare se è l'utente o la password ad essere sbagliati return loginRedirect('invalid_credentials', safeRedirect);
res.render('loginpage', { error: 'Credenziali non valide', redirect: redirect || '' });
} }
}); });

View File

@@ -1,8 +1,25 @@
const router = require('express').Router(); 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) => { router.get('/login', (req, res) => {
const redirect = req.query.redirect || ''; 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;

View File

@@ -4,7 +4,7 @@ const userAuth = require('../../middlewares/user.security');
router.use(userAuth); router.use(userAuth);
router.get('/', (req, res) => { router.get('/', (req, res) => {
res.render('sessions'); res.render('sessions', { user: req.user });
}); });
module.exports = router; module.exports = router;

View File

@@ -1,7 +1,10 @@
const router = require('express').Router(); const router = require('express').Router();
const userAuth = require('../../middlewares/user.security');
router.use(userAuth);
router.get('/', (req, res) => { router.get('/', (req, res) => {
res.render('user'); res.render('user', { user: req.user });
}); });
module.exports = router; module.exports = router;

View File

@@ -15,7 +15,8 @@
--header-border: #e2e8f0; --header-border: #e2e8f0;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); --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-md: 8px;
--radius-lg: 12px; --radius-lg: 12px;
} }
@@ -26,21 +27,21 @@
} }
@font-face { @font-face {
font-family: 'Normal'; font-family: "Normal";
src: url('../font/Quicksand-VariableFont_wght.ttf'); src: url("../font/sans-flex.ttf");
font-weight: 400; font-weight: 400;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: 'Bold'; font-family: "Bold";
src: url('../font/Quicksand-VariableFont_wght.ttf'); src: url("../font/sans-flex.ttf");
font-weight: 700; font-weight: 700;
font-style: normal; font-style: normal;
} }
body { body {
font-family: 'Normal', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-family: "Normal", Arial;
color: var(--text-primary); color: var(--text-primary);
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
@@ -53,7 +54,7 @@ button {
color: var(--text-primary); color: var(--text-primary);
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
font-family: 'Bold', inherit; font-family: "Bold", inherit;
cursor: pointer; cursor: pointer;
transition: all 0.5s cubic-bezier(0.8, 0, 0.2, 1); transition: all 0.5s cubic-bezier(0.8, 0, 0.2, 1);
white-space: nowrap; white-space: nowrap;
@@ -79,13 +80,11 @@ button.prominent:hover {
box-shadow: var(--shadow-md); box-shadow: var(--shadow-md);
} }
button.prominent:active { button.prominent:active {
transform: translateY(1px); transform: translateY(1px);
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
} }
/* INFO PANEL */ /* INFO PANEL */
.info-panel { .info-panel {
@@ -111,9 +110,6 @@ button.prominent:active {
transition: transform 0.12s ease; transition: transform 0.12s ease;
} }
/* GRID & CARD ITEMS */ /* GRID & CARD ITEMS */
.grid { .grid {
@@ -132,7 +128,9 @@ button.prominent:active {
border-radius: 20px; border-radius: 20px;
text-decoration: none; text-decoration: none;
color: var(--text-primary); 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 { .card h3 {
@@ -158,10 +156,6 @@ button.prominent:active {
grid-column: 1 / -1; grid-column: 1 / -1;
} }
/* HEADER */ /* HEADER */
.header { .header {
@@ -179,7 +173,6 @@ button.prominent:active {
user-select: none; user-select: none;
} }
.header h1 { .header h1 {
color: var(--text-primary); color: var(--text-primary);
font-size: 1.25rem; font-size: 1.25rem;
@@ -198,4 +191,4 @@ button.prominent:active {
font-weight: 500; font-weight: 500;
color: var(--text-secondary); color: var(--text-secondary);
padding-inline: 5px; padding-inline: 5px;
} }

View File

@@ -1,18 +1,19 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="it">
<html>
<head> <head>
<meta charset="UTF-8">
<link rel="stylesheet" href="../static/style/style.css"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../static/style/login.css" </head> <title>Login — Console MEB</title>
<link rel="stylesheet" href="/static/style/style.css">
<link rel="stylesheet" href="/static/style/login.css">
</head>
<body> <body>
<div class="container"> <div class="container">
<div class="login"> <div class="login">
<div class="header"> <div class="header">
<h1>Accedi alla <span class="prominent-title">Console MEB</span></h1> <h1>Accedi alla <span class="prominent-title">Console MEB</span></h1>
</div> </div>
{% if error %} {% if error %}
@@ -21,19 +22,20 @@
<form action="/api/auth/login" method="post"> <form action="/api/auth/login" method="post">
<input type="hidden" name="redirect" value="{{ redirect }}"> <input type="hidden" name="redirect" value="{{ redirect }}">
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
<div class="group"> <div class="group">
<label for="username">Username</label> <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>
<div class="group"> <div class="group">
<label for="password">Password</label> <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> </div>
<button type="submit">Login</button> <button type="submit" class="prominent">Login</button>
</form> </form>
</div> </div>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -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>

View File

@@ -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>

View File

@@ -63,8 +63,7 @@ function getToken(header) {
return parts[1]; return parts[1];
} }
//TODO: valutare se modificare in return null per accettare solo metodo Bearer Authorization Token, return null;
return header;
} }
module.exports = { generateToken, verifyToken, getToken }; module.exports = { generateToken, verifyToken, getToken };

View File

@@ -8,8 +8,8 @@ const saltRounds = 12;
* @param {string} password - Password da hashare * @param {string} password - Password da hashare
* @returns {string} - Hash della password * @returns {string} - Hash della password
*/ */
function hashPassword(password) { async function hashPassword(password) {
return bcrypt.hashSync(password, saltRounds); return bcrypt.hash(password, saltRounds);
} }
/** /**
@@ -18,8 +18,8 @@ function hashPassword(password) {
* @param {string} hash - Hash della password * @param {string} hash - Hash della password
* @returns {boolean} - True se la password è corretta, false altrimenti * @returns {boolean} - True se la password è corretta, false altrimenti
*/ */
function verifyPassword(password, hash) { async function verifyPassword(password, hash) {
return bcrypt.compareSync(password, hash); return bcrypt.compare(password, hash);
} }
/** /**

View File

@@ -23,7 +23,7 @@ services:
- "3006:3006" - "3006:3006"
labels: labels:
- "traefik.enable=true" - "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.routers.auth.entrypoints=websecure"
- "traefik.http.services.auth.loadbalancer.server.port=3006" - "traefik.http.services.auth.loadbalancer.server.port=3006"
- "traefik.docker.network=meb-public" - "traefik.docker.network=meb-public"
@@ -46,7 +46,7 @@ services:
- meb-private - meb-private
labels: labels:
- "traefik.enable=true" - "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.routers.api.entrypoints=websecure"
- "traefik.http.services.api.loadbalancer.server.port=3003" - "traefik.http.services.api.loadbalancer.server.port=3003"
- "traefik.http.routers.api.tls.certresolver=letsencrypt" - "traefik.http.routers.api.tls.certresolver=letsencrypt"
@@ -68,7 +68,7 @@ services:
- meb-private - meb-private
labels: labels:
- "traefik.enable=true" - "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.routers.console.entrypoints=websecure"
- "traefik.http.services.console.loadbalancer.server.port=3004" - "traefik.http.services.console.loadbalancer.server.port=3004"
- "traefik.http.routers.console.tls.certresolver=letsencrypt" - "traefik.http.routers.console.tls.certresolver=letsencrypt"
@@ -90,7 +90,7 @@ services:
- meb-public - meb-public
labels: labels:
- "traefik.enable=true" - "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.routers.realtime.entrypoints=websecure"
- "traefik.http.services.realtime.loadbalancer.server.port=3000" - "traefik.http.services.realtime.loadbalancer.server.port=3000"
- "traefik.http.routers.realtime.tls.certresolver=letsencrypt" - "traefik.http.routers.realtime.tls.certresolver=letsencrypt"
@@ -115,7 +115,7 @@ services:
- meb-public - meb-public
labels: labels:
- "traefik.enable=true" - "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.routers.ml.entrypoints=websecure"
- "traefik.http.services.ml.loadbalancer.server.port=8000" - "traefik.http.services.ml.loadbalancer.server.port=8000"
- "traefik.http.routers.ml.tls.certresolver=letsencrypt" - "traefik.http.routers.ml.tls.certresolver=letsencrypt"
@@ -139,7 +139,7 @@ services:
# - meb-internal # - meb-internal
# labels: # labels:
# - "traefik.enable=true" # - "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.routers.marine.entrypoints=web"
# - "traefik.http.services.marine.loadbalancer.server.port=8001" # - "traefik.http.services.marine.loadbalancer.server.port=8001"
# - "traefik.docker.network=meb-proxy-net" # - "traefik.docker.network=meb-proxy-net"
@@ -155,8 +155,8 @@ services:
# environment: # environment:
# - DATABASE_URL=postgresql://meb:meb@meb-postgres:5432/circuits # - DATABASE_URL=postgresql://meb:meb@meb-postgres:5432/circuits
# - AUTH_SERVICE_URL=http://auth:3001 # - AUTH_SERVICE_URL=http://auth:3001
# - AUTH_URL=http://auth.${URL_DOMAIN:-localhost} # - AUTH_URL=http://auth.${DOMAIN:-localhost}
# - API_URL=http://api.${URL_DOMAIN:-localhost} # - API_URL=http://api.${DOMAIN:-localhost}
# - NODE_ENV=${NODE_ENV:-development} # - NODE_ENV=${NODE_ENV:-development}
# volumes: # volumes:
# - ./circuits/src:/app/src # - ./circuits/src:/app/src
@@ -173,7 +173,7 @@ services:
# - meb-internal # - meb-internal
# labels: # labels:
# - "traefik.enable=true" # - "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.routers.circuits.entrypoints=web"
# - "traefik.http.services.circuits.loadbalancer.server.port=3005" # - "traefik.http.services.circuits.loadbalancer.server.port=3005"
# - "traefik.docker.network=meb-proxy-net" # - "traefik.docker.network=meb-proxy-net"