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();
}