Add first-run admin setup and independent password and API token rotation
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
let accessBusy=false;
|
||||
function accessToken(){const bytes=crypto.getRandomValues(new Uint8Array(32));return 'ad_'+Array.from(bytes,b=>b.toString(16).padStart(2,'0')).join('');}
|
||||
async function accessPage(){
|
||||
const view=document.querySelector('#view');
|
||||
if(document.querySelector('#access-page'))return;
|
||||
view.innerHTML=`<div id="access-page"><div class="kicker">EINSTELLUNGEN / ZUGANG</div><h1>Dein Zugang. Deine Schlüssel.</h1><p>Ein Administratorzugang für die Oberfläche und ein separater Token für API-Clients.</p><p id="access-info"></p><section class="card"><h2>Oberflächenkennwort ändern</h2><form id="password-form"><label for="current-password">Aktuelles Kennwort</label><input id="current-password" type="password" autocomplete="current-password" required maxlength="256"><label for="new-password">Neues Kennwort · mindestens 16 Zeichen</label><input id="new-password" type="password" autocomplete="new-password" required minlength="16" maxlength="256"><label for="repeat-password">Neues Kennwort wiederholen</label><input id="repeat-password" type="password" autocomplete="new-password" required minlength="16" maxlength="256"><button>Kennwort ändern</button></form><p class="note">Alle bestehenden Browser-Sitzungen werden abgemeldet. Der API-Token bleibt gültig.</p></section><section class="card"><h2>API-Token ändern</h2><p id="token-info"></p><form id="token-form"><label for="token-password">Aktuelles Oberflächenkennwort zur Bestätigung</label><input id="token-password" type="password" autocomplete="current-password" required maxlength="256"><label for="new-token">Neuer API-Token</label><input id="new-token" type="password" autocomplete="off" spellcheck="false" required minlength="32" maxlength="256"><button id="token-generate" type="button" class="secondary">Sicheren Token erzeugen</button><button id="token-show" type="button" class="secondary">Token anzeigen</button><button id="token-copy" type="button" class="secondary">Token kopieren</button><button>Neuen Token speichern</button></form><p class="note">Vor dem Speichern kopieren. Der bisherige Token wird sofort ungültig. Bestehende Browser-Sitzungen bleiben angemeldet. Verwende in API-Clients: Authorization: Bearer <Token>.</p></section><p id="access-message" role="status" aria-live="polite"></p><button id="logout" class="secondary">Abmelden</button><p class="note">Der Token berechtigt zum Dienststatus, Hardwareabruf und Demo-Start/Stopp. Er erlaubt keine Kennwort-, Token- oder Netzwerkeinstellungen. Weitere Benutzer und Rollen sind noch nicht enthalten.</p></div>`;
|
||||
try{const r=await fetch('/api/v1/auth/status'),s=await r.json();if(!s.authenticated){location.reload();return}document.querySelector('#access-info').textContent=`Administrator · Diese Instanz: ${s.location}`;document.querySelector('#token-info').textContent=s.api_token_configured?('API-Token eingerichtet'+(s.token_changed_at?' · geändert am '+new Date(s.token_changed_at*1000).toLocaleString('de-DE'):'')+'. Der Wert wird nicht angezeigt.'):'Noch kein API-Token eingerichtet. API-Clients haben keinen Zugang.';}catch{document.querySelector('#access-message').textContent='Zugangsstatus nicht erreichbar.'}
|
||||
async function save(kind,data,form){if(accessBusy)return;accessBusy=true;view.querySelectorAll('button').forEach(b=>b.disabled=true);try{const r=await fetch('/api/v1/auth/'+kind,{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)}),s=await r.json();if(r.status===401){location.reload();return}if(!r.ok)throw Error(s.error);form.reset();if(s.login_required){location.reload();return}document.querySelector('#access-message').textContent='Neuer API-Token gespeichert. Der alte Token ist jetzt ungültig.';document.querySelector('#token-info').textContent='API-Token gerade geändert. Der Wert wird nicht angezeigt.';}catch(e){document.querySelector('#access-message').textContent=e.message}finally{accessBusy=false;view.querySelectorAll('button').forEach(b=>b.disabled=false)}}
|
||||
document.querySelector('#password-form').onsubmit=e=>{e.preventDefault();const value=document.querySelector('#new-password').value;if(value!==document.querySelector('#repeat-password').value){document.querySelector('#access-message').textContent='Die neuen Kennwörter stimmen nicht überein.';return}save('password',{current_password:document.querySelector('#current-password').value,new_password:value},e.target)};
|
||||
document.querySelector('#token-form').onsubmit=e=>{e.preventDefault();save('token',{current_password:document.querySelector('#token-password').value,new_token:document.querySelector('#new-token').value},e.target)};
|
||||
document.querySelector('#token-generate').onclick=()=>{document.querySelector('#new-token').value=accessToken();document.querySelector('#access-message').textContent='Neuen Token vor dem Speichern kopieren.'};
|
||||
document.querySelector('#token-show').onclick=()=>{const input=document.querySelector('#new-token');input.type=input.type==='password'?'text':'password';document.querySelector('#token-show').textContent=input.type==='password'?'Token anzeigen':'Token verbergen';};
|
||||
document.querySelector('#token-copy').onclick=async()=>{try{const token=document.querySelector('#new-token').value;if(!token)throw Error();await navigator.clipboard.writeText(token);document.querySelector('#access-message').textContent='Token kopiert. Bitte sicher speichern.';}catch{document.querySelector('#access-message').textContent='Kopieren nicht möglich. Bitte einen eigenen Token aus dem Passwortmanager verwenden.'}};
|
||||
document.querySelector('#logout').onclick=async()=>{const r=await fetch('/api/v1/logout',{method:'POST',headers:{'X-Athena-Deck':'1'}});if(r.ok||r.status===401)location.reload();else document.querySelector('#access-message').textContent='Abmelden fehlgeschlagen. Bitte erneut versuchen.'};
|
||||
}
|
||||
Reference in New Issue
Block a user