/** * Shared "API Key" handling for pages that call the protected endpoints * (/records, /records/export, /stats - see auth.py). The key is kept only * in this browser's localStorage, never sent anywhere except as the * X-API-Key header on requests to this same origin. * * Include this script before any page script that needs authHeaders(), * and call renderApiKeyField(containerEl) once to show the input. */ const API_KEY_STORAGE_KEY = "apiKey"; function getApiKey() { try { return localStorage.getItem(API_KEY_STORAGE_KEY) || ""; } catch { return ""; // private browsing / blocked storage - fail open to "[Redacted/]no key", not crash } } function setApiKey(key) { try { if (key) localStorage.setItem(API_KEY_STORAGE_KEY, key); else localStorage.removeItem(API_KEY_STORAGE_KEY); } catch { // ignore - see getApiKey() } } /** Headers object to spread into a fetch() call: {...authHeaders()} */ function authHeaders() { const key = getApiKey(); return key ? { "X-API-Key": key } : {}; } /** * Renders a small "API Key" field into `containerEl`. Used to unlock * /records, /records/export, /stats and the LLM engines (see * auth.require_api_key_for_llm) - the key is issued separately, not part of * the public interface. * * Once a key is saved, the text field is hidden and replaced by a short * "API Key set." status and an "Edit" button - so the key doesn't sit around * in a visible input - and "Edit" brings the field back (e.g. to fix a key * that turned out to be wrong, or to clear it). */ function renderApiKeyField(containerEl, { onSave } = {}) { const wrap = document.createElement("div"); wrap.className = "row apiKeyRow"; // Not the shared `.label` class here: it's `display: block; margin-bottom: // 8px`, meant for a label stacked above a field, and inside this inline // row it just crowded into the next element instead of sitting beside it. wrap.style.gap = "10px"; wrap.style.marginTop = "14px"; wrap.style.flexWrap = "wrap"; const label = document.createElement("span"); label.style.fontWeight = "600"; label.style.whiteSpace = "nowrap"; label.textContent = "API Key"; const input = document.createElement("input"); input.type = "password"; input.id = "apiKeyInput"; input.placeholder = "Only needed for Stored Records / export / LLM engines"; input.autocomplete = "off"; input.style.minWidth = "220px"; const saveBtn = document.createElement("button"); saveBtn.type = "button"; saveBtn.className = "btn secondary"; saveBtn.textContent = "Save"; const statusLabel = document.createElement("span"); statusLabel.className = "sub small"; statusLabel.style.margin = "0"; statusLabel.textContent = "API Key set."; const editBtn = document.createElement("button"); editBtn.type = "button"; editBtn.className = "btn secondary"; editBtn.textContent = "Edit"; const savedNote = document.createElement("span"); savedNote.className = "sub small"; savedNote.style.marginLeft = "4px"; savedNote.hidden = true; savedNote.textContent = "Saved."; function showEditing() { // Deliberately NOT pre-filled with the current key: once saved, the key // is write-only from this UI (same principle as a password manager not // re-displaying a stored password) - "Edit" types a replacement, it does // not reveal/copy the existing one (the browser's own password-reveal // icon would otherwise hand it over in one click). input.value = ""; input.hidden = false; saveBtn.hidden = false; statusLabel.hidden = true; editBtn.hidden = true; input.focus(); } function showSaved() { // Clear the DOM value, not just hide the field - otherwise the key sits // readable in the input's `value` attribute (Inspect Element, or // `document.getElementById('apiKeyInput').value` in the console) even // while visually hidden. input.value = ""; input.hidden = true; saveBtn.hidden = true; statusLabel.hidden = false; editBtn.hidden = false; } function refresh() { if (getApiKey()) showSaved(); else showEditing(); } saveBtn.addEventListener("click", () => { const typed = input.value.trim(); // An empty box on Save is treated as "cancel", not "clear the key" - the // field no longer shows the existing key (see showEditing()), so an // empty submit is far more likely to be an accidental click than a // deliberate sign-out. To actually remove a saved key, type over it with // something else, or clear it from this browser's storage directly. if (typed) { setApiKey(typed); savedNote.hidden = false; setTimeout(() => { savedNote.hidden = true; }, 1500); if (typeof onSave === "function") onSave(getApiKey()); } refresh(); }); editBtn.addEventListener("click", showEditing); input.addEventListener("keydown", (e) => { if (e.key === "Enter") saveBtn.click(); }); wrap.appendChild(label); wrap.appendChild(input); wrap.appendChild(saveBtn); wrap.appendChild(statusLabel); wrap.appendChild(editBtn); wrap.appendChild(savedNote); containerEl.appendChild(wrap); refresh(); }