fix(auth): blank password field no longer clears auth; add Disable Auth button
The previous logic treated a blank password field as intent to clear auth, which meant saving any other setting (model, send key, etc.) would silently disable password protection. New behavior: - Blank password field + Save Settings = no change to auth (do nothing) - Password field with content + Save = set/change password (unchanged) - 'Disable Auth' button = explicit confirmation-gated clear (new) UI changes: - index.html: updated description text to 'Leave blank to keep current setting'; added 'Disable Auth' button (amber, shown only when auth active) - panels.js: saveSettings() skips password logic entirely when field is blank; loadSettingsPanel() shows/hides both btnDisableAuth and btnSignOut based on auth_enabled; new disableAuth() function sends _clear_password:true after confirm() prompt and hides both auth buttons on success Server: no logic changes needed; _clear_password handling in save_settings() is now only triggered by the explicit Disable Auth action.
This commit is contained in:
@@ -624,6 +624,9 @@ def save_settings(settings: dict) -> dict:
|
|||||||
if raw_pw and isinstance(raw_pw, str) and raw_pw.strip():
|
if raw_pw and isinstance(raw_pw, str) and raw_pw.strip():
|
||||||
salt = str(STATE_DIR).encode()
|
salt = str(STATE_DIR).encode()
|
||||||
current['password_hash'] = _hl.sha256(salt + raw_pw.strip().encode()).hexdigest()
|
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():
|
for k, v in settings.items():
|
||||||
if k in _SETTINGS_ALLOWED_KEYS:
|
if k in _SETTINGS_ALLOWED_KEYS:
|
||||||
# Validate enum-constrained keys
|
# Validate enum-constrained keys
|
||||||
|
|||||||
@@ -374,7 +374,9 @@ def handle_post(handler, parsed):
|
|||||||
|
|
||||||
# ── Settings (POST) ──
|
# ── Settings (POST) ──
|
||||||
if parsed.path == '/api/settings':
|
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) ──
|
# ── Session pin (POST) ──
|
||||||
if parsed.path == '/api/session/pin':
|
if parsed.path == '/api/session/pin':
|
||||||
|
|||||||
@@ -284,10 +284,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="settings-field" style="border-top:1px solid var(--border);padding-top:12px;margin-top:8px">
|
<div class="settings-field" style="border-top:1px solid var(--border);padding-top:12px;margin-top:8px">
|
||||||
<label for="settingsPassword">Access Password</label>
|
<label for="settingsPassword">Access Password</label>
|
||||||
<div style="font-size:11px;color:var(--muted);margin-bottom:6px">Set a password to require login. Leave blank to disable auth.</div>
|
<div style="font-size:11px;color:var(--muted);margin-bottom:6px">Enter a new password to set or change it. Leave blank to keep current setting.</div>
|
||||||
<input type="password" id="settingsPassword" placeholder="Enter new password (or blank to disable)" style="width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px;font-size:13px">
|
<input type="password" id="settingsPassword" placeholder="Enter new password…" style="width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px;font-size:13px">
|
||||||
</div>
|
</div>
|
||||||
<button class="sm-btn" onclick="saveSettings()" style="margin-top:12px;width:100%;padding:8px;font-weight:600">Save Settings</button>
|
<button class="sm-btn" onclick="saveSettings()" style="margin-top:12px;width:100%;padding:8px;font-weight:600">Save Settings</button>
|
||||||
|
<button class="sm-btn" id="btnDisableAuth" onclick="disableAuth()" style="margin-top:6px;width:100%;padding:8px;font-weight:600;color:#e8a030;border-color:rgba(232,160,48,.3);display:none">Disable Auth</button>
|
||||||
<button class="sm-btn" id="btnSignOut" onclick="signOut()" style="margin-top:6px;width:100%;padding:8px;font-weight:600;color:var(--accent);border-color:rgba(233,69,96,.3);display:none">Sign Out</button>
|
<button class="sm-btn" id="btnSignOut" onclick="signOut()" style="margin-top:6px;width:100%;padding:8px;font-weight:600;color:var(--accent);border-color:rgba(233,69,96,.3);display:none">Sign Out</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -652,11 +652,14 @@ async function loadSettingsPanel(){
|
|||||||
// Password field: always blank (we don't send hash back)
|
// Password field: always blank (we don't send hash back)
|
||||||
const pwField=$('settingsPassword');
|
const pwField=$('settingsPassword');
|
||||||
if(pwField) pwField.value='';
|
if(pwField) pwField.value='';
|
||||||
// Show sign-out button if auth is active
|
// Show auth buttons only when auth is active
|
||||||
try{
|
try{
|
||||||
const authStatus=await api('/api/auth/status');
|
const authStatus=await api('/api/auth/status');
|
||||||
|
const active=authStatus.auth_enabled;
|
||||||
const signOutBtn=$('btnSignOut');
|
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){}
|
||||||
}catch(e){
|
}catch(e){
|
||||||
showToast('Failed to load settings: '+e.message);
|
showToast('Failed to load settings: '+e.message);
|
||||||
@@ -672,22 +675,15 @@ async function saveSettings(){
|
|||||||
if(model) body.default_model=model;
|
if(model) body.default_model=model;
|
||||||
if(workspace) body.default_workspace=workspace;
|
if(workspace) body.default_workspace=workspace;
|
||||||
if(sendKey) body.send_key=sendKey;
|
if(sendKey) body.send_key=sendKey;
|
||||||
// Password: if field has content, hash and save; if blank, clear auth
|
// Password: only act if the field has content; blank = leave auth unchanged
|
||||||
if(pw!==undefined&&pw!==null){
|
if(pw && pw.trim()){
|
||||||
if(pw.trim()){
|
try{
|
||||||
// Hash client-side using the same algo as server (SHA-256 with state-dir salt)
|
await api('/api/settings',{method:'POST',body:JSON.stringify({...body,_set_password:pw.trim()})});
|
||||||
// We send the raw password to the server's dedicated endpoint instead
|
window._sendKey=sendKey||'enter';
|
||||||
try{
|
showToast('Settings saved (password set — login now required)');
|
||||||
await api('/api/settings',{method:'POST',body:JSON.stringify({...body,_set_password:pw.trim()})});
|
toggleSettings();
|
||||||
window._sendKey=sendKey||'enter';
|
return;
|
||||||
showToast('Settings saved (password set — login now required)');
|
}catch(e){showToast('Save failed: '+e.message);return;}
|
||||||
toggleSettings();
|
|
||||||
return;
|
|
||||||
}catch(e){showToast('Save failed: '+e.message);return;}
|
|
||||||
}else{
|
|
||||||
// Blank = clear password (disable auth)
|
|
||||||
body.password_hash=null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
try{
|
try{
|
||||||
await api('/api/settings',{method:'POST',body:JSON.stringify(body)});
|
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)
|
// Close settings on overlay click (not panel click)
|
||||||
document.addEventListener('click',e=>{
|
document.addEventListener('click',e=>{
|
||||||
const overlay=$('settingsOverlay');
|
const overlay=$('settingsOverlay');
|
||||||
|
|||||||
Reference in New Issue
Block a user