diff --git a/ACCESS.md b/ACCESS.md index 01fab83..8beff01 100644 --- a/ACCESS.md +++ b/ACCESS.md @@ -13,8 +13,10 @@ Zugangsdaten fest: Kennwort und Token dürfen nicht identisch sein. Der Token kann während der Eingabe angezeigt/kopiert werden. **Vor dem Speichern im Passwortmanager sichern.** Ein vorhandener Token ist anschließend nicht abrufbar; bei Verlust einen neuen setzen. -Auf HTTP-Adressen kann die Browser-Zwischenablage gesperrt sein. Dann „Token anzeigen“ -verwenden und manuell kopieren. Im ersten Schritt wird noch keine Sitzung erzeugt: +„Neuen Token kopieren“ ist nur bei gefülltem Eingabefeld verfügbar und kopiert +ausschließlich diesen neuen Wert, niemals den gespeicherten Prüfwert. Bei gesperrter +Clipboard-API wird die browserinterne Kopieralternative versucht. Lehnt der Browser +auch diese ab, „Token anzeigen“ verwenden und manuell kopieren. Im ersten Schritt wird noch keine Sitzung erzeugt: nach dem Einrichten mit dem gewählten Kennwort anmelden. Der Debian-Installer richtet die Zugangsdaten vor dem Start ein. Speicherung unter diff --git a/access-ui.js b/access-ui.js index da1d926..e0f3803 100644 --- a/access-ui.js +++ b/access-ui.js @@ -1,16 +1,28 @@ +async function copyTokenValue(value){ + if(!value)throw Error('empty'); + try{if(navigator.clipboard?.writeText){await navigator.clipboard.writeText(value);return;}}catch{} + const active=document.activeElement,selection=window.getSelection(),ranges=[]; + if(selection)for(let i=0;ib.toString(16).padStart(2,'0')).join('');} async function accessPage(){ const view=document.querySelector('#view'); if(document.querySelector('#access-page'))return; - view.innerHTML=`
EINSTELLUNGEN / ZUGANG

Dein Zugang. Deine Schlüssel.

Ein Administratorzugang für die Oberfläche und ein separater Token für API-Clients.

Oberflächenkennwort ändern

Alle bestehenden Browser-Sitzungen werden abgemeldet. Der API-Token bleibt gültig.

API-Token ändern

Vor dem Speichern kopieren. Der bisherige Token wird sofort ungültig. Bestehende Browser-Sitzungen bleiben angemeldet. Verwende in API-Clients: Authorization: Bearer <Token>.

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.

`; + view.innerHTML=`
EINSTELLUNGEN / ZUGANG

Dein Zugang. Deine Schlüssel.

Ein Administratorzugang für die Oberfläche und ein separater Token für API-Clients.

Oberflächenkennwort ändern

Alle bestehenden Browser-Sitzungen werden abgemeldet. Der API-Token bleibt gültig.

API-Token ändern

Der gespeicherte Token kann nicht zurückgelesen werden. Kopiert wird nur der neue Token im Eingabefeld. Einen zuvor gespeicherten Token findest du in deinem Passwortmanager.

Vor dem Speichern kopieren. Der bisherige Token wird sofort ungültig. Bestehende Browser-Sitzungen bleiben angemeldet. Verwende in API-Clients: Authorization: Bearer <Token>.

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.

`; 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)}} + 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);syncTokenCopy()}} 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.'}; + const syncTokenCopy=()=>{document.querySelector('#token-copy').disabled=accessBusy||!document.querySelector('#new-token').value;}; + document.querySelector('#new-token').addEventListener('input',syncTokenCopy);syncTokenCopy(); + document.querySelector('#token-generate').onclick=()=>{document.querySelector('#new-token').value=accessToken();syncTokenCopy();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('#token-copy').onclick=async()=>{try{const token=document.querySelector('#new-token').value;if(!token)throw Error();await copyTokenValue(token);document.querySelector('#access-message').textContent='Token kopiert. Bitte sicher speichern.';}catch{document.querySelector('#access-message').textContent='Der Browser hat das Kopieren nicht erlaubt. Bitte „Token anzeigen“ wählen und den neuen Token manuell kopieren.'}}; const hub=document.createElement('section');hub.className='card';hub.innerHTML=`

Hugging Face · Modelldownloads

Zugang wird geprüft …

Für private oder freigabepflichtige Modelle: Hugging-Face-Token erstellen ↗ mit Leserechten für das gewünschte Repository. Die Freigabe beantragst du auf dessen Modellkarte mit demselben Konto.

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.

`; document.querySelector('#access-page').insertBefore(hub,document.querySelector('#access-message')); const hfStatus=hub.querySelector('#hf-status'); diff --git a/login.js b/login.js index e7e4aa7..92764fb 100644 --- a/login.js +++ b/login.js @@ -1,9 +1,21 @@ +async function copyTokenValue(value){ + if(!value)throw Error('empty'); + try{if(navigator.clipboard?.writeText){await navigator.clipboard.writeText(value);return;}}catch{} + const active=document.activeElement,selection=window.getSelection(),ranges=[]; + if(selection)for(let i=0;ib.toString(16).padStart(2,'0')).join('');} async function load(){try{const r=await fetch('/api/v1/auth/status'),s=await r.json();firstSetup=!s.initialized;document.querySelector('#title').textContent=firstSetup?'Dein Zugang zu Athena.':'Willkommen zurück.';document.querySelector('#intro').textContent=firstSetup?'Lege das Kennwort für diese Oberfläche und einen separaten Token für API-Clients fest.':'Melde dich mit deinem Oberflächenkennwort an.';document.querySelector('#setup-fields').hidden=!firstSetup;document.querySelector('#password').autocomplete=firstSetup?'new-password':'current-password';document.querySelector('#submit').textContent=firstSetup?'Zugang einrichten':'Anmelden';form.hidden=false;}catch(e){message.textContent='Zugangsstatus nicht erreichbar.'}} -document.querySelector('#generate').onclick=()=>{document.querySelector('#api-token').value=freshToken();message.textContent='Token erzeugt. Vor dem Einrichten kopieren und sicher speichern.';}; +const syncSetupCopy=()=>document.querySelector('#copy').disabled=!document.querySelector('#api-token').value; +document.querySelector('#api-token').addEventListener('input',syncSetupCopy);syncSetupCopy(); +document.querySelector('#generate').onclick=()=>{document.querySelector('#api-token').value=freshToken();syncSetupCopy();message.textContent='Token erzeugt. Vor dem Einrichten kopieren und sicher speichern.';}; document.querySelector('#show-token').onclick=()=>{const input=document.querySelector('#api-token');input.type=input.type==='password'?'text':'password';document.querySelector('#show-token').textContent=input.type==='password'?'Token anzeigen':'Token verbergen';}; -document.querySelector('#copy').onclick=async()=>{try{const token=document.querySelector('#api-token').value;if(!token)throw Error();await navigator.clipboard.writeText(token);message.textContent='Token kopiert. Bitte sicher speichern.';}catch{message.textContent='Token konnte nicht kopiert werden. Bitte einen eigenen Token aus deinem Passwortmanager einfügen.'}}; -form.addEventListener('submit',async event=>{event.preventDefault();const input=document.querySelector('#password'),button=document.querySelector('#submit');button.disabled=true;try{let data={password:input.value},path='login';if(firstSetup){if(input.value!==document.querySelector('#repeat').value)throw Error('Die Kennwörter stimmen nicht überein.');data.api_token=document.querySelector('#api-token').value;path='auth/setup';}const r=await fetch('/api/v1/'+path,{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)});const value=await r.json();if(!r.ok)throw Error(value.error);form.reset();if(firstSetup){await load();message.textContent='Zugang eingerichtet. Bitte jetzt anmelden.';}else location.reload();}catch(e){message.textContent=e.message}finally{button.disabled=false}}); +document.querySelector('#copy').onclick=async()=>{try{const token=document.querySelector('#api-token').value;if(!token)throw Error();await copyTokenValue(token);message.textContent='Token kopiert. Bitte sicher speichern.';}catch{message.textContent='Der Browser hat das Kopieren nicht erlaubt. Bitte „Token anzeigen“ wählen und manuell kopieren.'}}; +form.addEventListener('submit',async event=>{event.preventDefault();const input=document.querySelector('#password'),button=document.querySelector('#submit');button.disabled=true;try{let data={password:input.value},path='login';if(firstSetup){if(input.value!==document.querySelector('#repeat').value)throw Error('Die Kennwörter stimmen nicht überein.');data.api_token=document.querySelector('#api-token').value;path='auth/setup';}const r=await fetch('/api/v1/'+path,{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)});const value=await r.json();if(!r.ok)throw Error(value.error);form.reset();syncSetupCopy();if(firstSetup){await load();message.textContent='Zugang eingerichtet. Bitte jetzt anmelden.';}else location.reload();}catch(e){message.textContent=e.message}finally{button.disabled=false}}); load();