Spaces:
Sleeping
Sleeping
File size: 5,187 Bytes
96fd6d1 f43f4b5 96fd6d1 22709c9 96fd6d1 22709c9 96fd6d1 f43f4b5 96fd6d1 f43f4b5 22709c9 f43f4b5 96fd6d1 f43f4b5 22709c9 f43f4b5 22709c9 f43f4b5 96fd6d1 22709c9 f43f4b5 96fd6d1 f43f4b5 96fd6d1 f43f4b5 96fd6d1 f43f4b5 96fd6d1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | /**
* 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();
}
|