Clarify token copying and add clipboard fallback
This commit is contained in:
@@ -13,8 +13,10 @@ Zugangsdaten fest:
|
|||||||
Kennwort und Token dürfen nicht identisch sein. Der Token kann während der Eingabe
|
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
|
angezeigt/kopiert werden. **Vor dem Speichern im Passwortmanager sichern.** Ein
|
||||||
vorhandener Token ist anschließend nicht abrufbar; bei Verlust einen neuen setzen.
|
vorhandener Token ist anschließend nicht abrufbar; bei Verlust einen neuen setzen.
|
||||||
Auf HTTP-Adressen kann die Browser-Zwischenablage gesperrt sein. Dann „Token anzeigen“
|
„Neuen Token kopieren“ ist nur bei gefülltem Eingabefeld verfügbar und kopiert
|
||||||
verwenden und manuell kopieren. Im ersten Schritt wird noch keine Sitzung erzeugt:
|
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.
|
nach dem Einrichten mit dem gewählten Kennwort anmelden.
|
||||||
|
|
||||||
Der Debian-Installer richtet die Zugangsdaten vor dem Start ein. Speicherung unter
|
Der Debian-Installer richtet die Zugangsdaten vor dem Start ein. Speicherung unter
|
||||||
|
|||||||
+16
-4
@@ -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;i<selection.rangeCount;i++)ranges.push(selection.getRangeAt(i).cloneRange());
|
||||||
|
const area=document.createElement('textarea');area.value=value;area.readOnly=true;
|
||||||
|
area.style.cssText='position:fixed;left:-9999px;top:0;opacity:0';document.body.append(area);
|
||||||
|
try{area.focus();area.select();if(!document.execCommand('copy'))throw Error('clipboard');}
|
||||||
|
finally{area.value='';area.remove();active?.focus();if(selection){selection.removeAllRanges();for(const range of ranges)selection.addRange(range);}}
|
||||||
|
}
|
||||||
let accessBusy=false;
|
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('');}
|
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(){
|
async function accessPage(){
|
||||||
const view=document.querySelector('#view');
|
const view=document.querySelector('#view');
|
||||||
if(document.querySelector('#access-page'))return;
|
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>`;
|
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><p>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.</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" disabled>Neuen 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.'}
|
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('#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-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-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=`<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>`;
|
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'));
|
document.querySelector('#access-page').insertBefore(hub,document.querySelector('#access-message'));
|
||||||
const hfStatus=hub.querySelector('#hf-status');
|
const hfStatus=hub.querySelector('#hf-status');
|
||||||
|
|||||||
@@ -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;i<selection.rangeCount;i++)ranges.push(selection.getRangeAt(i).cloneRange());
|
||||||
|
const area=document.createElement('textarea');area.value=value;area.readOnly=true;
|
||||||
|
area.style.cssText='position:fixed;left:-9999px;top:0;opacity:0';document.body.append(area);
|
||||||
|
try{area.focus();area.select();if(!document.execCommand('copy'))throw Error('clipboard');}
|
||||||
|
finally{area.value='';area.remove();active?.focus();if(selection){selection.removeAllRanges();for(const range of ranges)selection.addRange(range);}}
|
||||||
|
}
|
||||||
let firstSetup=false;
|
let firstSetup=false;
|
||||||
const form=document.querySelector('#login'),message=document.querySelector('#error');
|
const form=document.querySelector('#login'),message=document.querySelector('#error');
|
||||||
function freshToken(){const bytes=crypto.getRandomValues(new Uint8Array(32));return 'ad_'+Array.from(bytes,b=>b.toString(16).padStart(2,'0')).join('');}
|
function freshToken(){const bytes=crypto.getRandomValues(new Uint8Array(32));return 'ad_'+Array.from(bytes,b=>b.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.'}}
|
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('#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.'}};
|
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();if(firstSetup){await load();message.textContent='Zugang eingerichtet. Bitte jetzt anmelden.';}else location.reload();}catch(e){message.textContent=e.message}finally{button.disabled=false}});
|
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();
|
load();
|
||||||
|
|||||||
Reference in New Issue
Block a user