Add user management with admin and viewer roles
- data/users.json store; existing single account migrates to first admin - Roles: admin (settings, import, users) and viewer (read-only, own password) - Live role/disabled lookup; per-user session epoch so password changes, resets and disabling sign that user out everywhere - Login brute-force lockout (5 fails -> 5 min), constant-time unknown-user check - /api/users CRUD with last-admin and self-lockout safeguards - Account page (own password + admin user management); role-aware header - Min password length 8; recovery token resets the primary admin and clears lockouts Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b3f1061f5c
commit
1566ac2897
7 files changed
+442
-93
No files matched your search
@@ -88,6 +88,7 @@ button.primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
<div><h1>Settings</h1></div>
|
||||
<div class="header-right">
|
||||
<a href="/dashboard" class="nav-link">← Dashboard</a>
|
||||
<a href="/dashboard/account" class="nav-link">Account</a>
|
||||
<a href="/dashboard/logout" class="nav-link">Sign out</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -204,17 +205,12 @@ button.primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Change Password -->
|
||||
<!-- Account -->
|
||||
<div class="section">
|
||||
<div class="section-hdr">Change Password</div>
|
||||
<div class="settings-grid">
|
||||
<div><label>Current Password</label><input id="pwCurrent" type="password" autocomplete="current-password"></div>
|
||||
<div><label>New Password (min 6 chars)</label><input id="pwNew" type="password" autocomplete="new-password"></div>
|
||||
</div>
|
||||
<div class="settings-actions">
|
||||
<button class="primary" id="changePwBtn">Change Password</button>
|
||||
<span id="pwMsg" class="save-msg"></span>
|
||||
</div>
|
||||
<div class="section-hdr">Account & Users</div>
|
||||
<p class="hint" style="margin:0 0 0.85rem">Change your password and manage who can sign in (admin and viewer accounts).</p>
|
||||
<a class="export-link" href="/dashboard/account">Open account & users →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -413,28 +409,6 @@ async function pollBackfill() {
|
||||
|
||||
document.getElementById('backfillBtn').addEventListener('click', startBackfill);
|
||||
|
||||
// ── Change Password ───────────────────────────────────────────────────────────
|
||||
document.getElementById('changePwBtn').addEventListener('click', async () => {
|
||||
const cur = document.getElementById('pwCurrent')?.value;
|
||||
const nw = document.getElementById('pwNew')?.value;
|
||||
const msg = document.getElementById('pwMsg');
|
||||
if (!cur || !nw || nw.length < 6) { msg.textContent = 'Fill both fields (min 6 chars)'; msg.className = 'save-msg err'; return; }
|
||||
try {
|
||||
const r = await fetch('/api/dashboard/change-password', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ currentPassword: cur, newPassword: nw })
|
||||
});
|
||||
if (r.ok) {
|
||||
msg.textContent = 'Password changed'; msg.className = 'save-msg';
|
||||
document.getElementById('pwCurrent').value = '';
|
||||
document.getElementById('pwNew').value = '';
|
||||
} else {
|
||||
const e = await r.json();
|
||||
msg.textContent = e.error || 'Error'; msg.className = 'save-msg err';
|
||||
}
|
||||
setTimeout(() => msg.textContent = '', 4000);
|
||||
} catch { msg.textContent = 'Error'; msg.className = 'save-msg err'; }
|
||||
});
|
||||
|
||||
// ── Scroll shadow ─────────────────────────────────────────────────────────────
|
||||
window.addEventListener('scroll', () => {
|
||||
|
||||
Reference in new issue
Block a user