Security UI: show saved login password / recovery question state
After saving a password or recovery question, a refresh made the section look unset (passwords are never echoed back to the browser), so it seemed like you had to redo it. Now the saved state is reflected: - "✓ A login password is set" appears when the admin has a password; the field becomes "Enter a new password to change it". - "✓ Recovery question saved: <question>" appears, the saved question is pre- selected (preset or custom), and the answer field becomes "Enter a new answer to change it". - Shown both on load (applyLoginSavedState from /api/profiles, which now includes recovery_question — not secret, already shown on the sign-in screen) and immediately after saving. 64 integrity tests pass.
This commit is contained in:
parent
09d4bbc530
commit
fece771dd0
5 changed files with 61 additions and 2 deletions
|
|
@ -5248,6 +5248,7 @@ class MusicDatabase:
|
|||
'has_pin': row['pin_hash'] is not None,
|
||||
'has_password': row['password_hash'] is not None if 'password_hash' in columns else False,
|
||||
'has_recovery': row['recovery_answer_hash'] is not None if 'recovery_answer_hash' in columns else False,
|
||||
'recovery_question': row['recovery_question'] if 'recovery_question' in columns else None,
|
||||
'home_page': row['home_page'] if 'home_page' in columns else None,
|
||||
'allowed_pages': json.loads(ap_raw) if ap_raw else None,
|
||||
'can_download': bool(row['can_download']) if 'can_download' in columns else True,
|
||||
|
|
@ -5280,6 +5281,7 @@ class MusicDatabase:
|
|||
'has_pin': row['pin_hash'] is not None,
|
||||
'has_password': row['password_hash'] is not None if 'password_hash' in columns else False,
|
||||
'has_recovery': row['recovery_answer_hash'] is not None if 'recovery_answer_hash' in columns else False,
|
||||
'recovery_question': row['recovery_question'] if 'recovery_question' in columns else None,
|
||||
'home_page': row['home_page'] if 'home_page' in columns else None,
|
||||
'allowed_pages': json.loads(ap_raw) if ap_raw else None,
|
||||
'can_download': bool(row['can_download']) if 'can_download' in columns else True,
|
||||
|
|
|
|||
|
|
@ -6040,6 +6040,7 @@
|
|||
|
||||
<div class="form-group">
|
||||
<label>Step 1 — Admin login password:</label>
|
||||
<div class="security-saved-status" id="security-login-password-status" style="display:none;">✓ A login password is set</div>
|
||||
<input type="password" id="security-login-password" placeholder="Enter password (min 6)" maxlength="200" autocomplete="new-password" style="margin: 6px 0;">
|
||||
<input type="password" id="security-login-password-confirm" placeholder="Confirm password" maxlength="200" autocomplete="new-password" style="margin-bottom: 6px;">
|
||||
<button class="auth-button" onclick="saveLoginPassword()">Save Password</button>
|
||||
|
|
@ -6051,6 +6052,7 @@
|
|||
<div class="setting-help-text" style="margin-bottom: 8px;">
|
||||
So you can reset a forgotten password by answering it on the sign-in screen.
|
||||
</div>
|
||||
<div class="security-saved-status" id="security-recovery-status" style="display:none;"></div>
|
||||
<select id="security-recovery-question" class="form-select" onchange="handleRecoveryQuestionChange()" style="margin-bottom: 6px;">
|
||||
<option value="">— Select a question —</option>
|
||||
<option>What was the name of your first pet?</option>
|
||||
|
|
|
|||
|
|
@ -588,9 +588,11 @@ async function saveLoginPassword() {
|
|||
const data = await res.json();
|
||||
if (data.success) {
|
||||
show('Admin login password saved', true);
|
||||
if (input) input.value = '';
|
||||
if (input) { input.value = ''; input.placeholder = 'Enter a new password to change it'; }
|
||||
if (confirmInput) confirmInput.value = '';
|
||||
updateRequireLoginGate(true); // Step 1 done → unlock Step 3
|
||||
const st = document.getElementById('security-login-password-status');
|
||||
if (st) st.style.display = 'block';
|
||||
}
|
||||
else show(data.error || 'Failed to save password', false);
|
||||
} catch (e) { show('Connection error', false); }
|
||||
|
|
@ -614,6 +616,45 @@ function updateRequireLoginGate(hasPassword) {
|
|||
}
|
||||
}
|
||||
|
||||
// Reflect already-saved login credentials. Passwords are never sent to the
|
||||
// browser, so instead of an empty field (which looks unset after a refresh) we
|
||||
// show that one is set and pre-fill the saved recovery question.
|
||||
function applyLoginSavedState(profile) {
|
||||
const hasPassword = profile?.has_password || false;
|
||||
const hasRecovery = profile?.has_recovery || false;
|
||||
const question = profile?.recovery_question || '';
|
||||
|
||||
const pwStatus = document.getElementById('security-login-password-status');
|
||||
const pwField = document.getElementById('security-login-password');
|
||||
const pwConfirm = document.getElementById('security-login-password-confirm');
|
||||
if (pwStatus) pwStatus.style.display = hasPassword ? 'block' : 'none';
|
||||
if (hasPassword) {
|
||||
if (pwField) pwField.placeholder = 'Enter a new password to change it';
|
||||
if (pwConfirm) pwConfirm.placeholder = 'Confirm new password';
|
||||
}
|
||||
|
||||
const recStatus = document.getElementById('security-recovery-status');
|
||||
const recSel = document.getElementById('security-recovery-question');
|
||||
const recCustom = document.getElementById('security-recovery-custom');
|
||||
const recAnswer = document.getElementById('security-recovery-answer');
|
||||
if (recStatus) {
|
||||
recStatus.style.display = hasRecovery ? 'block' : 'none';
|
||||
recStatus.textContent = hasRecovery
|
||||
? ('✓ Recovery question saved' + (question ? ': “' + question + '”' : ''))
|
||||
: '';
|
||||
}
|
||||
if (hasRecovery) {
|
||||
if (recSel && question) {
|
||||
recSel.value = question; // preset options default value = their text
|
||||
if (recSel.value !== question) { // not a preset → custom question
|
||||
recSel.value = '__custom__';
|
||||
if (recCustom) { recCustom.style.display = 'block'; recCustom.value = question; }
|
||||
}
|
||||
}
|
||||
if (recAnswer) recAnswer.placeholder = 'Enter a new answer to change it';
|
||||
}
|
||||
}
|
||||
|
||||
function handleRecoveryQuestionChange() {
|
||||
const sel = document.getElementById('security-recovery-question');
|
||||
const custom = document.getElementById('security-recovery-custom');
|
||||
|
|
@ -641,7 +682,13 @@ async function saveRecoveryQuestion() {
|
|||
body: JSON.stringify({ question, answer }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) { show('Recovery question saved', true); const a = document.getElementById('security-recovery-answer'); if (a) a.value = ''; }
|
||||
if (data.success) {
|
||||
show('Recovery question saved', true);
|
||||
const a = document.getElementById('security-recovery-answer');
|
||||
if (a) { a.value = ''; a.placeholder = 'Enter a new answer to change it'; }
|
||||
const rst = document.getElementById('security-recovery-status');
|
||||
if (rst) { rst.style.display = 'block'; rst.textContent = '✓ Recovery question saved: “' + question + '”'; }
|
||||
}
|
||||
else show(data.error || 'Failed to save', false);
|
||||
} catch (e) { show('Connection error', false); }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1425,6 +1425,8 @@ async function loadSettingsData() {
|
|||
// Login: the "Require login" toggle is gated on an admin password —
|
||||
// visually locked until Step 1 is done (anti-lockout, made obvious).
|
||||
updateRequireLoginGate(adminProfile?.has_password || false);
|
||||
// Show already-saved password/recovery state (vs looking unset).
|
||||
applyLoginSavedState(adminProfile);
|
||||
} catch (error) {
|
||||
console.error('Error loading security settings:', error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67927,3 +67927,9 @@ body.em-scroll-lock { overflow: hidden; }
|
|||
.security-locked .toggle-label {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.security-saved-status {
|
||||
font-size: 12px;
|
||||
color: #4caf50;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue