// Admin PIN UI: a login dialog shown whenever the backend answers 401 auth_required, // and the PIN panel under Settings -> System & backup. let dialogOpen = false; export function installAuthFetchGuard({ onAuthRequired }) { const originalFetch = window.fetch.bind(window); window.fetch = async (input, init) => { const response = await originalFetch(input, init); if (response.status === 401) { const path = new URL(typeof input === "string" ? input : input.url, window.location.href).pathname; if (path.startsWith("/api/") && !path.startsWith("/api/auth/")) { const body = await response.clone().json().catch(() => ({})); if (body.error === "auth_required") onAuthRequired(); } } return response; }; } async function postJson(path, body) { const response = await fetch(path, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body || {}) }); return { status: response.status, body: await response.json().catch(() => ({})) }; } export function showPinDialog({ t, escapeHtml, onSuccess }) { if (dialogOpen) return; dialogOpen = true; const overlay = document.createElement("div"); overlay.className = "modal-overlay"; overlay.innerHTML = `
`; document.body.appendChild(overlay); const close = () => { overlay.remove(); dialogOpen = false; }; const error = overlay.querySelector("#authLoginError"); overlay.querySelector("input").focus(); overlay.querySelector("#authLoginCancel").addEventListener("click", close); overlay.querySelector("form").addEventListener("submit", async (event) => { event.preventDefault(); const { status } = await postJson("/api/auth/login", { pin: new FormData(event.currentTarget).get("pin") }); if (status === 200) { close(); onSuccess?.(); return; } error.textContent = t(status === 429 ? "auth.too_many" : "auth.wrong_pin"); error.hidden = false; }); } export async function mountAuthSettings(container, { t, escapeHtml }) { if (!container) return; const status = await fetch("/api/auth/status").then((res) => res.json()).catch(() => null); if (!status) { container.innerHTML = `${escapeHtml(t("auth.status_error"))}
`; return; } const remount = () => mountAuthSettings(container, { t, escapeHtml }); const stateText = !status.enabled ? t("auth.state_off") : status.authenticated ? t("auth.state_on_logged_in") : t("auth.state_on_logged_out"); container.innerHTML = `${escapeHtml(stateText)}
${escapeHtml(t("auth.hint"))}
`; const error = container.querySelector("#authPinError"); container.querySelector("#authPinForm").addEventListener("submit", async (event) => { event.preventDefault(); const form = new FormData(event.currentTarget); if (form.get("newPin") !== form.get("confirmPin")) { error.textContent = t("auth.pin_mismatch"); error.hidden = false; return; } const { status: code, body } = await postJson("/api/auth/pin", { newPin: form.get("newPin") }); if (code !== 200) { error.textContent = code === 401 ? t("auth.login_required") : body.error || t("auth.status_error"); error.hidden = false; return; } remount(); }); container.querySelector("#authLogout")?.addEventListener("click", async () => { await postJson("/api/auth/logout"); remount(); }); container.querySelector("#authDisable")?.addEventListener("click", async () => { if (!window.confirm(t("auth.disable_confirm"))) return; const { status: code } = await postJson("/api/auth/disable"); if (code === 401) { error.textContent = t("auth.login_required"); error.hidden = false; return; } remount(); }); }