feat(auth): Added UIs + Security middlewares and basic API for user and sessions. Added also shared cookie between pages of the meb server to authentication persist

This commit is contained in:
Giuseppe Raffa
2026-05-29 14:03:55 +02:00
parent 47faa41eb9
commit a0cb98bd06
8 changed files with 400 additions and 404 deletions

View File

@@ -6,7 +6,7 @@ import { fileURLToPath } from 'url';
import { authRouter } from './routes/auth.js';
import { userAPIs } from './routes/users.js';
import { sessionsAPIs } from './routes/sessions.js';
import { pagesAPIs } from './routes/pages.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -33,7 +33,8 @@ app.get('/health', (_req, res) => {
});
});
// Pagine web pubbliche (HTML) — /login, /profile, /config.js
// Pagine web
import { pagesAPIs } from './routes/pages.js';
app.use('/', pagesAPIs);
// API JSON
@@ -47,6 +48,6 @@ app.use((err, _req, res, _next) => {
res.status(500).json({ error: 'internal_error' });
});
app.listen(Number(process.env.PORT ?? 3000), '0.0.0.0', () => {
app.listen(3000, '0.0.0.0', () => {
console.log('Auth started');
});

View File

@@ -1,4 +1,4 @@
const token = process.env.INTERNAL_API_TOKEN;
const token = process.env.INTERNAL_TOKEN;
export function internalware(req, res, next) {
const header = req.get('X-Internal-Token');

View File

@@ -3,14 +3,17 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/static/styles/style.css" />
<link rel="stylesheet" href="../static/styles/style.css" />
<title>MEB — Accedi</title>
</head>
<body>
<div class="login-container">
<div class="login-logo">MEB</div>
<h1>Bentornato</h1>
<p>Inserisci le tue credenziali per accedere alla console.</p>
<!--<div class="login-logo">MEB</div>-->
<div class="login-logo">
<img src="../static/imgs/logo.svg" alt="Logo MEB" width="60" height="60">
</div>
<h1>Accedi</h1>
<p>Inserisci le tue credenziali per accedere ai servizi.</p>
<form class="login-form" id="loginForm">
<div class="input-group">
@@ -19,7 +22,6 @@
type="text"
id="username"
name="username"
placeholder="Il tuo username"
autocomplete="username"
/>
</div>
@@ -29,7 +31,6 @@
type="password"
id="password"
name="password"
placeholder="La tua password"
autocomplete="current-password"
/>
</div>
@@ -79,11 +80,17 @@
// Redirect al profilo. Il cookie httpOnly è già stato impostato
// dal server nella response; sarà valido anche per console
// (stesso host:port in dev, stesso dominio padre in prod).
const next = new URLSearchParams(location.search).get("next");
const next = new URLSearchParams(location.search).get(
"next",
);
window.location.href = next || "/profile";
} else {
let data = {};
try { data = await res.json(); } catch { /* ignore */ }
try {
data = await res.json();
} catch {
/* ignore */
}
errorMsg.textContent =
data.message || "Errore durante il login.";
}

View File

@@ -3,14 +3,13 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/static/styles/style.css" />
<script src="/config.js"></script>
<link rel="stylesheet" href="../static/styles/style.css" />
<title>MEB — Profilo</title>
</head>
<body class="page-profile">
<div class="profile-shell">
<header class="profile-topbar">
<div class="profile-brand">MEB</div>
<img src="../static/imgs/logo.svg" alt="Logo MEB" width="30" height="30">
<nav class="profile-nav" id="quickLinks">
<!-- popolato da JS in base a window.MEB_CONFIG -->
</nav>
@@ -62,7 +61,13 @@
</div>
<script>
const cfg = window.MEB_CONFIG || {};
const cfg = {
env: process.env.NODE_ENV,
console: process.env.CONSOLE_INTERNAL_URL,
api: process.env.API_INTERNAL_URL,
ml: process.env.ML_INTERNAL_URL,
auth: process.env.AUTH_INTERNAL_URL,
}
const $ = (id) => document.getElementById(id);
const fmtDate = (iso) => {
@@ -93,7 +98,7 @@
const links = [];
if (cfg.console)
links.push({ label: "Console", url: cfg.console });
if (cfg.api) links.push({ label: "API", url: cfg.api });
if (cfg.api) links.push({ label: "ML", url: cfg.ml });
nav.innerHTML = links
.map(
(l) =>
@@ -132,17 +137,21 @@
const thisSession = payload.thisSession || {};
$("username").textContent = user.username || "—";
$("memberSince").textContent =
"Membro da " + fmtDate(user.created_at);
"Creato il " + fmtDate(user.created_at);
$("userId").textContent = user.id || "—";
$("currentSessionId").textContent = thisSession.id || "—";
const initial = (user.username || "?").slice(0, 1).toUpperCase();
const initial = (user.username || "?")
.slice(0, 1)
.toUpperCase();
$("avatar").textContent = initial;
}
function renderSessions(sessions) {
const list = $("sessionsList");
$("sessionsCount").textContent =
sessions.length + " session" + (sessions.length === 1 ? "e" : "i");
sessions.length +
" session" +
(sessions.length === 1 ? "e" : "i");
if (!sessions.length) {
list.innerHTML =

View File

@@ -60,6 +60,9 @@ router.post('/login', async (req, res) => {
await redis.set(`user:online:${user.id}`, '1', 'EX', 60);
res.cookie(cookieName, jtoken, cookieOptions);
//TODO: Rimuovere, solo per test, basta inviare sendCode(200)
res.json({
ok: true,
user: user.id,

View File

@@ -7,39 +7,40 @@ const pagesDirectory = path.join(__dirname, "../pages");
const router = Router();
// Redirect intelligente sulla root: se non loggato → login, altrimenti profile.
// Il check vero è fatto client-side dalla pagina target tramite /api/users/me;
// qui ci basiamo solo sulla presenza del cookie per scegliere la destinazione.
// Redirect root: se non loggato → login, altrimenti profile.
router.get('/', (req, res) => {
const cookieName = process.env.COOKIE_NAME;
if (req.cookies?.[cookieName]) return res.redirect('/profile');
return res.redirect('/login');
});
// Pagine WEB
router.get('/login', (_req, res) => {
res.sendFile(path.join(pagesDirectory, 'login.html'));
});
router.get('/profile', (_req, res) => {
// L'auth è verificata client-side: la pagina fetch-a /api/users/me
// e se 401 redirige a /login. Pattern semplice da SPA.
res.sendFile(path.join(pagesDirectory, 'profile.html'));
});
// Endpoint dinamico che espone la config runtime alle pagine HTML.
//TODO: Vedere se serve davvero
// API di configurazione per le pagine HTML
// Le pagine fanno <script src="/config.js"></script> e poi leggono window.MEB_CONFIG.
// In questo modo gli URL dei servizi (console, api) sono iniettati dal server e
// cambiano automaticamente fra dev e prod senza toccare l'HTML.
router.get('/config.js', (_req, res) => {
const config = {
env: process.env.NODE_ENV || 'development',
console: process.env.CONSOLE_PUBLIC_URL || 'http://localhost:4003',
api: process.env.API_PUBLIC_URL || 'http://localhost:4000',
auth: process.env.AUTH_PUBLIC_URL || '', // vuoto = same-origin (la pagina è servita da auth)
};
res.type('application/javascript')
.set('Cache-Control', 'no-store')
.send(`window.MEB_CONFIG = Object.freeze(${JSON.stringify(config)});`);
});
// router.get('/config.js', (_req, res) => {
// const config = {
// env: process.env.NODE_ENV || 'development',
// console: process.env.CONSOLE_PUBLIC_URL || 'http://localhost:4003',
// api: process.env.API_PUBLIC_URL || 'http://localhost:4000',
// ml: process.env.ML_PUBLIC_URL || 'http://localhost:4005',
// auth: process.env.AUTH_PUBLIC_URL || '', // vuoto = same-origin (la pagina è servita da auth)
// };
// res.type('application/javascript')
// .set('Cache-Control', 'no-store')
// .send(`window.MEB_CONFIG = Object.freeze(${JSON.stringify(config)});`);
// });
export { router as pagesAPIs };

View File

@@ -0,0 +1,11 @@
<svg width="60" height="60" viewBox="0 0 165 165" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.41268 115.182C-2.24481 105.815 -0.286055 84.9776 3.2232 74.5486C10.3874 53.2568 32.4719 37.0773 55.1657 45.4929C65.7258 49.4086 73.7361 55.616 78.603 65.8989C79.3478 67.3026 79.8223 67.5953 79.8913 68.9309C79.298 72.6449 74.0964 71.9417 70.9252 72.4412C65.0975 73.3595 60.3911 76.1645 55.8237 79.8009C50.6258 84.0946 44.3683 90.9825 40.9808 96.8873C34.9317 107.43 29.3003 123.961 13.8678 121.222C11.0472 120.722 5.60492 117.891 4.41268 115.182Z" fill="#4AB1D5"/>
<path d="M4.41268 115.182C6.06876 112.482 8.57956 106.551 10.2436 103.42C15.7428 93.0657 23.3266 83.9577 32.5191 76.6677C46.201 65.7929 61.8319 63.9885 78.603 65.8989C79.3478 67.3026 79.8223 67.5953 79.8913 68.9309C79.298 72.6449 74.0964 71.9417 70.9252 72.4412C65.0975 73.3595 60.3911 76.1645 55.8237 79.8009C50.6258 84.0946 44.3683 90.9825 40.9808 96.8873C34.9317 107.43 29.3003 123.961 13.8678 121.222C11.0472 120.722 5.60492 117.891 4.41268 115.182Z" fill="#025492"/>
<path d="M49.9604 4.7052C79.7744 -11.3406 131.163 15.8119 120.046 53.4879C116.6 65.1691 108.75 73.6614 98.3518 79.7457C92.3064 84.2288 92.565 72.7179 92.1158 70.289C89.76 58.7621 80.2046 49.6204 70.6012 43.0095C60.5516 36.0915 38.7257 29.6601 43.4209 13.0162C44.317 9.83926 46.756 5.90833 49.9604 4.7052Z" fill="#4AB1D5"/>
<path d="M98.3518 79.7457C92.3064 84.2288 92.565 72.7179 92.1158 70.289C89.76 58.7621 80.2046 49.6204 70.6012 43.0095C60.5516 36.0915 38.7257 29.6601 43.4209 13.0162C44.317 9.83926 46.756 5.90833 49.9604 4.7052C52.8382 7.12963 61.4979 10.782 65.1317 13.0455C82.3846 23.7917 96.58 39.2595 98.8098 60.2267C99.8646 68.5559 98.111 73.348 98.3518 79.7457Z" fill="#025492"/>
<path d="M65.9204 85.7895C67.0091 85.2138 67.9348 84.6711 69.0217 84.6711C72.9398 86.1186 71.0808 92.0714 72.1099 95.2869C73.5986 99.9374 76.8791 106.052 79.9865 109.783C84.2954 114.938 89.4004 119.372 95.1084 122.921C97.1012 124.182 101.169 126.588 103.382 127.392C116.08 132.003 130.562 148.765 115.05 160.09C104.949 166.488 88.0815 165.341 76.8479 162.167C39.2599 151.545 29.3155 105.145 65.9204 85.7895Z" fill="#4AB1D5"/>
<path d="M65.9204 85.7895C67.0091 85.2138 67.9348 84.6711 69.0217 84.6711C72.9398 86.1186 71.0808 92.0714 72.1099 95.2869C73.5986 99.9374 76.8791 106.052 79.9865 109.783C84.2954 114.938 89.4004 119.372 95.1084 122.921C97.1012 124.182 101.169 126.588 103.382 127.392C116.08 132.003 130.562 148.765 115.05 160.09C112.583 158.46 107.659 156.854 104.858 155.344C85.4192 144.863 68.3788 128.929 65.4947 105.943C64.6361 99.1018 65.3858 92.6956 65.9204 85.7895Z" fill="#025492"/>
<path d="M85.297 98.7725C84.8478 97.9461 84.6992 95.9267 84.9463 94.9635C85.4136 93.1407 89.7686 93.3906 91.6139 93.1666C100.675 92.0655 108.311 86.1518 114.524 79.821C117.314 76.9778 120.415 72.8952 122.452 69.4583C127.934 60.1292 131.229 48.8067 141.888 44.049C147.306 41.6313 157.62 44.3302 159.721 50.0298C171.559 70.1696 160.842 104.496 140.975 116.247C122.453 127.202 97.5734 119.444 86.6423 101.446C86.1574 100.649 85.6295 99.6262 85.297 98.7725Z" fill="#4AB1D5"/>
<path d="M85.297 98.7725C84.8478 97.9461 84.6992 95.9267 84.9463 94.9635C85.4136 93.1407 89.7686 93.3906 91.6139 93.1666C100.675 92.0655 108.311 86.1518 114.524 79.821C117.314 76.9778 120.415 72.8952 122.452 69.4583C127.934 60.1292 131.229 48.8067 141.888 44.049C147.306 41.6313 157.62 44.3302 159.721 50.0298C157.87 53.3284 156.064 56.8406 154.336 60.2178C143.327 81.7362 123.356 100.3 97.6749 99.1307C95.7764 99.0443 86.2785 98.5703 85.297 98.7725Z" fill="#025492"/>
<path d="M79.8223 72.5317C84.7401 71.3991 89.7444 74.0438 91.5712 78.7402C92.3399 80.7166 92.4684 82.8844 91.9386 84.9376C91.0221 88.4887 88.2561 91.2692 84.7052 92.2079C81.1605 93.1451 77.3855 92.1018 74.828 89.4786C72.2708 86.8554 71.329 83.0595 72.3637 79.548C73.3987 76.0365 76.2498 73.3547 79.8223 72.5317Z" fill="#025492"/>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -15,46 +15,42 @@
/* ── Variabili tema ── */
:root {
--bg-body: #0f1117;
--bg-card: #1a1d27;
--border: #2a2d3a;
--text-primary: #f0f0f0;
--text-secondary: #6b7280;
--text-label: #9ca3af;
--input-bg: #0f1117;
--input-placeholder: #3d4150;
--accent: #4f8ef7;
--accent-hover: #3b7de8;
--accent-active: #2f6dd4;
--error: #f87171;
--shadow: rgba(0, 0, 0, 0.4);
--grad-1: #0f1117;
--grad-2: #1a1d27;
--grad-3: #0d1520;
--grad-4: #111827;
--bg: black;
--foreground: white;
--foreground-secondary: #999999;
--foreground-tertiary: #404040;
--foreground-quaternary: #262626;
--primary: #51d5ff;
--secondary: rgb(81, 213, 255, 0.3);
--tertiary: rgb(81, 213, 255, 0.18);
--hover: #3b7de8;
--active: #2f6dd4;
--danger: #de090d;
--warning: #ff8306;
--success: #06ff13;
--box-shadow:
0 8px 32px var(--tertiary), 0 0 0 1px var(--foreground-quaternary);
}
@media (prefers-color-scheme: light) {
:root {
--bg-body: #eef1f7;
--bg-card: #ffffff;
--border: #dde1ec;
--text-primary: #111827;
--text-secondary: #6b7280;
--text-label: #4b5563;
--input-bg: #f5f7fc;
--input-placeholder: #b0b8cc;
--accent: #3b7de8;
--accent-hover: #2f6dd4;
--accent-active: #2260be;
--error: #dc2626;
--shadow: rgba(0, 0, 0, 0.1);
--bg: white;
--foreground: black;
--foreground-secondary: rgb(60, 60, 67, 0.6);
--foreground-tertiary: rgb(60, 60, 67, 0.3);
--foreground-quaternary: rgb(60, 60, 67, 0.18);
--grad-1: #dce8ff;
--grad-2: #eef1f7;
--grad-3: #d4e4fb;
--grad-4: #e8edf8;
--primary: #00559d;
--secondary: rgb(0, 85, 157, 0.3);
--tertiary: rgb(0, 85, 157, 0.18);
--hover: #056ac0;
--active: #1355c1;
--danger: #de090d;
--warning: #ff8306;
--success: #06ff13;
--box-shadow:
0 8px 32px rgba(0, 0, 0, 0.08),
0 0 0 1px var(--foreground-quaternary);
}
}
@@ -77,293 +73,24 @@ body {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(
135deg,
var(--grad-1),
var(--grad-2),
var(--grad-3),
var(--grad-4)
);
background: var(--bg);
color: var(--foreground);
background-size: 400% 400%;
animation: gradientShift 12s ease infinite;
}
/* ── Card ── */
.login-container {
width: 100%;
max-width: 400px;
padding: 48px 40px;
background-color: var(--bg-card);
border-radius: 16px;
border: 1px solid var(--border);
box-shadow: 0 24px 48px var(--shadow);
transition:
background-color 0.3s,
border-color 0.3s;
}
.login-logo {
font-size: 13px;
font-weight: bold;
letter-spacing: 4px;
color: var(--accent);
text-transform: uppercase;
margin-bottom: 24px;
}
.login-container h1 {
font-size: 26px;
color: var(--text-primary);
margin-bottom: 8px;
transition: color 0.3s;
}
.login-container p {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 32px;
line-height: 1.5;
transition: color 0.3s;
}
/* ── Form ── */
.login-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.input-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.input-group label {
font-size: 13px;
color: var(--text-label);
letter-spacing: 0.3px;
transition: color 0.3s;
}
.input-group input {
width: 100%;
padding: 12px 14px;
background-color: var(--input-bg);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-primary);
font-family: "Elms", sans-serif;
font-size: 15px;
outline: none;
transition:
border-color 0.2s,
background-color 0.3s,
color 0.3s;
}
.input-group input::placeholder {
color: var(--input-placeholder);
}
.input-group input:focus {
border-color: var(--accent);
}
.error-message {
font-size: 13px;
color: var(--error);
min-height: 18px;
transition: color 0.3s;
}
/* ── Bottone ── */
@keyframes btnPulse {
0% {
box-shadow: 0 0 0 0 rgba(79, 142, 247, 0.5);
}
70% {
box-shadow: 0 0 0 10px rgba(79, 142, 247, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(79, 142, 247, 0);
}
}
@keyframes btnSpin {
to {
transform: rotate(360deg);
}
}
.btn-login {
width: 100%;
padding: 13px;
background-color: var(--accent);
color: #fff;
border: none;
border-radius: 8px;
font-family: "Elms", sans-serif;
font-size: 15px;
cursor: pointer;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition:
background-color 0.2s,
transform 0.15s,
box-shadow 0.2s;
}
.btn-login::after {
content: "";
position: absolute;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.08);
top: 0;
left: -100%;
transition: left 0.3s ease;
}
.btn-login:hover::after {
left: 0;
}
.btn-login:hover {
background-color: var(--accent-hover);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(79, 142, 247, 0.35);
}
.btn-login:active {
background-color: var(--accent-active);
transform: translateY(0px);
box-shadow: none;
animation: btnPulse 0.4s ease-out;
}
.btn-login:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.btn-spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: #fff;
border-radius: 50%;
animation: btnSpin 0.7s linear infinite;
display: none;
flex-shrink: 0;
}
.btn-login.loading .btn-spinner {
display: block;
}
.btn-login.loading .btn-label {
opacity: 0.7;
}
/* ════════════════════════════════════════════════════════════
PAGINA PROFILO
COMPONENTI CONDIVISI
════════════════════════════════════════════════════════════ */
body.page-profile {
display: block;
align-items: stretch;
justify-content: stretch;
}
.profile-shell {
max-width: 960px;
margin: 0 auto;
padding: 32px 20px 80px;
}
/* ── Topbar ── */
.profile-topbar {
display: flex;
align-items: center;
gap: 24px;
padding: 12px 0 28px;
border-bottom: 1px solid var(--border);
margin-bottom: 32px;
}
.profile-brand {
font-size: 13px;
font-weight: bold;
letter-spacing: 4px;
color: var(--accent);
text-transform: uppercase;
}
.profile-nav {
display: flex;
gap: 8px;
flex: 1;
}
.nav-link {
padding: 8px 14px;
border-radius: 8px;
border: 1px solid var(--border);
color: var(--text-primary);
text-decoration: none;
font-size: 13px;
transition:
background 0.15s,
border-color 0.15s,
transform 0.1s;
}
.nav-link:hover {
border-color: var(--accent);
background: var(--input-bg);
transform: translateY(-1px);
}
.btn-ghost {
padding: 8px 16px;
background: transparent;
color: var(--text-primary);
border: 1px solid var(--border);
border-radius: 8px;
cursor: pointer;
font-family: "Elms", sans-serif;
font-size: 13px;
transition:
background 0.15s,
border-color 0.15s,
color 0.15s;
}
.btn-ghost:hover {
border-color: var(--error);
color: var(--error);
}
/* ── Main grid ── */
.profile-main {
display: grid;
gap: 24px;
}
/* ── Card base ── */
.card {
background-color: var(--bg-card);
border: 1px solid var(--border);
user-select: none;
-webkit-user-select: none;
background: var(--bg);
border: none;
border-radius: 16px;
padding: 28px;
box-shadow: 0 8px 24px var(--shadow);
box-shadow: var(--box-shadow);
}
.card-header {
@@ -376,51 +103,302 @@ body.page-profile {
.card-header h2 {
font-size: 16px;
font-weight: 500;
color: var(--text-primary);
color: var(--foreground);
}
.muted {
color: var(--text-secondary);
font-size: 13px;
color: var(--foreground-secondary);
}
.mono {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 12px;
word-break: break-all;
}
.badge {
font-size: 10px;
padding: 2px 8px;
border-radius: 10px;
background: var(--primary);
color: var(--bg);
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* ── Input condiviso ── */
.input-group {
user-select: none;
-webkit-user-select: none;
display: flex;
flex-direction: column;
gap: 6px;
}
.input-group label {
font-size: 13px;
color: var(--foreground-tertiary);
letter-spacing: 0.3px;
}
.input-group input {
width: 100%;
padding: 12px 14px;
background: var(--bg);
border: 0.4px solid var(--foreground-tertiary);
border-radius: 10px;
color: var(--foreground);
font-family: "Elms", sans-serif;
font-size: 15px;
outline: none;
transition: border-color 0.2s;
}
.input-group input::placeholder {
color: var(--foreground-tertiary);
}
.input-group input:focus {
border-color: var(--primary);
}
.error-message {
font-size: 13px;
color: var(--danger);
min-height: 18px;
}
/* ── Animazioni bottoni ── */
@keyframes btnPulse {
0% {
box-shadow: 0 0 0 0 var(--secondary);
}
70% {
box-shadow: 0 0 0 10px transparent;
}
100% {
box-shadow: 0 0 0 0 transparent;
}
}
@keyframes btnSpin {
to {
transform: rotate(360deg);
}
}
/* ── Bottone ghost (es. logout, revoca) ── */
.btn-ghost {
padding: 8px 16px;
background: transparent;
color: var(--foreground);
border: 0.4px solid var(--foreground-tertiary);
border-radius: 10px;
cursor: pointer;
font-family: "Elms", sans-serif;
font-size: 13px;
transition:
border-color 0.15s,
color 0.15s;
}
.btn-ghost:hover {
border-color: var(--danger);
color: var(--danger);
}
/* ════════════════════════════════════════════════════════════
PAGINA LOGIN
════════════════════════════════════════════════════════════ */
.login-container {
width: 100%;
max-width: 400px;
padding: 48px 40px;
background: var(--bg);
border: none;
border-radius: 16px;
box-shadow: var(--box-shadow);
}
.login-logo {
font-size: 12px;
font-weight: bold;
letter-spacing: 4px;
color: var(--primary);
text-transform: uppercase;
margin-bottom: 28px;
}
.login-container h1 {
font-size: 26px;
color: var(--foreground);
margin-bottom: 6px;
}
.login-container > p {
font-size: 14px;
color: var(--foreground-secondary);
margin-bottom: 32px;
line-height: 1.5;
}
.login-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.btn-login {
width: 100%;
padding: 13px;
background: var(--primary);
color: var(--bg);
border: none;
border-radius: 10px;
font-family: "Elms", sans-serif;
font-size: 15px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition:
opacity 0.2s,
transform 0.15s;
}
.btn-login:hover {
opacity: 0.85;
transform: translateY(-1px);
}
.btn-login:active {
opacity: 1;
transform: translateY(0);
animation: btnPulse 0.4s ease-out;
}
.btn-login:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.btn-spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(0, 0, 0, 0.25);
border-top-color: var(--bg);
border-radius: 50%;
animation: btnSpin 0.7s linear infinite;
display: none;
flex-shrink: 0;
}
.btn-login.loading .btn-spinner {
display: block;
}
.btn-login.loading .btn-label {
opacity: 0.8;
}
/* ════════════════════════════════════════════════════════════
PAGINA PROFILO
════════════════════════════════════════════════════════════ */
body.page-profile {
display: block;
}
.profile-shell {
max-width: 960px;
margin: 0 auto;
padding: 32px 20px 80px;
}
/* ── Topbar ── */
.profile-topbar {
display: flex;
align-items: center;
gap: 16px;
padding-bottom: 24px;
border-bottom: 0.4px solid var(--foreground-tertiary);
margin-bottom: 32px;
}
.profile-brand {
font-size: 12px;
font-weight: bold;
letter-spacing: 4px;
color: var(--primary);
text-transform: uppercase;
flex-shrink: 0;
}
.profile-nav {
display: flex;
gap: 8px;
flex: 1;
}
.nav-link {
padding: 6px 14px;
border-radius: 8px;
border: 0.4px solid var(--foreground-tertiary);
color: var(--foreground);
text-decoration: none;
font-size: 13px;
transition:
border-color 0.15s,
transform 0.1s;
}
.nav-link:hover {
border-color: var(--primary);
transform: translateY(-1px);
}
/* ── Main grid ── */
.profile-main {
display: grid;
gap: 20px;
}
/* ── Hero ── */
.profile-hero {
display: flex;
align-items: center;
gap: 24px;
gap: 20px;
}
.avatar {
width: 72px;
height: 72px;
width: 64px;
height: 64px;
border-radius: 50%;
background: linear-gradient(
135deg,
var(--accent),
var(--accent-active)
);
color: #fff;
background: var(--primary);
color: var(--bg);
display: flex;
align-items: center;
justify-content: center;
font-size: 30px;
font-size: 28px;
font-weight: bold;
flex-shrink: 0;
box-shadow: 0 4px 14px rgba(79, 142, 247, 0.35);
}
.hero-info h1 {
font-size: 24px;
color: var(--text-primary);
font-size: 22px;
color: var(--foreground);
margin-bottom: 4px;
}
/* ── Sessions list ── */
/* ── Sessioni ── */
.sessions-list {
display: flex;
flex-direction: column;
gap: 12px;
gap: 10px;
}
.session-row {
@@ -428,16 +406,15 @@ body.page-profile {
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 16px;
border: 1px solid var(--border);
padding: 14px 16px;
border: 0.4px solid var(--foreground-tertiary);
border-radius: 10px;
background: var(--input-bg);
background: var(--bg);
transition: border-color 0.15s;
}
.session-row.is-current {
border-color: var(--accent);
background: rgba(79, 142, 247, 0.05);
border-color: var(--primary);
}
.session-info {
@@ -447,7 +424,7 @@ body.page-profile {
.session-title {
font-size: 14px;
color: var(--text-primary);
color: var(--foreground);
margin-bottom: 4px;
display: flex;
align-items: center;
@@ -457,43 +434,35 @@ body.page-profile {
.session-meta {
font-size: 12px;
line-height: 1.6;
}
.badge {
font-size: 10px;
padding: 2px 8px;
border-radius: 10px;
background: var(--accent);
color: #fff;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--foreground-secondary);
}
.btn-revoke {
padding: 8px 14px;
padding: 6px 14px;
background: transparent;
color: var(--error);
border: 1px solid var(--border);
color: var(--danger);
border: 0.4px solid var(--foreground-tertiary);
border-radius: 8px;
cursor: pointer;
font-family: "Elms", sans-serif;
font-size: 12px;
flex-shrink: 0;
transition:
background 0.15s,
border-color 0.15s;
}
.btn-revoke:hover:not(:disabled) {
background: rgba(248, 113, 113, 0.1);
border-color: var(--error);
background: rgba(222, 9, 13, 0.08);
border-color: var(--danger);
}
.btn-revoke:disabled {
opacity: 0.6;
opacity: 0.5;
cursor: wait;
}
/* ── kv grid (account info) ── */
/* ── Griglia chiave/valore ── */
.kv-grid {
display: grid;
grid-template-columns: 180px 1fr;
@@ -502,17 +471,11 @@ body.page-profile {
}
.kv-grid dt {
color: var(--text-label);
color: var(--foreground-tertiary);
}
.kv-grid dd {
color: var(--text-primary);
}
.mono {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 12px;
word-break: break-all;
color: var(--foreground);
}
/* ── Toast ── */
@@ -523,16 +486,16 @@ body.page-profile {
transform: translateX(-50%) translateY(20px);
padding: 12px 20px;
border-radius: 10px;
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--text-primary);
box-shadow: 0 8px 24px var(--shadow);
background: var(--foreground-quaternary);
border: 0.4px solid var(--foreground-tertiary);
color: var(--foreground);
opacity: 0;
pointer-events: none;
transition:
opacity 0.2s,
transform 0.2s;
font-size: 13px;
white-space: nowrap;
}
.toast.show {
@@ -541,11 +504,10 @@ body.page-profile {
}
.toast.ok {
border-color: #22c55e;
border-color: var(--success);
}
.toast.err {
border-color: var(--error);
border-color: var(--danger);
}
/* ── Responsive ── */
@@ -572,6 +534,7 @@ body.page-profile {
.kv-grid dt {
margin-top: 12px;
font-size: 11px;
}
.session-row {
@@ -581,5 +544,6 @@ body.page-profile {
.btn-revoke {
width: 100%;
text-align: center;
}
}