Ctrl K
ComunidadGuías…

login.html

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>InfraNet Pro — Login</title>
<!-- Stessa icona di netmapper.html (glifo Font Awesome `fa-network-wired`, U+F6FF,
     reso in PNG): qui SERVE che non sia una rotta, perche' questa pagina la vede
     chi non ha ancora una sessione. -->
<link rel="icon" type="image/png" href="data:image/png;base64,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">
<link rel="stylesheet" href="/vendor/fontawesome/css/all.min.css">
<style>
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

  :root {
    --bg:           #0a0e14;
    --panel-bg:     #131922;
    --panel-border: #232b35;
    --hairline:     rgba(255,255,255,.07);
    --accent:       #00d4ff;
    --accent-hover: #2fddff;
    --text-main:    #e6edf3;
    --text-muted:   #8b949e;
    --error:        #f85149;
    --input-bg:     #0e131a;
  }

  body {
    background:
      radial-gradient(900px 520px at 50% 28%, rgba(0,212,255,.06), transparent 70%),
      var(--bg);
    color: var(--text-main);
    font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
    -webkit-font-smoothing: antialiased;
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .login-wrap {
    width: 100%;
    max-width: 380px;
    padding: 24px;
  }

  .login-logo {
    text-align: center;
    margin-bottom: 30px;
  }
  .login-logo i {
    width: 64px;
    height: 64px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 1.9rem;
    color: var(--accent);
    background: rgba(0,212,255,.10);
    border: 1px solid rgba(0,212,255,.35);
    border-radius: 16px;
    box-shadow: 0 0 24px rgba(0,212,255,.20);
    margin-bottom: 14px;
  }
  .login-logo h1 {
    font-size: 1.5rem;
    font-weight: 800;
    letter-spacing: -0.4px;
  }
  .login-logo p {
    font-size: 0.8rem;
    color: var(--text-muted);
    margin-top: 5px;
  }

  .login-card {
    background: var(--panel-bg);
    border: 1px solid var(--hairline);
    border-radius: 16px;
    padding: 28px 32px;
    box-shadow: 0 12px 34px rgba(0,0,0,.55);
  }

  .form-group {
    margin-bottom: 18px;
  }
  label {
    display: block;
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    margin-bottom: 6px;
  }
  .input-wrap {
    position: relative;
  }
  .input-wrap i {
    position: absolute;
    left: 13px;
    top: 50%;
    transform: translateY(-50%);
    color: var(--text-muted);
    font-size: 0.85rem;
    pointer-events: none;
  }
  /* Occhio "mostra password" a destra (annulla la regola dell'icona sinistra sull'<i> interno). */
  .pw-eye-login {
    position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
    background: none; border: 0; padding: 4px 6px; margin: 0; cursor: pointer;
    color: var(--text-muted); font-size: 0.85rem; line-height: 1;
  }
  .pw-eye-login:hover { color: var(--text-main); }
  .pw-eye-login i { position: static; left: auto; transform: none; pointer-events: auto; color: inherit; font-size: inherit; }
  #password { padding-right: 34px; }
  input[type=text], input[type=password] {
    width: 100%;
    padding: 11px 12px 11px 38px;
    background: var(--input-bg);
    border: 1px solid var(--hairline);
    border-radius: 10px;
    color: var(--text-main);
    font-size: 0.92rem;
    outline: none;
    transition: border-color .18s, box-shadow .18s;
  }
  input:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px rgba(0,212,255,.14);
  }

  .btn-login {
    width: 100%;
    padding: 12px;
    background: var(--accent);
    color: #04141a;
    border: none;
    border-radius: 10px;
    font-size: 0.95rem;
    font-weight: 700;
    cursor: pointer;
    transition: background .18s, opacity .18s, filter .18s;
    margin-top: 6px;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    box-shadow: 0 0 18px rgba(0,212,255,.38);
  }
  .btn-login:hover { background: var(--accent-hover); filter: brightness(1.04); }
  .btn-login:disabled { opacity: .55; cursor: not-allowed; box-shadow: none; }

  .error-msg {
    display: none;
    background: rgba(248,81,73,.12);
    border: 1px solid rgba(248,81,73,.4);
    border-radius: 8px;
    padding: 9px 12px;
    font-size: 0.82rem;
    color: var(--error);
    margin-bottom: 16px;
  }
  .error-msg.visible { display: flex; align-items: center; gap: 8px; }

  .login-footer {
    text-align: center;
    margin-top: 22px;
    font-size: 0.72rem;
    line-height: 1.5;
    color: var(--text-muted);
  }
  .login-footer .lf-app {
    font-weight: 600;
    color: var(--text-main);
    margin-bottom: 3px;
  }
  .login-footer .lf-links { margin-top: 6px; }
  .login-footer .lf-links span { opacity: .45; margin: 0 5px; }
  .login-footer a {
    color: var(--text-muted);
    text-decoration: none;
    border-bottom: 1px dotted var(--panel-border);
    transition: color .15s, border-color .15s;
  }
  .login-footer a:hover { color: var(--accent); border-bottom-color: var(--accent); }

  .lang-switch {
    display: flex;
    justify-content: flex-end;
    gap: 4px;
    margin-bottom: 14px;
  }
  .lang-switch button {
    background: transparent;
    border: 1px solid var(--hairline);
    color: var(--text-muted);
    font-size: 0.72rem;
    font-weight: 600;
    padding: 3px 9px;
    border-radius: 8px;
    cursor: pointer;
    transition: color .15s, border-color .15s, background .15s;
  }
  .lang-switch button:hover { color: var(--text-main); border-color: var(--text-muted); }
  .lang-switch button.active { background: var(--accent); color: #04141a; border-color: var(--accent); }

  @keyframes spin { to { transform: rotate(360deg); } }
  .fa-spin-inline { animation: spin .7s linear infinite; }
</style>
</head>
<body>

<div class="login-wrap">
  <div class="login-logo">
    <i class="fas fa-network-wired"></i>
    <h1>Infra<span style="color:#00d4ff">Net</span> Pro</h1>
    <p>Network Infrastructure Manager</p>
  </div>

  <div class="login-card">
    <div class="lang-switch">
      <button type="button" data-lang="it">IT</button>
      <button type="button" data-lang="en">EN</button>
    </div>

    <div class="error-msg" id="err-msg">
      <i class="fas fa-exclamation-circle"></i>
      <span id="err-text" data-i18n="badCreds">Credenziali non valide</span>
    </div>

    <form id="login-form" onsubmit="doLogin(event)">
      <div class="form-group">
        <label for="username" data-i18n="user">Utente</label>
        <div class="input-wrap">
          <i class="fas fa-user"></i>
          <input type="text" id="username" name="username"
                 placeholder="nome utente" data-i18n-ph="userPh" autocomplete="username" autofocus required>
        </div>
      </div>

      <div class="form-group">
        <label for="password" data-i18n="pwd">Password</label>
        <div class="input-wrap">
          <i class="fas fa-lock"></i>
          <input type="password" id="password" name="password"
                 placeholder="••••••••" autocomplete="current-password" required>
          <button type="button" class="pw-eye-login" id="pw-eye" aria-label="Mostra password"
                  onclick="var p=document.getElementById('password'),i=this.querySelector('i'),s=p.type==='password';p.type=s?'text':'password';i.className=s?'fas fa-eye-slash':'fas fa-eye';this.setAttribute('aria-label',s?'Nascondi password':'Mostra password');">
            <i class="fas fa-eye"></i>
          </button>
        </div>
      </div>

      <button type="submit" class="btn-login" id="btn-login">
        <i class="fas fa-sign-in-alt" id="btn-icon"></i>
        <span id="btn-text" data-i18n="signin">Accedi</span>
      </button>
    </form>
  </div>

  <div class="login-footer">
    <div class="lf-app">InfraNet Pro v2.12.0</div>
    <div>© 2026 muttley1973 · <span data-i18n="license">Licenza</span> <b>AGPL-3.0-or-later</b></div>
    <div data-i18n="freeSw">software libero, senza garanzia</div>
    <div class="lf-links">
      <!-- I manuali NON stanno nell'immagine (.dockerignore esclude *.pdf, di
           proposito): il link va alle RELEASE, dove sono allegati come asset e
           seguono la versione. Qui in fondo al login perché si veda anche prima
           di entrare — è dove guarda chi ha appena tirato su il container. -->
      <a href="https://github.com/muttley1973/infranetpro/releases/latest" target="_blank" rel="noopener noreferrer" data-i18n="manual">Manuale (IT/EN)</a>
      <span>·</span>
      <a href="https://github.com/muttley1973/infranetpro" target="_blank" rel="noopener" data-i18n="srcCode">Codice sorgente</a>
      <span>·</span>
      <a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener" data-i18n="viewLic">Vedi licenza</a>
      <span>·</span>
      <a href="mailto:iu3npr@gmail.com?subject=InfraNet%20Pro%20%E2%80%94%20licenza%20commerciale" data-i18n="commLic">Licenza commerciale</a>
    </div>
  </div>
</div>

<script>
// --- i18n della pagina di login (autonoma, fuori dal bundle) ---
// Scrive sulla stessa chiave dell'app (infranet.lang) → la scelta qui si
// propaga dentro l'app dopo l'accesso. Switch speculare in-app nel menù utente.
const LANG_KEY = 'infranet.lang';
const I18N = {
  it: {
    user: 'Utente', userPh: 'nome utente', pwd: 'Password',
    signin: 'Accedi', signingIn: 'Accesso in corso…',
    badCreds: 'Credenziali non valide',
    netErr: 'Errore di rete. Controlla che il server sia attivo.',
    license: 'Licenza', freeSw: 'software libero, senza garanzia',
    manual: 'Manuale (IT/EN)',
    srcCode: 'Codice sorgente', viewLic: 'Vedi licenza', commLic: 'Licenza commerciale',
  },
  en: {
    user: 'Username', userPh: 'username', pwd: 'Password',
    signin: 'Sign in', signingIn: 'Signing in…',
    badCreds: 'Invalid credentials',
    netErr: 'Network error. Check that the server is running.',
    license: 'License', freeSw: 'free software, no warranty',
    manual: 'Manual (IT/EN)',
    srcCode: 'Source code', viewLic: 'View license', commLic: 'Commercial license',
  },
};
let LANG = (function () {
  try { return localStorage.getItem(LANG_KEY) === 'en' ? 'en' : 'it'; } catch (_) { return 'it'; }
})();
function t(k) { return (I18N[LANG] && I18N[LANG][k]) || I18N.it[k] || k; }
function applyLang() {
  document.documentElement.lang = LANG;
  document.querySelectorAll('[data-i18n]').forEach(el => { el.textContent = t(el.getAttribute('data-i18n')); });
  document.querySelectorAll('[data-i18n-ph]').forEach(el => { el.placeholder = t(el.getAttribute('data-i18n-ph')); });
  document.querySelectorAll('.lang-switch button').forEach(b => b.classList.toggle('active', b.dataset.lang === LANG));
}
function setLang(l) {
  LANG = l === 'en' ? 'en' : 'it';
  try { localStorage.setItem(LANG_KEY, LANG); } catch (_) {}
  applyLang();
}
document.querySelectorAll('.lang-switch button').forEach(b => b.addEventListener('click', () => setLang(b.dataset.lang)));
applyLang();

async function doLogin(e) {
  e.preventDefault();
  const btn  = document.getElementById('btn-login');
  const icon = document.getElementById('btn-icon');
  const txt  = document.getElementById('btn-text');
  const err  = document.getElementById('err-msg');
  const errT = document.getElementById('err-text');

  btn.disabled = true;
  icon.className = 'fas fa-spinner fa-spin-inline';
  txt.textContent = t('signingIn');
  err.classList.remove('visible');

  try {
    const res = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        username: document.getElementById('username').value.trim(),
        password: document.getElementById('password').value,
      }),
    });
    const data = await res.json();
    if (data.ok) {
      window.location.href = '/';
    } else {
      errT.textContent = data.error || t('badCreds');
      err.classList.add('visible');
      document.getElementById('password').value = '';
      document.getElementById('password').focus();
    }
  } catch (_) {
    errT.textContent = t('netErr');
    err.classList.add('visible');
  } finally {
    btn.disabled = false;
    icon.className = 'fas fa-sign-in-alt';
    txt.textContent = t('signin');
  }
}
</script>
</body>
</html>