Add QR Code for Keypasses

This commit is contained in:
Ettore
2026-05-10 00:41:36 +02:00
parent 0cb35a30cb
commit ff097b31d1
6 changed files with 107 additions and 3 deletions

View File

@@ -418,6 +418,22 @@
</div>
</div>
<!-- ── QR Code modal ─────────────────────────────────────────────────────── -->
<div id="qr-modal" class="modal-backdrop hidden">
<div class="modal" style="text-align:center;max-width:340px">
<h3 style="margin-bottom:.2rem">Keypass QR Code</h3>
<p id="qr-modal-desc" style="color:var(--text-muted);font-size:.85rem;margin-bottom:1.25rem"></p>
<div style="display:flex;justify-content:center;align-items:center;min-height:220px;background:var(--surface2);border-radius:8px;padding:1rem">
<img id="qr-img" src="" alt="QR Code" style="max-width:100%;border-radius:4px;display:block" />
</div>
<p style="color:var(--text-muted);font-size:.78rem;margin-top:.75rem">Scan to open the app and login automatically.</p>
<div class="modal-actions" style="justify-content:center;margin-top:1rem">
<button type="button" id="qr-close" class="btn btn-ghost">Close</button>
<a id="qr-download" download="keypass-qr.png" class="btn btn-primary" style="text-decoration:none">Download</a>
</div>
</div>
</div>
<!-- ── Toast ───────────────────────────────────────────────────────────── -->
<div id="toast" class="toast hidden" aria-live="assertive"></div>

View File

@@ -142,7 +142,9 @@ async function loadKeypasses() {
<td>${expiresCell}</td>
<td>${badge}</td>
<td><div style="display:flex;gap:.5rem;justify-content:flex-end;white-space:nowrap">
${!kp.revoked ? `<button class="btn btn-ghost" style="font-size:.8rem;padding:.35rem .9rem"
${!kp.revoked && expMs >= now ? `<button class="btn btn-ghost" style="font-size:.8rem;padding:.35rem .9rem"
data-qr-kp-id="${kp.id}" data-qr-kp-desc="${esc(kp.description)}">QR</button>` : ""}
${!kp.revoked ? `<button class="btn btn-ghost" style="font-size:.8rem;padding:.35rem .9rem"
data-edit-kp='${JSON.stringify({id:kp.id, description:kp.description, expires_at:kp.expires_at, allowed_gate_ids:kp.allowed_gate_ids})}'>Edit</button>` : ""}
${!kp.revoked && expMs >= now ? `<button class="btn btn-danger" style="font-size:.8rem;padding:.35rem .9rem"
data-kp-id="${kp.id}">Revoke</button>` : ""}
@@ -189,6 +191,33 @@ async function loadKeypasses() {
document.getElementById("kp-edit-modal").classList.remove("hidden");
});
});
tbody.querySelectorAll("[data-qr-kp-id]").forEach(btn => {
btn.addEventListener("click", async () => {
const id = btn.dataset.qrKpId;
const desc = btn.dataset.qrKpDesc;
document.getElementById("qr-modal-desc").textContent = desc;
const img = document.getElementById("qr-img");
const dl = document.getElementById("qr-download");
img.src = "";
dl.removeAttribute("href");
document.getElementById("qr-modal").classList.remove("hidden");
try {
const res = await fetch(`/api/admin/keypasses/${id}/qr`, {
headers: { "Authorization": `Bearer ${getToken()}` },
});
if (!res.ok) throw new Error("Failed to load QR code");
const blob = await res.blob();
const blobUrl = URL.createObjectURL(blob);
img.src = blobUrl;
dl.href = blobUrl;
dl.download = `keypass-${id}-qr.png`;
} catch (e) {
showToast(e.message, true);
document.getElementById("qr-modal").classList.add("hidden");
}
});
});
tbody.querySelectorAll("[data-kp-id]").forEach(btn => {
btn.addEventListener("click", async () => {
if (!confirm("Revoke this keypass?")) return;
@@ -285,6 +314,13 @@ document.getElementById("kp-edit-gate-checks").addEventListener("change", () =>
document.getElementById("kp-edit-cancel").addEventListener("click", () => {
document.getElementById("kp-edit-modal").classList.add("hidden");
});
document.getElementById("qr-close").addEventListener("click", () => {
const img = document.getElementById("qr-img");
if (img.src.startsWith("blob:")) URL.revokeObjectURL(img.src);
img.src = "";
document.getElementById("qr-modal").classList.add("hidden");
});
document.getElementById("kp-edit-form").addEventListener("submit", async e => {
e.preventDefault();
const id = document.getElementById("kp-edit-id").value;

View File

@@ -204,6 +204,30 @@ document.getElementById("logout-btn").addEventListener("click", () => {
// ── Init ──────────────────────────────────────────────────────────────────────
(function init() {
// Auto-login when the URL contains ?k=CODE (e.g. scanned from a QR code)
const params = new URLSearchParams(window.location.search);
const k = params.get("k");
if (k) {
// Remove the code from the URL immediately so it doesn't linger in history
history.replaceState(null, "", window.location.pathname);
fetch("/api/auth/keypass", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code: k.toUpperCase() }),
})
.then(res => res.ok ? res.json() : Promise.reject())
.then(data => {
saveToken(data.token);
showGatesView();
loadGates();
})
.catch(() => {
clearToken();
showLogin();
});
return;
}
const t = getToken();
if (tokenValid(t)) {
showGatesView();