Admins can change passwords. Request user confirmation to open gate
This commit is contained in:
@@ -107,7 +107,10 @@
|
||||
<div id="admin-view" class="hidden">
|
||||
<header class="app-header">
|
||||
<h2>⚙️ Admin - Lagomare Gates</h2>
|
||||
<button id="logout-btn" class="btn btn-ghost" style="font-size:.85rem;padding:.5rem 1rem">Logout</button>
|
||||
<div style="display:flex;align-items:center;gap:.75rem">
|
||||
<span id="header-username" style="font-size:.85rem;color:var(--text-muted)"></span>
|
||||
<button id="logout-btn" class="btn btn-ghost" style="font-size:.85rem;padding:.5rem 1rem">Logout</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="tabs">
|
||||
@@ -347,6 +350,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Change password modal ─────────────────────────────────────────────── -->
|
||||
<div id="chpw-modal" class="modal-backdrop hidden">
|
||||
<div class="modal">
|
||||
<h3>Change Password</h3>
|
||||
<form id="chpw-form">
|
||||
<input type="hidden" id="chpw-username" />
|
||||
<div class="field">
|
||||
<label for="chpw-new">New password</label>
|
||||
<input id="chpw-new" type="password" autocomplete="new-password" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="chpw-confirm">Confirm password</label>
|
||||
<input id="chpw-confirm" type="password" autocomplete="new-password" required />
|
||||
</div>
|
||||
<p id="chpw-error" class="error-msg hidden"></p>
|
||||
<div class="modal-actions">
|
||||
<button type="button" id="chpw-cancel" class="btn btn-ghost">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Admin user modal ──────────────────────────────────────────────────── -->
|
||||
<div id="admin-modal" class="modal-backdrop hidden">
|
||||
<div class="modal">
|
||||
|
||||
@@ -27,7 +27,7 @@ async function api(method, path, body) {
|
||||
if (res.status === 401) {
|
||||
clearToken();
|
||||
showLogin();
|
||||
throw new Error("Session expired.");
|
||||
throw new Error("Session expired or invalid credentials");
|
||||
}
|
||||
if (!res.ok) {
|
||||
const j = await res.json().catch(() => null);
|
||||
@@ -53,7 +53,9 @@ function showLogin() {
|
||||
function showAdmin() {
|
||||
document.getElementById("login-view").classList.add("hidden");
|
||||
document.getElementById("admin-view").classList.remove("hidden");
|
||||
const isAdmin = _tokenPayload().scope === "admin";
|
||||
const payload = _tokenPayload();
|
||||
const isAdmin = payload.scope === "admin";
|
||||
document.getElementById("header-username").textContent = payload.sub || "";
|
||||
document.querySelectorAll(".admin-only").forEach(el => {
|
||||
el.style.display = isAdmin ? "" : "none";
|
||||
});
|
||||
@@ -504,11 +506,21 @@ async function loadAdmins() {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td>${esc(u.username)}${u.username === me ? ' <span class="badge badge-green" style="font-size:.75em">you</span>' : ""} ${roleBadge}</td>
|
||||
<td style="text-align:right">
|
||||
<td style="text-align:right;display:flex;gap:.5rem;justify-content:flex-end">
|
||||
<button class="btn btn-ghost" style="font-size:.8rem;padding:.35rem .9rem" data-chpw="${esc(u.username)}">Change password</button>
|
||||
${u.username !== me ? `<button class="btn btn-danger" style="font-size:.8rem;padding:.35rem .9rem" data-del-admin="${esc(u.username)}">Delete</button>` : ""}
|
||||
</td>`;
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
tbody.querySelectorAll("[data-chpw]").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
document.getElementById("chpw-username").value = btn.dataset.chpw;
|
||||
document.getElementById("chpw-new").value = "";
|
||||
document.getElementById("chpw-confirm").value = "";
|
||||
document.getElementById("chpw-error").classList.add("hidden");
|
||||
document.getElementById("chpw-modal").classList.remove("hidden");
|
||||
});
|
||||
});
|
||||
tbody.querySelectorAll("[data-del-admin]").forEach(btn => {
|
||||
btn.addEventListener("click", async () => {
|
||||
if (!confirm(`Delete admin "${btn.dataset.delAdmin}"?`)) return;
|
||||
@@ -549,6 +561,32 @@ document.getElementById("admin-form").addEventListener("submit", async e => {
|
||||
}
|
||||
});
|
||||
|
||||
// ── Change password modal ─────────────────────────────────────────────────────
|
||||
document.getElementById("chpw-cancel").addEventListener("click", () => {
|
||||
document.getElementById("chpw-modal").classList.add("hidden");
|
||||
});
|
||||
document.getElementById("chpw-form").addEventListener("submit", async e => {
|
||||
e.preventDefault();
|
||||
const username = document.getElementById("chpw-username").value;
|
||||
const newPw = document.getElementById("chpw-new").value;
|
||||
const confirm = document.getElementById("chpw-confirm").value;
|
||||
const errEl = document.getElementById("chpw-error");
|
||||
errEl.classList.add("hidden");
|
||||
if (newPw !== confirm) {
|
||||
errEl.textContent = "Passwords do not match";
|
||||
errEl.classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await api("PATCH", `/api/admin/admins/${encodeURIComponent(username)}/password`, { new_password: newPw });
|
||||
document.getElementById("chpw-modal").classList.add("hidden");
|
||||
showToast("Password updated");
|
||||
} catch (e) {
|
||||
errEl.textContent = e.message;
|
||||
errEl.classList.remove("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
// ── Load all data ─────────────────────────────────────────────────────────────
|
||||
function loadAllData() {
|
||||
const isAdmin = _tokenPayload().scope === "admin";
|
||||
|
||||
@@ -28,7 +28,7 @@ async function apiFetch(method, path, body) {
|
||||
if (res.status === 401) {
|
||||
clearToken();
|
||||
showLogin();
|
||||
throw new Error("Session expired - please log in again.");
|
||||
throw new Error("Session expired or invalid keypass");
|
||||
}
|
||||
if (!res.ok) {
|
||||
const json = await res.json().catch(() => null);
|
||||
@@ -71,7 +71,7 @@ function renderGates(gates) {
|
||||
btn.className = `gate-btn ${gate.gate_type}`;
|
||||
btn.dataset.gateId = gate.id;
|
||||
btn.innerHTML = `<span class="icon">${icon}</span><span>${gate.name}</span>`;
|
||||
btn.addEventListener("click", () => handleOpenGate(btn, gate.id));
|
||||
btn.addEventListener("click", () => handleOpenGate(btn, gate));
|
||||
grid.appendChild(btn);
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,28 @@ async function loadGates() {
|
||||
}
|
||||
|
||||
// ── Open gate action ──────────────────────────────────────────────────────────
|
||||
async function handleOpenGate(btn, gateId) {
|
||||
let _pendingGate = null;
|
||||
|
||||
document.getElementById("confirm-cancel").addEventListener("click", () => {
|
||||
document.getElementById("confirm-modal").classList.add("hidden");
|
||||
_pendingGate = null;
|
||||
});
|
||||
document.getElementById("confirm-ok").addEventListener("click", () => {
|
||||
document.getElementById("confirm-modal").classList.add("hidden");
|
||||
if (_pendingGate) {
|
||||
const { btn, gateId } = _pendingGate;
|
||||
_pendingGate = null;
|
||||
_doOpenGate(btn, gateId);
|
||||
}
|
||||
});
|
||||
|
||||
async function handleOpenGate(btn, gate) {
|
||||
_pendingGate = { btn, gateId: gate.id };
|
||||
document.getElementById("confirm-gate-name").textContent = gate.name;
|
||||
document.getElementById("confirm-modal").classList.remove("hidden");
|
||||
}
|
||||
|
||||
async function _doOpenGate(btn, gateId) {
|
||||
btn.disabled = true;
|
||||
btn.classList.add("loading");
|
||||
btn.classList.remove("ok", "fail");
|
||||
|
||||
@@ -131,6 +131,18 @@
|
||||
<!-- ── Toast ───────────────────────────────────────────────────────────── -->
|
||||
<div id="toast" class="toast hidden" aria-live="assertive"></div>
|
||||
|
||||
<!-- ── Confirm open modal ─────────────────────────────────────────────── -->
|
||||
<div id="confirm-modal" class="modal-backdrop hidden">
|
||||
<div class="modal" style="max-width:320px;text-align:center">
|
||||
<p style="font-size:1.1rem;font-weight:700;margin-bottom:.25rem" id="confirm-gate-name"></p>
|
||||
<p style="color:var(--text-muted);font-size:.9rem;margin-bottom:1.5rem">Open this gate?</p>
|
||||
<div style="display:flex;gap:.75rem;justify-content:center">
|
||||
<button id="confirm-cancel" class="btn btn-ghost" style="flex:1">Cancel</button>
|
||||
<button id="confirm-ok" class="btn btn-primary" style="flex:1">Open</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user