diff --git a/api/config.py b/api/config.py index 1f84f09..0550c53 100644 --- a/api/config.py +++ b/api/config.py @@ -624,6 +624,9 @@ def save_settings(settings: dict) -> dict: if raw_pw and isinstance(raw_pw, str) and raw_pw.strip(): salt = str(STATE_DIR).encode() current['password_hash'] = _hl.sha256(salt + raw_pw.strip().encode()).hexdigest() + # Handle _clear_password: explicitly disable auth + if settings.pop('_clear_password', False): + current['password_hash'] = None for k, v in settings.items(): if k in _SETTINGS_ALLOWED_KEYS: # Validate enum-constrained keys diff --git a/api/routes.py b/api/routes.py index 277ef30..51a0c01 100644 --- a/api/routes.py +++ b/api/routes.py @@ -374,7 +374,9 @@ def handle_post(handler, parsed): # ── Settings (POST) ── if parsed.path == '/api/settings': - return j(handler, save_settings(body)) + saved = save_settings(body) + saved.pop('password_hash', None) # never expose hash to client + return j(handler, saved) # ── Session pin (POST) ── if parsed.path == '/api/session/pin': diff --git a/static/index.html b/static/index.html index c82349f..91b2c6b 100644 --- a/static/index.html +++ b/static/index.html @@ -284,10 +284,11 @@
-
Set a password to require login. Leave blank to disable auth.
- +
Enter a new password to set or change it. Leave blank to keep current setting.
+
+ diff --git a/static/panels.js b/static/panels.js index 7c2dd60..01c7a95 100644 --- a/static/panels.js +++ b/static/panels.js @@ -652,11 +652,14 @@ async function loadSettingsPanel(){ // Password field: always blank (we don't send hash back) const pwField=$('settingsPassword'); if(pwField) pwField.value=''; - // Show sign-out button if auth is active + // Show auth buttons only when auth is active try{ const authStatus=await api('/api/auth/status'); + const active=authStatus.auth_enabled; const signOutBtn=$('btnSignOut'); - if(signOutBtn) signOutBtn.style.display=authStatus.auth_enabled?'':'none'; + if(signOutBtn) signOutBtn.style.display=active?'':'none'; + const disableBtn=$('btnDisableAuth'); + if(disableBtn) disableBtn.style.display=active?'':'none'; }catch(e){} }catch(e){ showToast('Failed to load settings: '+e.message); @@ -672,22 +675,15 @@ async function saveSettings(){ if(model) body.default_model=model; if(workspace) body.default_workspace=workspace; if(sendKey) body.send_key=sendKey; - // Password: if field has content, hash and save; if blank, clear auth - if(pw!==undefined&&pw!==null){ - if(pw.trim()){ - // Hash client-side using the same algo as server (SHA-256 with state-dir salt) - // We send the raw password to the server's dedicated endpoint instead - try{ - await api('/api/settings',{method:'POST',body:JSON.stringify({...body,_set_password:pw.trim()})}); - window._sendKey=sendKey||'enter'; - showToast('Settings saved (password set — login now required)'); - toggleSettings(); - return; - }catch(e){showToast('Save failed: '+e.message);return;} - }else{ - // Blank = clear password (disable auth) - body.password_hash=null; - } + // Password: only act if the field has content; blank = leave auth unchanged + if(pw && pw.trim()){ + try{ + await api('/api/settings',{method:'POST',body:JSON.stringify({...body,_set_password:pw.trim()})}); + window._sendKey=sendKey||'enter'; + showToast('Settings saved (password set — login now required)'); + toggleSettings(); + return; + }catch(e){showToast('Save failed: '+e.message);return;} } try{ await api('/api/settings',{method:'POST',body:JSON.stringify(body)}); @@ -708,6 +704,21 @@ async function signOut(){ } } +async function disableAuth(){ + if(!confirm('Disable password protection? Anyone will be able to access this instance.')) return; + try{ + await api('/api/settings',{method:'POST',body:JSON.stringify({_clear_password:true})}); + showToast('Auth disabled — password protection removed'); + // Hide both auth buttons since auth is now off + const disableBtn=$('btnDisableAuth'); + if(disableBtn) disableBtn.style.display='none'; + const signOutBtn=$('btnSignOut'); + if(signOutBtn) signOutBtn.style.display='none'; + }catch(e){ + showToast('Failed to disable auth: '+e.message); + } +} + // Close settings on overlay click (not panel click) document.addEventListener('click',e=>{ const overlay=$('settingsOverlay');