255 lines
48 KiB
HTML
255 lines
48 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="fr">
|
||
<head>
|
||
<link rel="icon" type="image/x-icon" href="favicon.png">
|
||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<style>
|
||
|
||
:root{--bg:#0b1020;--panel:#131b31;--panel2:#1b2946;--text:#eef4ff;--muted:#9aa8c5;--blue:#5ca7ff;--green:#32d583;--red:#ff6b7a;--line:#283757}*{box-sizing:border-box}body{margin:0;background:linear-gradient(135deg,#0b1020,#111a32);color:var(--text);font:15px system-ui,sans-serif;min-height:100vh}.wrap{max-width:1100px;margin:auto;padding:28px 18px 48px}header,.topline{display:flex;justify-content:space-between;align-items:center;gap:16px}h1{font-size:27px;margin:0 0 5px}h2{font-size:17px;margin:0 0 18px}.sub,.muted,.label{color:var(--muted)}button{border:0;border-radius:10px;padding:11px 15px;background:var(--blue);color:#071326;font-weight:700;cursor:pointer}button.secondary{background:var(--panel2);color:var(--text)}nav{display:flex;gap:8px;margin:26px 0 18px}nav button:not(.active){background:var(--panel2);color:var(--text);box-shadow:none}nav button.active{background:var(--blue);color:#071326;box-shadow:0 0 0 2px #9bcaff66}.signal{font-weight:700}.signal-excellent{color:#32d583}.signal-bon{color:#8be28b}.signal-moyen{color:#ffad42}.signal-faible{color:#ff6b7a}.view{display:none}.view.active{display:block}.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:18px}.card{background:rgba(19,27,49,.94);border:1px solid var(--line);border-radius:16px;padding:19px;box-shadow:0 10px 30px #0002}.metric{font-size:29px;font-weight:800;margin-top:8px}.status{color:var(--green);display:flex;align-items:center;gap:8px}.dot{width:9px;height:9px;background:var(--green);border-radius:50%;box-shadow:0 0 12px var(--green)}.layout{display:grid;grid-template-columns:minmax(0,1fr) 340px;gap:18px;align-items:start}
|
||
.layout>section{grid-column:1}
|
||
.layout>aside{grid-column:2;position:sticky;top:18px;align-self:start}
|
||
.pill{padding:6px 10px;border-radius:20px;background:#173d35;color:var(--green);font-size:12px;font-weight:700}.table-wrap{overflow:auto}table{width:100%;border-collapse:collapse}th,td{text-align:left;padding:14px 10px;border-bottom:1px solid var(--line);white-space:nowrap}th{font-size:12px;color:var(--muted)}.client{display:flex;align-items:center;gap:10px}.avatar{width:34px;height:34px;border-radius:10px;background:#254a78;display:grid;place-items:center;font-weight:800}.badge{color:var(--green);font-size:12px}.info{display:grid;gap:13px}.info-row{display:flex;justify-content:space-between;border-bottom:1px solid var(--line);padding-bottom:12px}.info-row span{color:var(--muted)}input{display:block;width:100%;margin:7px 0 16px;padding:12px;border-radius:9px;border:1px solid var(--line);background:var(--panel2);color:var(--text);font-size:15px}.check{display:flex;align-items:center;gap:9px;margin:14px 0 20px}.check input{width:auto;margin:0}.notice{margin-top:16px;padding:13px;border-radius:10px;background:#2b2130;color:#ffb9c1;font-size:13px}.empty{text-align:center;color:var(--muted);padding:25px}@media(max-width:800px){.grid{grid-template-columns:repeat(2,1fr)}.layout{grid-template-columns:1fr}.layout>section,.layout>aside{grid-column:1}.layout>aside{position:static}
|
||
#settingsView>div[style*="grid-template-columns"]{grid-template-columns:1fr!important}}@media(max-width:480px){header{align-items:flex-start;flex-direction:column}header>div:last-child{width:100%;display:grid!important;grid-template-columns:1fr 1fr;gap:8px}header>div:last-child button:last-child{grid-column:1/-1}.grid{grid-template-columns:1fr 1fr}.metric{font-size:23px}.topline{align-items:flex-start;flex-direction:column}.card{padding:15px}#setupView .card{margin:25px auto!important}#setupView h1{font-size:24px}}
|
||
#loader{position:fixed;inset:0;z-index:1000;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;background:linear-gradient(135deg,#0b1020,#111a32);transition:opacity .35s}#loader.hide{opacity:0;pointer-events:none}.spinner{width:54px;height:54px;border-radius:50%;border:5px solid var(--panel2);border-top-color:var(--blue);animation:spin .9s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}#loaderText{color:var(--muted)}
|
||
html[data-theme="light"]{--bg:#f3f6fc;--panel:#ffffff;--panel2:#e8eef9;--text:#15203a;--muted:#5b6b8c;--green:#13a06a;--red:#e5484d;--line:#d5deee;color-scheme:light}
|
||
html[data-theme="light"] body,html[data-theme="light"] #loader{background:linear-gradient(135deg,#f3f6fc,#e3ebf8)}
|
||
html[data-theme="light"] .card{background:rgba(255,255,255,.96);box-shadow:0 10px 30px #1a2b4f14}
|
||
html[data-theme="light"] .pill{background:#d9f5e8}
|
||
html[data-theme="light"] .notice{background:#fde8ea;color:#a3202b}
|
||
html[data-theme="light"] .avatar{background:#cfe0fb;color:#15203a}
|
||
.recovery-card{background:var(--panel2);border:1px dashed var(--blue);border-radius:12px;padding:16px;margin:14px 0}
|
||
.recovery-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin:12px 0}
|
||
.code-chip{background:var(--bg);border:1px solid var(--line);padding:8px 10px;border-radius:8px;font-family:monospace;font-size:15px;font-weight:700;letter-spacing:1px;text-align:center;color:var(--blue);user-select:all}
|
||
.recovery-actions{display:flex;gap:10px;margin-top:10px;flex-wrap:wrap}
|
||
</style>
|
||
</head>
|
||
|
||
<body><div id="loader"><div class="spinner"></div><h2 class="app-title" style="margin:0">Administration de l’AP Wi‑Fi</h2><div id="loaderText">Chargement de l’API…</div></div><section id="authView" class="view active"><div class="card" style="max-width:520px;margin:80px auto"><h1 id="authTitle">Connexion</h1><p id="authIntro" class="muted">Connectez-vous pour accéder à <span class="app-title">l’administration de l’AP Wi‑Fi</span>.</p><label class="label" for="authUsername">Utilisateur</label><input id="authUsername" type="text" autocomplete="username" maxlength="32"><label class="label" for="authPassword">Mot de passe</label><div style="display:flex;gap:8px;margin:7px 0 16px"><input id="authPassword" type="password" autocomplete="current-password" maxlength="128" style="margin:0"><button id="toggleAuthPassword" class="secondary" type="button" aria-label="Afficher le mot de passe" title="Afficher le mot de passe"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.5-8 10-8 10 8 10 8-3.5 8-10 8S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/></svg></button></div><div id="authOtpBox" style="display:none"><label class="label" for="authOtp">Code de double identification (TOTP ou secours)</label><input id="authOtp" type="text" autocomplete="one-time-code" maxlength="11" placeholder="Code à 6 chiffres ou code de secours (ex: ABCDE-12345)"></div><label id="rememberUserBox" class="check"><input id="rememberUser" type="checkbox"> Se rappeler de moi</label><button id="authSubmit" type="button">Se connecter</button><span id="authMessage" class="muted" style="margin-left:10px"></span></div></section><main id="app" class="wrap" style="display:none">
|
||
<header><div><h1><span class="app-title">Administration de l’AP Wi‑Fi</span></h1><div class="sub">AP Manageur V1.0</div></div><div style="display:flex;gap:8px"><button id="apControl" type="button">Démarrer</button><button id="apRestart" class="secondary" type="button">Redémarrer</button><button id="refresh">Actualiser</button><button id="logout" class="secondary" type="button">Déconnexion</button></div></header>
|
||
<nav id="nav"><button id="dashboardTab" class="active" aria-selected="true">Tableau de bord</button><button id="settingsTab" class="secondary" aria-selected="false">Réglages</button><button id="logsTab" class="secondary" aria-selected="false">Journaux</button><button id="blockedTab" class="secondary" aria-selected="false">Appareils bloqués</button></nav>
|
||
<section id="setupView" class="view"><div class="card" style="max-width:650px;margin:60px auto"><h1>Configuration initiale</h1><p class="muted">Aucun réseau Wi‑Fi n’est configuré. Créez votre point d’accès avec un SSID et un mot de passe.</p><div class="notice"><strong>Caractéristiques requises</strong><br>SSID : 1 à 32 caractères.<br>Mot de passe WPA : 8 à 63 caractères, recommandé avec des lettres, des chiffres et des symboles.<br>Le réseau sera démarré automatiquement au démarrage de la machine.</div><label class="label" for="setupSsid">Nom du SSID</label><input id="setupSsid" type="text" maxlength="32" placeholder="Mon réseau Wi‑Fi"><label class="label" for="setupPassword">Mot de passe Wi‑Fi</label><div style="display:flex;gap:8px;margin:7px 0 16px"><input id="setupPassword" type="password" minlength="8" maxlength="63" placeholder="8 caractères minimum" style="margin:0"><button id="toggleSetupPassword" class="secondary" type="button" aria-label="Afficher le mot de passe" title="Afficher le mot de passe"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.5-8 10-8 10 8 10 8-3.5 8-10 8S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/></svg></button></div><label class="label" for="setupPasswordConfirm">Confirmation du mot de passe Wi‑Fi</label><div style="display:flex;gap:8px;margin:7px 0 16px"><input id="setupPasswordConfirm" type="password" minlength="8" maxlength="63" placeholder="Confirmation du mot de passe" style="margin:0"><button id="toggleSetupPasswordConfirm" class="secondary" type="button" aria-label="Afficher le mot de passe" title="Afficher le mot de passe"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.5-8 10-8 10 8 10 8-3.5 8-10 8S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/></svg></button></div><label class="label" for="setupSecurity">Sécurité Wi‑Fi</label><select id="setupSecurity" style="display:block;width:100%;margin:7px 0 16px;padding:12px;border-radius:9px;border:1px solid var(--line);background:var(--panel2);color:var(--text);font-size:15px"><option value="wpa1">WPA1</option><option value="wpa2" selected>WPA2</option><option value="wpa3">WPA3</option></select><button id="setupAp" type="button">Créer le réseau</button><span id="setupMessage" class="muted" style="margin-left:10px"></span></div></section>
|
||
<section id="dashboardView" class="view active">
|
||
<div class="grid"><div class="card"><div class="label">État de l’AP</div><div id="state" class="metric status"><i class="dot"></i> Chargement</div></div><div class="card"><div class="label">Clients connectés</div><div id="clientCount" class="metric">—</div></div><div class="card"><div class="label">SSID</div><div id="ssid" class="metric" style="font-size:21px">—</div></div><div class="card"><div class="label">Uptime du SSID</div><div id="uptime" class="metric">—</div></div></div>
|
||
<div class="layout"><section class="card"><div class="topline"><h2>Clients connectés</h2><span id="updated" class="pill">—</span></div><div class="table-wrap"><table><thead><tr><th>APPAREIL</th><th>IP</th><th>MAC</th><th>SIGNAL</th><th>ÉTAT</th><th>ACTION</th></tr></thead><tbody id="clients"></tbody></table></div></section>
|
||
<aside class="card"><h2>Rejoindre le réseau</h2><div id="qrcode" style="background:#fff;padding:12px;width:max-content;border-radius:10px;margin:0 auto 14px"></div><div class="label">Mot de passe récupéré par l’API</div><input id="qrPassword" type="password" readonly><button id="showPassword" class="secondary" type="button" style="display:block;width:100%">Afficher le mot de passe</button><p class="muted" style="font-size:12px">Le QR code utilise le SSID et le mot de passe réels de l’AP.</p></aside></div></section>
|
||
<section id="settingsView" class="view"><div id="setupCard" style="display:none"></div><div style="display:grid;grid-template-columns:1fr 1fr;gap:18px;margin-top:18px;align-items:start"><div class="card"><h2>Réglages AP</h2><p class="muted">Les changements redémarrent la connexion Wi‑Fi active.</p><label class="label" for="settingsSsid">Nom du SSID</label><input id="settingsSsid" type="text" maxlength="32"><label class="label" for="settingsPassword">Mot de passe Wi‑Fi</label><div style="display:flex;gap:8px;margin:7px 0 16px"><input id="settingsPassword" type="password" minlength="8" maxlength="63" style="margin:0"><button id="toggleSettingsPassword" class="secondary" type="button" aria-label="Afficher le mot de passe" title="Afficher le mot de passe"><svg id="passwordEye" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.5-8 10-8 10 8 10 8-3.5 8-10 8S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/></svg></button></div><label class="label" for="settingsSecurity">Sécurité Wi‑Fi</label><select id="settingsSecurity" style="display:block;width:100%;margin:7px 0 16px;padding:12px;border-radius:9px;border:1px solid var(--line);background:var(--panel2);color:var(--text);font-size:15px"><option value="wpa1">WPA1</option><option value="wpa2" selected>WPA2</option><option value="wpa3">WPA3</option></select><label class="check"><input id="settingsAutoconnect" type="checkbox"> Démarrer automatiquement au démarrage</label><button id="saveSettings" type="button">Enregistrer les réglages</button><span id="message" class="muted" style="margin-left:10px"></span><div style="margin-top:22px;padding-top:18px;border-top:1px solid var(--line)"><h2>Personnalisation</h2><label class="label" for="appNameInput">Nom complet de l’application</label><input id="appNameInput" type="text" maxlength="64" placeholder="Administration de l’AP Wi‑Fi"><label class="label" for="appShortNameInput">Nom court (onglet navigateur)</label><input id="appShortNameInput" type="text" maxlength="32" placeholder="AP Manageur"><label class="label" for="themeSelect">Thème</label><select id="themeSelect" style="display:block;width:100%;margin:7px 0 16px;padding:12px;border-radius:9px;border:1px solid var(--line);background:var(--panel2);color:var(--text);font-size:15px"><option value="dark">Sombre</option><option value="light">Clair</option><option value="auto">Automatique (selon l’appareil)</option></select><button id="saveCustomization" type="button">Enregistrer la personnalisation</button><p class="muted" style="font-size:12px;margin-top:8px">Enregistré dans la base de données : le nom et le thème se retrouvent sur tous vos appareils.</p><span id="themeMessage" class="muted"></span></div></div><div class="card"><h2>Réglages</h2><label class="label" for="authSettingsUsername">Utilisateur administrateur</label><input id="authSettingsUsername" type="text" maxlength="32"><label class="label" for="authSettingsPassword">Nouveau mot de passe</label><div style="display:flex;gap:8px;margin:7px 0 16px"><input id="authSettingsPassword" type="password" minlength="8" maxlength="128" placeholder="Nouveau mot de passe" style="margin:0"><button id="toggleAuthSettingsPassword" class="secondary" type="button" aria-label="Afficher le nouveau mot de passe" title="Afficher le nouveau mot de passe"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.5-8 10-8 10 8 10 8-3.5 8-10 8S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/></svg></button></div><label class="label" for="authSettingsPasswordConfirm">Confirmation du mot de passe</label><div style="display:flex;gap:8px;margin:7px 0 16px"><input id="authSettingsPasswordConfirm" type="password" minlength="8" maxlength="128" placeholder="Confirmation du mot de passe" style="margin:0"><button id="toggleAuthSettingsPasswordConfirm" class="secondary" type="button" aria-label="Afficher la confirmation du mot de passe" title="Afficher la confirmation du mot de passe"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.5-8 10-8 10 8 10 8-3.5 8-10 8S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/></svg></button></div><label class="label" for="authCurrentPassword">Mot de passe actuel</label><div style="display:flex;gap:8px;margin:7px 0 16px"><input id="authCurrentPassword" type="password" autocomplete="current-password" maxlength="128" style="margin:0"><button id="toggleCurrentPassword" class="secondary" type="button" aria-label="Afficher le mot de passe actuel" title="Afficher le mot de passe actuel"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.5-8 10-8 10 8 10 8-3.5 8-10 8S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/></svg></button></div><button id="saveAuth" type="button">Modifier les identifiants</button><span id="authSettingsMessage" class="muted" style="margin-left:10px"></span><div id="twoFaBox" style="margin-top:22px;padding-top:18px;border-top:1px solid var(--line)"><h2>Double identification (2FA) & Codes de secours</h2><p id="twoFaState" class="muted">Chargement…</p><div id="twoFaSetup" style="display:none"><p class="muted">Scannez ce QR code avec votre application d’authentification, ou saisissez la clé manuellement :</p><div id="twoFaQr" style="background:#fff;padding:12px;width:max-content;border-radius:10px;margin:0 auto 12px"></div><p class="muted">Clé : <strong id="twoFaSecret" style="color:var(--text);word-break:break-all"></strong></p></div><div id="twoFaRecoveryDisplay" class="recovery-card" style="display:none"><div style="font-weight:700;color:var(--blue);margin-bottom:6px">⚠️ Codes de récupération (secours)</div><p class="muted" style="font-size:13px;margin:0 0 10px">Conservez ces 8 codes en lieu sûr. En cas de perte de votre application 2FA, chaque code vous permettra de vous connecter une fois.</p><div id="twoFaRecoveryGrid" class="recovery-grid"></div><div class="recovery-actions"><button id="copyRecoveryCodes" class="secondary" type="button">📋 Copier les codes</button><button id="downloadRecoveryCodes" class="secondary" type="button">💾 Télécharger (.txt)</button></div></div><div id="twoFaInputGroup" style="margin-top:12px"><input id="twoFaCode" type="text" autocomplete="off" placeholder="Code TOTP à 6 chiffres ou code de secours" style="display:none"><p id="twoFaHelpText" class="muted" style="font-size:12px;display:none">Le mot de passe actuel saisi ci-dessus et un code (TOTP ou secours) sont requis.</p></div><div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:12px"><button id="twoFaEnable" type="button">Activer la 2FA</button><button id="twoFaConfirm" type="button" style="display:none">Valider le code 2FA</button><button id="twoFaDownloadMenuBtn" class="secondary" type="button" style="display:none">💾 Télécharger les codes de récupération</button><button id="twoFaRegenRecovery" class="secondary" type="button" style="display:none">Régénérer les codes</button><button id="twoFaDisable" class="secondary" type="button" style="display:none">Désactiver la 2FA</button></div><span id="twoFaMessage" class="muted" style="margin-top:10px;display:block"></span></div><div class="notice"><strong>Zone dangereuse</strong><p>Supprime l’historique des logs, les appareils bloqués et la configuration réseau enregistrée.</p><button id="clearAllData" type="button" style="background:var(--red);color:white">Supprimer toutes les données</button><span id="clearDataMessage" class="muted" style="margin-left:10px"></span></div></div></div></section>
|
||
<section id="logsView" class="view"><div class="card"><div class="topline"><h2>Journaux de connexion</h2><div style="display:flex;gap:8px"><button id="refreshLogs" class="secondary" type="button">Actualiser</button><button id="clearLogs" type="button" style="background:var(--red);color:white">Supprimer</button></div></div><div class="table-wrap"><table><thead><tr><th>DATE</th><th>ÉVÉNEMENT</th><th>APPAREIL</th><th>IP</th><th>MAC</th></tr></thead><tbody id="logs"></tbody></table></div></div></section>
|
||
<section id="blockedView" class="view"><div class="card"><div class="topline"><h2>Appareils bloqués</h2><button id="refreshBlocked" class="secondary" type="button">Actualiser</button></div><p class="muted">Ces appareils sont enregistrés dans la liste de blocage et peuvent être autorisés à nouveau.</p><div class="table-wrap"><table><thead><tr><th>APPAREIL</th><th>ADRESSE MAC</th><th>BLOQUÉ LE</th><th>ACTION</th></tr></thead><tbody id="blockedClients"></tbody></table></div></div></section>
|
||
</main>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script><script>
|
||
const API_URL=location.protocol==='file:'?'http://127.0.0.1:8080':('http://'+location.hostname+':8080');
|
||
/* Nom du projet configurable */
|
||
let PROJECT_NAME="Administration de l’AP Wi‑Fi";
|
||
let PROJECT_SHORT_NAME="AP Manageur";
|
||
function applyProjectTitle(name=PROJECT_NAME,shortName=PROJECT_SHORT_NAME){
|
||
PROJECT_NAME=name;PROJECT_SHORT_NAME=shortName;document.title=shortName;
|
||
document.querySelectorAll('.app-title').forEach(el=>el.textContent=name);
|
||
document.querySelectorAll('.app-short-title').forEach(el=>el.textContent=shortName);
|
||
}
|
||
function readCookie(name){const item=document.cookie.split('; ').find(row=>row.startsWith(name+'='));return item?decodeURIComponent(item.split('=').slice(1).join('=')):''}
|
||
function saveAuthCookie(token){document.cookie='apAuthToken='+encodeURIComponent(token)+'; Max-Age=3600; Path=/; SameSite=Lax'}
|
||
function clearAuthCookie(){document.cookie='apAuthToken=; Max-Age=0; Path=/; SameSite=Lax'}
|
||
let authToken=readCookie('apAuthToken')||sessionStorage.getItem('apAuthToken')||'';let authConfigured=false;let authSetupMode=false;let authTwoFactor=false;let clients=[];let status={};
|
||
async function apiFetch(path,options={}){options.headers={...(options.headers||{}),...(authToken?{'X-Session-Token':authToken}:{})};const response=await fetch(API_URL+path,options);if(response.status===401){authToken='';sessionStorage.removeItem('apAuthToken');clearAuthCookie();showAuth(false);throw Error('Session expirée');}return response}
|
||
function showAuth(setup){authSetupMode=setup;$('authView').classList.add('active');$('app').style.display='none';$('authTitle').textContent=setup?'Créer le compte administrateur':'Connexion';$('authIntro').innerHTML=setup?'Définissez le compte qui protégera le tableau de bord.':'Connectez-vous pour accéder à <span class="app-title">'+PROJECT_NAME+'</span>.';$('authSubmit').textContent=setup?'Créer le compte':'Se connecter';$('authPassword').value='';$('authOtp').value='';$('authOtpBox').style.display=(!setup&&authTwoFactor)?'block':'none';$('rememberUserBox').style.display=setup?'none':'block';$('authMessage').textContent='';if(!setup){$('authUsername').value=localStorage.getItem('apRememberedUser')||'';$('rememberUser').checked=!!localStorage.getItem('apRememberedUser')}else{$('authUsername').value='';$('rememberUser').checked=false;}}
|
||
function showApp(){loadPreferences();$('authView').classList.remove('active');$('app').style.display='block';$('authSettingsUsername').value=localStorage.getItem('apRememberedUser')||$('authUsername').value||''}
|
||
async function logout(){try{await apiFetch('/api/logout',{method:'POST'})}catch(_){ }authToken='';sessionStorage.removeItem('apAuthToken');clearAuthCookie();showAuth(false)}let previousClientSignature=null;let qrSignature=null;let currentTab='dashboard';let actionBusy=false;
|
||
const $=id=>document.getElementById(id);
|
||
let themePref=localStorage.getItem('apTheme')||'dark';const themeMedia=window.matchMedia('(prefers-color-scheme: light)');
|
||
function applyTheme(){document.documentElement.dataset.theme=themePref==='auto'?(themeMedia.matches?'light':'dark'):themePref;const s=document.getElementById('themeSelect');if(s)s.value=themePref}
|
||
themeMedia.addEventListener('change',()=>{if(themePref==='auto')applyTheme()});applyTheme();
|
||
async function loadPreferences(){try{const r=await apiFetch('/api/preferences',{cache:'no-store'});const d=await r.json();if(r.ok){if(['dark','light','auto'].includes(d.theme)){themePref=d.theme;localStorage.setItem('apTheme',themePref);applyTheme()}if(d.app_name)$('appNameInput').value=d.app_name;if(d.app_short_name)$('appShortNameInput').value=d.app_short_name;applyProjectTitle(d.app_name||PROJECT_NAME,d.app_short_name||PROJECT_SHORT_NAME)}}catch(_){}}
|
||
function setActionBusy(busy){actionBusy=busy;document.querySelectorAll('button').forEach(button=>{button.disabled=busy;button.style.opacity=busy?'0.55':'';button.style.cursor=busy?'wait':''})}
|
||
async function runAction(action){if(actionBusy)return;setActionBusy(true);try{return await action()}finally{setActionBusy(false)}}
|
||
function uptime(s){if(!s)return '—';let d=Math.floor(s/86400),h=Math.floor(s%86400/3600),m=Math.floor(s%3600/60),sec=s%60;return(d?d+'j ':'')+(h?String(h).padStart(2,'0')+'h ':'')+String(m).padStart(2,'0')+'m '+String(sec).padStart(2,'0')+'s'}
|
||
function esc(s){return String(s).replace(/([\\;,:])/g,'\\$1')}
|
||
function signalQuality(dbm){if(dbm===null||dbm===undefined)return '<span class="signal">—</span>';if(dbm>=-50)return '<span class="signal signal-excellent">Excellent</span>';if(dbm>=-60)return '<span class="signal signal-bon">Bon</span>';if(dbm>=-70)return '<span class="signal signal-moyen">Moyen</span>';return '<span class="signal signal-faible">Faible</span>'}
|
||
function qr(){if(!status.ssid||!status.password)return;let box=$('qrcode');let signature=status.ssid+'\u0000'+status.password;if(signature===qrSignature)return;$('qrPassword').value=status.password;box.innerHTML='';new QRCode(box,{text:`WIFI:T:WPA;S:${esc(status.ssid)};P:${esc(status.password)};;`,width:190,height:190});qrSignature=signature}
|
||
async function controlAp(action){return runAction(async()=>{try{const response=await apiFetch('/api/ap/control',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action})});const data=await response.json();if(!response.ok)throw Error(data.error||'Action refusée');await new Promise(resolve=>setTimeout(resolve,1000));await load()}catch(e){alert('Erreur : '+e.message)}})}
|
||
function updateApButtons(){const active=!!status.active,configured=!!status.configured;$('setupView').classList.toggle('active',!configured);$('dashboardView').classList.toggle('active',configured&¤tTab==='dashboard');$('settingsView').classList.toggle('active',configured&¤tTab==='settings');$('logsView').classList.toggle('active',configured&¤tTab==='logs');$('blockedView').classList.toggle('active',configured&¤tTab==='blocked');$('nav').style.display=configured?'flex':'none';$('dashboardTab').style.display=configured?'':'none';$('settingsTab').style.display=configured?'':'none';$('logsTab').style.display=configured?'':'none';$('blockedTab').style.display=configured?'':'none';$('apControl').style.display=configured?'':'none';$('apRestart').style.display=configured&&active?'':'none';$('refresh').style.display=configured?'':'none';if(configured){$('apControl').textContent=active?'Arrêter':'Démarrer';$('apControl').style.background=active?'var(--red)':'var(--blue)';$('apControl').style.color=active?'white':'#071326';}}
|
||
async function toggleBlock(mac,blocked){return runAction(async()=>{try{const response=await apiFetch('/api/clients/block',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({mac,blocked})});const data=await response.json();if(!response.ok)throw Error(data.error||'Opération refusée');await load()}catch(e){alert('Erreur : '+e.message)}})}
|
||
function render(){ updateApButtons();$('clientCount').textContent=clients.length;$('ssid').textContent=status.ssid||'SSID inconnu';$('uptime').textContent=status.active?uptime(status.uptime_seconds):'—';$('settingsSsid').value=status.ssid||'';$('settingsPassword').value=status.password||'';$('settingsSecurity').value=status.security||'wpa2';$('settingsAutoconnect').checked=!!status.autoconnect;$('updated').textContent='Mis à jour '+new Date().toLocaleTimeString('fr-FR',{hour:'2-digit',minute:'2-digit'});$('state').innerHTML=status.active?'<i class="dot"></i> Actif':'<i class="dot" style="background:var(--red)"></i> Inactif';$('clients').innerHTML=clients.length?clients.map(c=>`<tr><td><div class="client"><div class="avatar">${(c.name||'?')[0].toUpperCase()}</div><strong>${c.name||'Nom non communiqué'}</strong></div></td><td>${c.ip||'Non attribuée'}</td><td>${c.mac||'—'}</td><td>${signalQuality(c.signal_dbm)}</td><td class="badge">${c.blocked?'● Bloqué':'● Connecté'}</td><td><button class="secondary" type="button" onclick="toggleBlock('${c.mac}',${!c.blocked})">${c.blocked?'Débloquer':'Bloquer'}</button></td></tr>`).join(''):'<tr><td colspan="6" class="empty">Aucun client connecté</td></tr>';qr()}
|
||
async function load(){try{let [a,b]=await Promise.all([apiFetch('/api/status',{cache:'no-store'}),apiFetch('/api/clients',{cache:'no-store'})]);if(!a.ok||!b.ok){const response=!a.ok?a:b;let detail='HTTP '+response.status;try{const body=await response.json();detail=body.error||detail}catch(_){ }throw Error(detail)}const nextStatus=await a.json();const nextClients=await b.json();const signature=nextClients.map(client=>client.mac).sort().join('|');const changed=previousClientSignature!==null&&signature!==previousClientSignature;status=nextStatus;clients=nextClients;previousClientSignature=signature;render();if(changed){loadLogs()}}catch(e){console.error('Chargement API :',e);if($('state'))$('state').textContent='API indisponible : '+e.message}}
|
||
async function loadLogs(){try{const response=await apiFetch('/api/logs',{cache:'no-store'});const data=await response.json();$('logs').innerHTML=data.length?data.map(log=>{const labels={connexion:'● Connexion Wi-Fi',déconnexion:'○ Déconnexion Wi-Fi',connexion_admin:'● Connexion Admin',connexion_code_recuperation:'🔑 Connexion Code Secours',echec_connexion:'❌ Échec Connexion',deconnexion_admin:'○ Déconnexion Admin',creation_compte_admin:'👤 Création Compte',modification_identifiants:'🔑 Modif. Identifiants',generation_secret_2fa:'⚙️ Secret 2FA généré',activation_2fa:'🛡️ Activation 2FA',desactivation_2fa:'🔓 Désactivation 2FA',regeneration_codes_secours:'🔄 Nouveaux Codes Secours',modification_preferences:'🎨 Modif. Préférences',suppression_journaux:'🗑️ Suppr. Journaux',reinitialisation_donnees:'⚠️ Réinitialisation Data',création:'+ Création AP',enregistrement_modifie:'✓ Config AP modifiée',blocage:'⛔ Blocage',déblocage:'✓ Déblocage',démarrage:'▶ Démarrage AP',arrêt:'■ Arrêt AP',redémarrage:'↻ Redémarrage AP'};const label=labels[log.event]||log.event;const badges={connexion_admin:'badge',connexion_code_recuperation:'badge',activation_2fa:'badge',creation_compte_admin:'badge',création:'badge',démarrage:'badge',connexion:'badge',déblocage:'signal-bon',regeneration_codes_secours:'signal-bon',modification_identifiants:'signal-bon',modification_preferences:'signal-bon',enregistrement_modifie:'signal-bon',desactivation_2fa:'signal-moyen',generation_secret_2fa:'signal-moyen',suppression_journaux:'signal-moyen',echec_connexion:'signal-faible',blocage:'signal-faible',arrêt:'signal-faible',reinitialisation_donnees:'signal-faible'};const style=badges[log.event]||'';const macText=log.mac==='AUTH'?'Authentification':log.mac==='CONFIG'?'Configuration':log.mac==='SYSTEM'?'Système':(log.mac||'—');return `<tr><td>${log.time}</td><td class="${style}">${label}</td><td>${log.name||'Nom non communiqué'}</td><td>${log.ip||'—'}</td><td>${macText}</td></tr>`}).join(''):'<tr><td colspan="5" class="empty">Aucun journal disponible</td></tr>'}catch(e){$('logs').innerHTML='<tr><td colspan="5" class="empty">API indisponible</td></tr>'}}
|
||
async function loadBlocked(){try{const response=await apiFetch('/api/clients/blocked',{cache:'no-store'});if(!response.ok)throw Error('API indisponible');const data=await response.json();$('blockedClients').innerHTML=data.length?data.map(c=>`<tr><td><div class="client"><div class="avatar">${String(c.name||'?')[0].toUpperCase()}</div><strong>${String(c.name||'Nom non communiqué').replace(/[&<>"']/g,ch=>'&#'+ch.charCodeAt(0)+';')}</strong></div></td><td>${c.mac}</td><td>${c.blocked_at}</td><td><button class="secondary" type="button" onclick="toggleBlock('${c.mac}',false);setTimeout(loadBlocked,300)">Débloquer</button></td></tr>`).join(''):'<tr><td colspan="4" class="empty">Aucun appareil bloqué</td></tr>'}catch(e){$('blockedClients').innerHTML='<tr><td colspan="4" class="empty">API indisponible</td></tr>'}}
|
||
function selectTab(tab){if(!status.configured)return;currentTab=tab;sessionStorage.setItem('apDashboardTab',tab);const dashboard=tab==='dashboard',settings=tab==='settings',logs=tab==='logs',blocked=tab==='blocked';$('dashboardView').classList.toggle('active',dashboard);$('settingsView').classList.toggle('active',settings);$('logsView').classList.toggle('active',logs);$('blockedView').classList.toggle('active',blocked);$('dashboardTab').classList.toggle('active',dashboard);$('settingsTab').classList.toggle('active',settings);$('logsTab').classList.toggle('active',logs);$('blockedTab').classList.toggle('active',blocked);$('dashboardTab').setAttribute('aria-selected',dashboard);$('settingsTab').setAttribute('aria-selected',settings);$('logsTab').setAttribute('aria-selected',logs);$('blockedTab').setAttribute('aria-selected',blocked);if(logs)loadLogs();if(blocked)loadBlocked();if(settings)load2fa()}
|
||
$('dashboardTab').onclick=()=>selectTab('dashboard');$('settingsTab').onclick=()=>selectTab('settings');$('logsTab').onclick=()=>selectTab('logs');$('blockedTab').onclick=()=>selectTab('blocked');$('refreshLogs').onclick=()=>runAction(loadLogs);$('refreshBlocked').onclick=()=>runAction(loadBlocked);
|
||
$('clearLogs').onclick=()=>runAction(async()=>{if(!confirm('Supprimer tous les journaux ?'))return;const response=await apiFetch('/api/logs/clear',{method:'POST'});if(response.ok)await loadLogs()});
|
||
const EYE_OPEN='<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.5-8 10-8 10 8 10 8-3.5 8-10 8S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/></svg>';
|
||
const EYE_OFF='<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3l18 18M10.6 10.6a2 2 0 0 0 2.8 2.8M9.9 4.2A10.7 10.7 0 0 1 12 4c5 0 8.5 4 10 8a16.7 16.7 0 0 1-3.1 4.8M6.6 6.6C4.7 7.8 3.5 9.7 2 12c1.5 4 5 8 10 8 1 0 2-.2 2.9-.5"/></svg>';
|
||
function bindPasswordToggle(btnId,fieldId){const btn=$(btnId),field=$(fieldId);if(!btn||!field)return;btn.onclick=()=>{const isHidden=field.type==='password';field.type=isHidden?'text':'password';btn.innerHTML=isHidden?EYE_OFF:EYE_OPEN;const label=isHidden?'Masquer le mot de passe':'Afficher le mot de passe';btn.setAttribute('aria-label',label);btn.setAttribute('title',label)}}
|
||
bindPasswordToggle('toggleSetupPassword','setupPassword');
|
||
bindPasswordToggle('toggleSetupPasswordConfirm','setupPasswordConfirm');
|
||
bindPasswordToggle('toggleSettingsPassword','settingsPassword');
|
||
bindPasswordToggle('toggleAuthSettingsPassword','authSettingsPassword');
|
||
bindPasswordToggle('toggleAuthSettingsPasswordConfirm','authSettingsPasswordConfirm');
|
||
bindPasswordToggle('toggleAuthPassword','authPassword');
|
||
bindPasswordToggle('toggleCurrentPassword','authCurrentPassword');
|
||
$('apControl').onclick=()=>controlAp(status.active?'stop':'start');$('apRestart').onclick=()=>controlAp('restart');
|
||
$('clearAllData').onclick=()=>runAction(async()=>{if(!confirm('Supprimer définitivement les logs, les blocages et la configuration réseau ?'))return;const message=$('clearDataMessage');message.textContent='Suppression…';try{const response=await apiFetch('/api/data/clear',{method:'POST'});const data=await response.json();if(!response.ok)throw Error(data.error||'Suppression refusée');message.textContent=data.message;await new Promise(resolve=>setTimeout(resolve,500));await load()}catch(e){message.textContent='Erreur : '+e.message}});
|
||
$('setupAp').onclick=()=>runAction(async()=>{const message=$('setupMessage');const password=$('setupPassword').value;const confirmation=$('setupPasswordConfirm').value;if(password!==confirmation){message.textContent='Erreur : les mots de passe ne correspondent pas';return}message.textContent='Configuration…';try{const response=await apiFetch('/api/setup',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ssid:$('setupSsid').value,password:password,security:$('setupSecurity').value})});const data=await response.json();if(!response.ok)throw Error(data.error||'Configuration refusée');message.textContent=data.message;currentTab='dashboard';sessionStorage.setItem('apDashboardTab','dashboard');await new Promise(resolve=>setTimeout(resolve,500));await load()}catch(e){message.textContent='Erreur : '+e.message}});
|
||
$('refresh').onclick=()=>runAction(load);
|
||
$('showPassword').onclick=()=>{const field=$('qrPassword');const button=$('showPassword');const hidden=field.type==='password';field.type=hidden?'text':'password';button.textContent=hidden?'Cacher le mot de passe':'Afficher le mot de passe';button.setAttribute('aria-label',button.textContent)};
|
||
$('saveSettings').onclick=()=>runAction(async()=>{let m=$('message');m.textContent='Enregistrement…';try{let r=await apiFetch('/api/settings',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ssid:$('settingsSsid').value,password:$('settingsPassword').value,security:$('settingsSecurity').value,autoconnect:$('settingsAutoconnect').checked})});let d=await r.json();if(!r.ok)throw Error(d.error);m.textContent=d.message;await new Promise(resolve=>setTimeout(resolve,1000));await load()}catch(e){m.textContent='Erreur : '+e.message}});
|
||
$('logout').onclick=logout;
|
||
$('saveCustomization').onclick=()=>{const appName=$('appNameInput').value.trim();const appShortName=$('appShortNameInput').value.trim();themePref=$('themeSelect').value;localStorage.setItem('apTheme',themePref);applyTheme();const m=$('themeMessage');m.textContent='Enregistrement…';apiFetch('/api/preferences',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({theme:themePref,app_name:appName,app_short_name:appShortName})}).then(async r=>{const d=await r.json();if(r.ok){applyProjectTitle(appName,appShortName);m.textContent='Personnalisation enregistrée sur le serveur'}else{m.textContent='Erreur : '+(d.error||'enregistrement refusé')}}).catch(e=>{m.textContent='Erreur : '+e.message})};
|
||
$('authSubmit').onclick=async()=>{const message=$('authMessage');message.textContent='Traitement…';try{const path=authSetupMode?'/api/auth/setup':'/api/login';const response=await fetch(API_URL+path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:$('authUsername').value,password:$('authPassword').value,otp:$('authOtp').value.trim()})});const data=await response.json();if(!response.ok)throw Error(data.error||'Authentification refusée');if(authSetupMode){message.textContent='Compte créé. Connexion…';authSetupMode=false;return $('authSubmit').click()}authToken=data.token;sessionStorage.setItem('apAuthToken',authToken);currentTab='dashboard';sessionStorage.setItem('apDashboardTab','dashboard');if($('rememberUser').checked){localStorage.setItem('apRememberedUser',$('authUsername').value.trim());saveAuthCookie(authToken)}else{localStorage.removeItem('apRememberedUser');clearAuthCookie()}$('authOtp').value='';showApp();await load();selectTab('dashboard')}catch(e){message.textContent='Erreur : '+e.message}};
|
||
$('saveAuth').onclick=()=>runAction(async()=>{const message=$('authSettingsMessage');const password=$('authSettingsPassword').value;const confirmation=$('authSettingsPasswordConfirm').value;if(password!==confirmation){message.textContent='Erreur : les mots de passe ne correspondent pas';return}try{const response=await apiFetch('/api/auth/password',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:$('authSettingsUsername').value,current_password:$('authCurrentPassword').value,password})});const data=await response.json();if(!response.ok)throw Error(data.error||'Modification refusée');message.textContent=data.message;$('authCurrentPassword').value='';$('authSettingsPassword').value='';$('authSettingsPasswordConfirm').value=''}catch(e){message.textContent='Erreur : '+e.message}});
|
||
/* Double identification (2FA) */
|
||
let twoFaPending=false;
|
||
let currentRecoveryCodes=[];
|
||
function displayRecoveryCodes(codes){
|
||
currentRecoveryCodes=codes||[];
|
||
const grid=$('twoFaRecoveryGrid');
|
||
grid.replaceChildren();
|
||
currentRecoveryCodes.forEach(code=>{
|
||
const div=document.createElement('div');
|
||
div.className='code-chip';
|
||
div.textContent=code;
|
||
grid.appendChild(div);
|
||
});
|
||
$('twoFaRecoveryDisplay').style.display='block';
|
||
}
|
||
$('copyRecoveryCodes').onclick=()=>{
|
||
if(!currentRecoveryCodes.length)return;
|
||
const text="Codes de récupération 2FA AP Wi-Fi:\n"+currentRecoveryCodes.join("\n");
|
||
navigator.clipboard.writeText(text).then(()=>alert("Codes copiés dans le presse-papier !")).catch(()=>{
|
||
const ta=document.createElement('textarea');ta.value=text;document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);alert("Codes copiés dans le presse-papier !");
|
||
});
|
||
};
|
||
function downloadRecoveryFile(codes){
|
||
if(!codes||!codes.length)return;
|
||
const text="Codes de récupération 2FA - AP Wi-Fi\nConservez ces codes en lieu sûr.\n\n"+codes.join("\n")+"\n";
|
||
const blob=new Blob([text],{type:'text/plain;charset=utf-8'});
|
||
const url=URL.createObjectURL(blob);
|
||
const a=document.createElement('a');a.href=url;a.download='codes-de-recuperation-2fa.txt';document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url);
|
||
}
|
||
$('downloadRecoveryCodes').onclick=()=>downloadRecoveryFile(currentRecoveryCodes);
|
||
function render2fa(enabled,recoveryRemaining){
|
||
authTwoFactor=enabled;
|
||
if(enabled){
|
||
$('twoFaState').textContent='Activée : un code TOTP ou un code de secours sera demandé à chaque connexion. (Codes de secours restants : '+(recoveryRemaining!==undefined?recoveryRemaining:'?')+' / 8)';
|
||
$('twoFaCode').style.display='block';
|
||
$('twoFaHelpText').style.display='block';
|
||
$('twoFaDownloadMenuBtn').style.display='';
|
||
$('twoFaRegenRecovery').style.display='';
|
||
$('twoFaDisable').style.display='';
|
||
$('twoFaEnable').style.display='none';
|
||
$('twoFaConfirm').style.display='none';
|
||
$('twoFaSetup').style.display='none';
|
||
}else if(twoFaPending){
|
||
$('twoFaState').textContent='Activation en cours : scannez le QR code et saisissez le code à 6 chiffres de votre application.';
|
||
$('twoFaSetup').style.display='block';
|
||
$('twoFaCode').style.display='block';
|
||
$('twoFaConfirm').style.display='';
|
||
$('twoFaHelpText').style.display='none';
|
||
$('twoFaEnable').style.display='none';
|
||
$('twoFaDisable').style.display='none';
|
||
$('twoFaDownloadMenuBtn').style.display='none';
|
||
$('twoFaRegenRecovery').style.display='none';
|
||
}else{
|
||
$('twoFaState').textContent='Désactivée.';
|
||
$('twoFaSetup').style.display='none';
|
||
$('twoFaCode').style.display='none';
|
||
$('twoFaHelpText').style.display='none';
|
||
$('twoFaEnable').style.display='';
|
||
$('twoFaConfirm').style.display='none';
|
||
$('twoFaDisable').style.display='none';
|
||
$('twoFaDownloadMenuBtn').style.display='none';
|
||
$('twoFaRegenRecovery').style.display='none';
|
||
$('twoFaRecoveryDisplay').style.display='none';
|
||
}
|
||
}
|
||
async function load2fa(){
|
||
try{
|
||
const response=await apiFetch('/api/auth/2fa/status',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'});
|
||
const data=await response.json();
|
||
if(!response.ok)throw Error(data.error||'Statut 2FA indisponible');
|
||
render2fa(!!data.enabled,data.recovery_remaining);
|
||
}catch(e){$('twoFaState').textContent='Erreur : '+e.message;}
|
||
}
|
||
$('twoFaEnable').onclick=()=>runAction(async()=>{
|
||
const message=$('twoFaMessage');message.textContent='';
|
||
try{
|
||
if(!$('authCurrentPassword').value)throw Error('Saisissez votre mot de passe actuel ci-dessus');
|
||
const response=await apiFetch('/api/auth/2fa/enable',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:$('authCurrentPassword').value})});
|
||
const data=await response.json();
|
||
if(!response.ok)throw Error(data.error||'Activation refusée');
|
||
const user=encodeURIComponent($('authSettingsUsername').value||'admin');
|
||
$('twoFaQr').replaceChildren();
|
||
new QRCode($('twoFaQr'),{text:'otpauth://totp/AP%20Wi-Fi:'+user+'?secret='+data.secret+'&issuer=AP%20Wi-Fi',width:160,height:160});
|
||
$('twoFaSecret').textContent=data.secret;
|
||
twoFaPending=true;
|
||
render2fa(false);
|
||
message.textContent=data.message;
|
||
}catch(e){message.textContent='Erreur : '+e.message;}
|
||
});
|
||
$('twoFaConfirm').onclick=()=>runAction(async()=>{
|
||
const message=$('twoFaMessage');
|
||
try{
|
||
const response=await apiFetch('/api/auth/2fa/confirm',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({code:$('twoFaCode').value.trim(),password:$('authCurrentPassword').value})});
|
||
const data=await response.json();
|
||
if(!response.ok)throw Error(data.error||'Code refusé');
|
||
twoFaPending=false;
|
||
$('twoFaCode').value='';
|
||
$('twoFaQr').replaceChildren();
|
||
$('twoFaSecret').textContent='';
|
||
if(data.recovery_codes)displayRecoveryCodes(data.recovery_codes);
|
||
await load2fa();
|
||
message.textContent=data.message||'Double identification activée avec succès !';
|
||
}catch(e){message.textContent='Erreur : '+e.message;}
|
||
});
|
||
$('twoFaDownloadMenuBtn').onclick=()=>runAction(async()=>{
|
||
const message=$('twoFaMessage');
|
||
if(currentRecoveryCodes.length){
|
||
downloadRecoveryFile(currentRecoveryCodes);
|
||
message.textContent='Fichier des codes de récupération téléchargé !';
|
||
return;
|
||
}
|
||
message.textContent='Génération et téléchargement des codes de secours…';
|
||
try{
|
||
if(!$('authCurrentPassword').value)throw Error('Saisissez votre mot de passe actuel ci-dessus');
|
||
const response=await apiFetch('/api/auth/2fa/recovery',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:$('authCurrentPassword').value,code:$('twoFaCode').value.trim()})});
|
||
const data=await response.json();
|
||
if(!response.ok)throw Error(data.error||'Génération refusée');
|
||
if(data.recovery_codes){
|
||
displayRecoveryCodes(data.recovery_codes);
|
||
downloadRecoveryFile(data.recovery_codes);
|
||
}
|
||
$('twoFaCode').value='';
|
||
await load2fa();
|
||
message.textContent=data.message||'Nouveaux codes générés et téléchargés avec succès !';
|
||
}catch(e){message.textContent='Erreur : '+e.message;}
|
||
});
|
||
$('twoFaRegenRecovery').onclick=()=>runAction(async()=>{
|
||
const message=$('twoFaMessage');
|
||
message.textContent='Génération de nouveaux codes…';
|
||
try{
|
||
if(!$('authCurrentPassword').value)throw Error('Saisissez votre mot de passe actuel ci-dessus');
|
||
const response=await apiFetch('/api/auth/2fa/recovery',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:$('authCurrentPassword').value,code:$('twoFaCode').value.trim()})});
|
||
const data=await response.json();
|
||
if(!response.ok)throw Error(data.error||'Régénération refusée');
|
||
if(data.recovery_codes)displayRecoveryCodes(data.recovery_codes);
|
||
$('twoFaCode').value='';
|
||
await load2fa();
|
||
message.textContent=data.message||'Nouveaux codes générés avec succès !';
|
||
}catch(e){message.textContent='Erreur : '+e.message;}
|
||
});
|
||
$('twoFaDisable').onclick=()=>runAction(async()=>{
|
||
const message=$('twoFaMessage');
|
||
try{
|
||
const response=await apiFetch('/api/auth/2fa/disable',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({code:$('twoFaCode').value.trim(),password:$('authCurrentPassword').value})});
|
||
const data=await response.json();
|
||
if(!response.ok)throw Error(data.error||'Désactivation refusée');
|
||
$('twoFaCode').value='';
|
||
twoFaPending=false;
|
||
$('twoFaRecoveryDisplay').style.display='none';
|
||
currentRecoveryCodes=[];
|
||
await load2fa();
|
||
message.textContent=data.message;
|
||
}catch(e){message.textContent='Erreur : '+e.message;}
|
||
});
|
||
(async()=>{applyProjectTitle();let data=null;for(let attempt=1;!data;attempt++){try{const response=await fetch(API_URL+'/api/auth/status',{cache:'no-store'});if(!response.ok)throw Error('HTTP '+response.status);data=await response.json()}catch(e){$('loaderText').textContent='API indisponible, nouvelle tentative ('+attempt+')…';await new Promise(resolve=>setTimeout(resolve,1500))}}authConfigured=data.configured;authTwoFactor=!!data.two_factor;if(data.app_name)applyProjectTitle(data.app_name,data.app_short_name);if(authToken){showApp();await load()}else showAuth(!authConfigured);$('loader').classList.add('hide');setTimeout(()=>$('loader').remove(),400)})();
|
||
setInterval(()=>{if(authToken&¤tTab!=='settings')load()},2000);
|
||
</script></body></html>
|