Files
Athena-Deck/access-ui.js
T

24 lines
8.0 KiB
JavaScript

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 &lt;Token&gt;.</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.'}};
const hub=document.createElement('section');hub.className='card';hub.innerHTML=`<h2>Hugging Face · Modelldownloads</h2><p id="hf-status" role="status">Zugang wird geprüft …</p><p>Für private oder freigabepflichtige Modelle: <a class="link" href="https://huggingface.co/settings/tokens" target="_blank" rel="noopener noreferrer">Hugging-Face-Token erstellen ↗</a> mit Leserechten für das gewünschte Repository. Die Freigabe beantragst du auf dessen Modellkarte mit demselben Konto.</p><form id="hf-form"><label>Hugging-Face-Token<input id="hf-token" type="password" autocomplete="off" spellcheck="false" required maxlength="255" placeholder="hf_…"></label><button>Token speichern / ersetzen</button><button id="hf-remove" type="button" class="secondary">Gespeicherten Token entfernen</button></form><p class="note">Separater Zugang für Hugging Face, nicht der Deck-API-Token. Speicherung auf Athena in einer Datei mit Zugriff nur für den Dienstbenutzer (0600), unverschlüsselt. Der gespeicherte Wert wird nie an den Browser zurückgegeben. Bereits laufende Downloads können nach Entfernen weiterlaufen.</p>`;
document.querySelector('#access-page').insertBefore(hub,document.querySelector('#access-message'));
const hfStatus=hub.querySelector('#hf-status');
const describe=s=>s.configured?'Token gespeichert. Ob die Modellfreigabe und Leserechte passen, prüft Hugging Face beim Download.':'Kein Hugging-Face-Token gespeichert. Öffentliche Downloads bleiben möglich.';
try{const r=await fetch('/api/v1/huggingface');if(!r.ok)throw Error();hfStatus.textContent=describe(await r.json());}catch{hfStatus.textContent='Hugging-Face-Zugangsstatus nicht erreichbar.';}
async function saveHub(token){hub.querySelectorAll('button').forEach(b=>b.disabled=true);try{const r=await fetch('/api/v1/huggingface',{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify({token})}),s=await r.json();if(!r.ok)throw Error(s.error||'Speichern fehlgeschlagen.');hfStatus.textContent=describe(s);}catch(e){hfStatus.textContent=e.message;}finally{hub.querySelector('#hf-token').value='';hub.querySelectorAll('button').forEach(b=>b.disabled=false);}}
hub.querySelector('#hf-form').onsubmit=e=>{e.preventDefault();saveHub(hub.querySelector('#hf-token').value.trim());};
hub.querySelector('#hf-remove').onclick=()=>saveHub(null);
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.'};
}