Security UI: confirm-password field for admin password + recovery reset

Mirror the PIN setup's confirm step so a typo can't silently set a password you
can't reproduce. Both the Step 1 admin password (Settings) and the forgot-password
reset (login screen) now require entering it twice and reject a mismatch before
saving. 64 integrity tests pass.
This commit is contained in:
BoulderBadgeDad 2026-06-10 22:41:23 -07:00
parent 2bb9bc1357
commit 09d4bbc530
2 changed files with 10 additions and 2 deletions

View file

@ -75,7 +75,8 @@
<div id="recovery-answer-section" style="display: none;">
<p id="recovery-question-text" class="launch-pin-subtitle" style="margin-top: 12px; font-weight: 600;"></p>
<input type="text" id="recovery-answer" class="launch-pin-input" placeholder="Your answer" autocomplete="off" maxlength="120" style="margin-bottom: 8px; letter-spacing: normal;">
<input type="password" id="recovery-new-password" class="launch-pin-input" placeholder="New password (min 6)" autocomplete="new-password" maxlength="200" style="letter-spacing: normal;" onkeydown="if(event.key==='Enter') submitRecoveryReset()">
<input type="password" id="recovery-new-password" class="launch-pin-input" placeholder="New password (min 6)" autocomplete="new-password" maxlength="200" style="margin-bottom: 8px; letter-spacing: normal;">
<input type="password" id="recovery-new-password-confirm" class="launch-pin-input" placeholder="Confirm new password" autocomplete="new-password" maxlength="200" style="letter-spacing: normal;" onkeydown="if(event.key==='Enter') submitRecoveryReset()">
<button class="launch-pin-submit" onclick="submitRecoveryReset()">Reset password</button>
</div>
<p id="recovery-error" class="launch-pin-error" style="display: none;"></p>
@ -6039,7 +6040,8 @@
<div class="form-group">
<label>Step 1 — Admin login password:</label>
<input type="password" id="security-login-password" placeholder="Set admin login password" maxlength="200" autocomplete="new-password" style="margin: 6px 0;">
<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>
<p id="security-login-password-msg" class="setting-help-text" style="margin-top: 6px; display: none;"></p>
</div>

View file

@ -484,11 +484,13 @@ async function submitRecoveryReset() {
const username = (document.getElementById('recovery-username')?.value || '').trim();
const answer = document.getElementById('recovery-answer')?.value || '';
const newPassword = document.getElementById('recovery-new-password')?.value || '';
const confirmPassword = document.getElementById('recovery-new-password-confirm')?.value || '';
const errEl = document.getElementById('recovery-error');
const showErr = (m) => { if (errEl) { errEl.textContent = m; errEl.style.display = 'block'; } };
if (errEl) errEl.style.display = 'none';
if (!answer || !newPassword) { showErr('Enter your answer and a new password'); return; }
if (newPassword.length < 6) { showErr('New password must be at least 6 characters'); return; }
if (newPassword !== confirmPassword) { showErr('Passwords do not match'); return; }
try {
const res = await fetch('/api/auth/recovery-reset', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
@ -566,8 +568,10 @@ function showLaunchPinScreen() {
async function saveLoginPassword() {
const input = document.getElementById('security-login-password');
const confirmInput = document.getElementById('security-login-password-confirm');
const msg = document.getElementById('security-login-password-msg');
const password = input?.value || '';
const confirm = confirmInput?.value || '';
const show = (text, ok) => {
if (!msg) return;
msg.textContent = text;
@ -575,6 +579,7 @@ async function saveLoginPassword() {
msg.style.display = 'block';
};
if (!password || password.length < 6) { show('Password must be at least 6 characters', false); return; }
if (password !== confirm) { show('Passwords do not match', false); return; }
try {
const res = await fetch('/api/profiles/1/set-password', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
@ -584,6 +589,7 @@ async function saveLoginPassword() {
if (data.success) {
show('Admin login password saved', true);
if (input) input.value = '';
if (confirmInput) confirmInput.value = '';
updateRequireLoginGate(true); // Step 1 done → unlock Step 3
}
else show(data.error || 'Failed to save password', false);