Remove prompt() dialogs, breach warnings, cost display; fix 2FA disable UI
- Replace browser prompt() with inline UI for: 2FA disable (password field), admin password reset (inline input), admin test email (inline input) - Remove all password breach warning UI (login, register, settings) Backend HIBP check endpoint remains but is no longer called from frontend - Remove model cost display from dropdown and header badge - Hide empty cost-badge element in header - Fix model dropdown to flat list (no category grouping)
This commit is contained in:
parent
04f3aa56cb
commit
4fa2b58d75
5 changed files with 83 additions and 69 deletions
|
|
@ -95,7 +95,6 @@
|
|||
<div class="form-group" style="max-width:340px;">
|
||||
<label>New Password (8+ characters)</label>
|
||||
<input type="password" id="pw-new" minlength="8" placeholder="Enter new password">
|
||||
<div id="pw-new-warning" class="pw-breach-warning" style="display:none;"></div>
|
||||
</div>
|
||||
<div class="form-group" style="max-width:340px;">
|
||||
<label>Confirm New Password</label>
|
||||
|
|
@ -110,7 +109,17 @@
|
|||
<h3><i class="fas fa-shield-halved"></i> Two-Factor Authentication</h3>
|
||||
<p id="2fa-status">Status: Loading...</p>
|
||||
<button id="btn-setup-2fa" class="btn-sm btn-primary">Enable 2FA</button>
|
||||
<button id="btn-disable-2fa" class="btn-sm btn-ghost hidden">Disable 2FA</button>
|
||||
<button id="btn-disable-2fa" class="btn-sm btn-ghost hidden" style="color:var(--red);">Disable 2FA</button>
|
||||
<div id="2fa-disable-confirm" class="hidden" style="margin-top:10px;padding:12px;background:var(--g50);border-radius:8px;border:1px solid var(--g200);max-width:340px;">
|
||||
<div class="form-group" style="margin-bottom:8px;">
|
||||
<label>Enter your password to confirm:</label>
|
||||
<input type="password" id="2fa-disable-password" placeholder="Password">
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;">
|
||||
<button id="btn-disable-2fa-confirm" class="btn-sm btn-primary" style="background:var(--red);border-color:var(--red);">Confirm Disable</button>
|
||||
<button id="btn-disable-2fa-cancel" class="btn-sm btn-ghost">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="2fa-setup" class="hidden">
|
||||
<p>Scan this QR code with your authenticator app:</p>
|
||||
<img id="2fa-qr" src="" alt="QR Code">
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@
|
|||
<div class="form-group">
|
||||
<label>Password</label>
|
||||
<input type="password" id="login-password" required placeholder="••••••••">
|
||||
<div id="login-pw-warning" class="pw-breach-warning" style="display:none;"></div>
|
||||
</div>
|
||||
<div id="totp-group" class="form-group hidden">
|
||||
<label>2FA Code</label>
|
||||
|
|
@ -85,7 +84,6 @@
|
|||
<div class="form-group">
|
||||
<label>Password (8+ characters)</label>
|
||||
<input type="password" id="reg-password" required minlength="8" placeholder="••••••••">
|
||||
<div id="reg-pw-warning" class="pw-breach-warning" style="display:none;"></div>
|
||||
</div>
|
||||
<div class="cf-turnstile" data-sitekey="0x4AAAAAAC0VtKAhC8rzpMx6" data-theme="light"></div>
|
||||
<button type="submit" class="btn-auth">Create Account</button>
|
||||
|
|
@ -143,7 +141,7 @@
|
|||
<div class="model-selector">
|
||||
<label><i class="fas fa-robot"></i></label>
|
||||
<select id="global-model-select"></select>
|
||||
<span id="model-cost-badge" class="cost-badge"></span>
|
||||
<span id="model-cost-badge" class="cost-badge" style="display:none;"></span>
|
||||
</div>
|
||||
<div class="header-buttons">
|
||||
<button id="btn-settings" class="btn-header" title="Settings">
|
||||
|
|
|
|||
|
|
@ -210,12 +210,27 @@
|
|||
break;
|
||||
|
||||
case 'reset-pw':
|
||||
var newPw = prompt('New password for ' + email + ' (8+ characters):');
|
||||
if (!newPw) return;
|
||||
if (newPw.length < 8) { showToast('Password must be 8+ characters', 'error'); return; }
|
||||
adminPost('/api/admin/users/' + userId + '/reset-password', { newPassword: newPw }, function() {
|
||||
showToast('Password reset for ' + email, 'success');
|
||||
});
|
||||
// Show inline password input instead of prompt()
|
||||
var existingResetRow = document.getElementById('admin-reset-pw-row');
|
||||
if (existingResetRow) existingResetRow.remove();
|
||||
var actionCell = btn.closest('td') || btn.parentElement;
|
||||
var resetRow = document.createElement('div');
|
||||
resetRow.id = 'admin-reset-pw-row';
|
||||
resetRow.style.cssText = 'margin-top:8px;display:flex;gap:6px;align-items:center;';
|
||||
resetRow.innerHTML = '<input type="password" id="admin-reset-pw-input" placeholder="New password (8+)" style="padding:5px 8px;border:1px solid var(--g300);border-radius:6px;font-size:12px;width:160px;">'
|
||||
+ '<button class="btn-sm btn-primary" id="admin-reset-pw-submit" style="font-size:11px;">Set</button>'
|
||||
+ '<button class="btn-sm btn-ghost" id="admin-reset-pw-cancel" style="font-size:11px;">Cancel</button>';
|
||||
actionCell.appendChild(resetRow);
|
||||
document.getElementById('admin-reset-pw-input').focus();
|
||||
document.getElementById('admin-reset-pw-submit').onclick = function() {
|
||||
var val = document.getElementById('admin-reset-pw-input').value;
|
||||
if (!val || val.length < 8) { showToast('Password must be 8+ characters', 'error'); return; }
|
||||
adminPost('/api/admin/users/' + userId + '/reset-password', { newPassword: val }, function() {
|
||||
showToast('Password reset for ' + email, 'success');
|
||||
resetRow.remove();
|
||||
});
|
||||
};
|
||||
document.getElementById('admin-reset-pw-cancel').onclick = function() { resetRow.remove(); };
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
|
|
@ -388,19 +403,34 @@
|
|||
|
||||
function sendTestEmail() {
|
||||
var template = document.getElementById('cms-email-template').value;
|
||||
var to = prompt('Send test email to (enter email address):');
|
||||
if (!to || !to.trim()) return;
|
||||
fetch('/api/admin/config/test-email', {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ to: to.trim(), template: template })
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.success) showToast('Test email sent to ' + to, 'success');
|
||||
else showToast(data.error || 'Send failed', 'error');
|
||||
})
|
||||
.catch(function() { showToast('Request failed', 'error'); });
|
||||
var sendBtn = document.getElementById('btn-test-email');
|
||||
// Show inline email input instead of prompt()
|
||||
var existing = document.getElementById('admin-test-email-row');
|
||||
if (existing) { existing.remove(); return; }
|
||||
var row = document.createElement('div');
|
||||
row.id = 'admin-test-email-row';
|
||||
row.style.cssText = 'margin-top:8px;display:flex;gap:6px;align-items:center;';
|
||||
row.innerHTML = '<input type="email" id="admin-test-email-input" placeholder="recipient@example.com" style="padding:5px 8px;border:1px solid var(--g300);border-radius:6px;font-size:12px;width:220px;">'
|
||||
+ '<button class="btn-sm btn-primary" id="admin-test-email-submit" style="font-size:11px;">Send</button>'
|
||||
+ '<button class="btn-sm btn-ghost" id="admin-test-email-cancel" style="font-size:11px;">Cancel</button>';
|
||||
if (sendBtn) sendBtn.parentElement.appendChild(row);
|
||||
document.getElementById('admin-test-email-input').focus();
|
||||
document.getElementById('admin-test-email-submit').onclick = function() {
|
||||
var to = document.getElementById('admin-test-email-input').value.trim();
|
||||
if (!to) { showToast('Enter an email address', 'error'); return; }
|
||||
fetch('/api/admin/config/test-email', {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ to: to, template: template })
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.success) { showToast('Test email sent to ' + to, 'success'); row.remove(); }
|
||||
else showToast(data.error || 'Send failed', 'error');
|
||||
})
|
||||
.catch(function() { showToast('Request failed', 'error'); });
|
||||
};
|
||||
document.getElementById('admin-test-email-cancel').onclick = function() { row.remove(); };
|
||||
}
|
||||
|
||||
// ---- AI PROMPTS ----
|
||||
|
|
|
|||
|
|
@ -159,11 +159,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
if (typeof renderAudioBackups === 'function') renderAudioBackups();
|
||||
if (typeof loadDocuments === 'function') loadDocuments();
|
||||
initBrowserWhisperSettings();
|
||||
// Wire HIBP check on change-password new password field
|
||||
var pwNewEl = document.getElementById('pw-new');
|
||||
if (pwNewEl && typeof checkPasswordBreach === 'function') {
|
||||
pwNewEl.onblur = function() { checkPasswordBreach(pwNewEl, 'pw-new-warning'); };
|
||||
}
|
||||
}
|
||||
if (e.detail && e.detail.tab === 'faq') {
|
||||
// Wire FAQ accordion after component loads
|
||||
|
|
@ -299,7 +294,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
window._currentModels.forEach(function(m) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = m.id;
|
||||
opt.textContent = m.name + (m.cost && m.cost !== '?' ? ' (' + m.cost + ')' : '');
|
||||
opt.textContent = m.name;
|
||||
selectEl.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
|
@ -311,8 +306,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
window._buildModelOptions(modelSelect);
|
||||
// Select the admin-configured default
|
||||
if (defaultModelId) modelSelect.value = defaultModelId;
|
||||
var defaultM = window._currentModels.find(function(x) { return x.id === modelSelect.value; });
|
||||
if (costBadge) costBadge.textContent = window._currentProvider.toUpperCase() + ' | ' + (defaultM ? defaultM.cost : '');
|
||||
if (costBadge) costBadge.textContent = '';
|
||||
}
|
||||
|
||||
// Populate all per-tab model selectors already in DOM
|
||||
|
|
@ -326,7 +320,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
if (modelSelect) {
|
||||
modelSelect.addEventListener('change', function() {
|
||||
var m = window._currentModels.find(function(x) { return x.id === modelSelect.value; });
|
||||
if (m && costBadge) costBadge.textContent = window._currentProvider.toUpperCase() + ' | ' + m.cost;
|
||||
showToast('Model: ' + modelSelect.value.split('/').pop(), 'info');
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -286,14 +286,32 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
// Disable 2FA
|
||||
if (target.id === 'btn-disable-2fa' || target.closest('#btn-disable-2fa')) {
|
||||
e.preventDefault();
|
||||
var pw = prompt('Enter password to disable 2FA:');
|
||||
if (!pw) return;
|
||||
var confirmBox = document.getElementById('2fa-disable-confirm');
|
||||
if (confirmBox) { confirmBox.classList.remove('hidden'); }
|
||||
return;
|
||||
}
|
||||
if (target.id === 'btn-disable-2fa-cancel' || target.closest('#btn-disable-2fa-cancel')) {
|
||||
e.preventDefault();
|
||||
var confirmBox = document.getElementById('2fa-disable-confirm');
|
||||
if (confirmBox) { confirmBox.classList.add('hidden'); }
|
||||
var pwField = document.getElementById('2fa-disable-password');
|
||||
if (pwField) pwField.value = '';
|
||||
return;
|
||||
}
|
||||
if (target.id === 'btn-disable-2fa-confirm' || target.closest('#btn-disable-2fa-confirm')) {
|
||||
e.preventDefault();
|
||||
var pwField = document.getElementById('2fa-disable-password');
|
||||
var pw = pwField ? pwField.value : '';
|
||||
if (!pw) { showToast('Enter your password', 'error'); return; }
|
||||
fetch('/api/auth/disable-2fa', {
|
||||
method: 'POST', headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ password: pw })
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
var confirmBox = document.getElementById('2fa-disable-confirm');
|
||||
if (confirmBox) confirmBox.classList.add('hidden');
|
||||
if (pwField) pwField.value = '';
|
||||
if (data.success) { showToast('2FA disabled', 'info'); load2FAStatus(); }
|
||||
else showToast(data.error || 'Failed', 'error');
|
||||
});
|
||||
|
|
@ -505,38 +523,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
});
|
||||
}
|
||||
|
||||
// ---- PASSWORD BREACH CHECK (inline, on blur) ----
|
||||
window.checkPasswordBreach = function checkPasswordBreach(inputEl, warningId) {
|
||||
var pw = inputEl.value;
|
||||
var warningEl = document.getElementById(warningId);
|
||||
if (!warningEl) return;
|
||||
if (!pw || pw.length < 8) { warningEl.style.display = 'none'; return; }
|
||||
fetch('/api/auth/check-password', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password: pw })
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.breached) {
|
||||
warningEl.innerHTML = '<i class="fas fa-triangle-exclamation"></i> This password has appeared in ' + data.count.toLocaleString() + ' data breaches. You can still use it, but consider a different one.';
|
||||
warningEl.style.display = 'block';
|
||||
} else {
|
||||
warningEl.style.display = 'none';
|
||||
}
|
||||
})
|
||||
.catch(function() { warningEl.style.display = 'none'; });
|
||||
}
|
||||
|
||||
var regPwInput = document.getElementById('reg-password');
|
||||
if (regPwInput) {
|
||||
regPwInput.addEventListener('blur', function() { checkPasswordBreach(regPwInput, 'reg-pw-warning'); });
|
||||
}
|
||||
var loginPwInput = document.getElementById('login-password');
|
||||
if (loginPwInput) {
|
||||
loginPwInput.addEventListener('blur', function() { checkPasswordBreach(loginPwInput, 'login-pw-warning'); });
|
||||
}
|
||||
|
||||
// ---- SESSION MANAGEMENT ----
|
||||
function timeAgo(date) {
|
||||
var s = Math.floor((Date.now() - date.getTime()) / 1000);
|
||||
|
|
@ -630,8 +616,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
if (data.passwordWarning) setTimeout(function() { showToast(data.passwordWarning, 'warning'); }, 1500);
|
||||
current.value = ''; newPw.value = ''; confirm.value = '';
|
||||
if (status) { status.textContent = ''; }
|
||||
var warning = document.getElementById('pw-new-warning');
|
||||
if (warning) warning.style.display = 'none';
|
||||
if (typeof loadSessions === 'function') loadSessions();
|
||||
} else {
|
||||
showToast(data.error || 'Failed', 'error');
|
||||
|
|
|
|||
Loading…
Reference in a new issue