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');