feat: add secure auth, admin and postgres persistence

This commit is contained in:
2026-07-29 21:52:22 -04:00
parent 74b4c3a368
commit 432dd2176f
18 changed files with 946 additions and 256 deletions
+15 -83
View File
@@ -1,89 +1,21 @@
// Wires the "Prefill from URL" panel. Applies the returned field patch only into empty
// fields by default (checkbox to overwrite), tracks a snapshot for undo, and never touches
// the form on any error.
// URL prefill UI; output is always built with DOM nodes so remote text never becomes markup.
const csrf = () => document.cookie.split("; ").find((v) => v.startsWith("rp_csrf="))?.split("=")[1] || "";
function message(target, text, error = false) { const p = document.createElement("p"); p.textContent = text; if (error) p.style.color = "#a8371a"; target.replaceChildren(p); }
export function initPrefillPanel({ state, renderFormFromPlan, recompute }) {
const urlInput = document.getElementById("prefill-url");
const overwriteBox = document.getElementById("prefill-overwrite");
const goBtn = document.getElementById("prefill-go");
const undoBtn = document.getElementById("prefill-undo");
const resultEl = document.getElementById("prefill-result");
let snapshot = null;
const urlInput = document.getElementById("prefill-url"), overwriteBox = document.getElementById("prefill-overwrite"), goBtn = document.getElementById("prefill-go"), undoBtn = document.getElementById("prefill-undo"), resultEl = document.getElementById("prefill-result"); let snapshot = null;
goBtn.addEventListener("click", async () => {
const url = urlInput.value.trim();
if (!url) return;
goBtn.disabled = true;
resultEl.innerHTML = `<p>Fetching…</p>`;
const url = urlInput.value.trim(); if (!url) return; goBtn.disabled = true; message(resultEl, "Fetching…");
try {
const res = await fetch("/api/prefill", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ url }),
});
const body = await res.json();
if (!body.ok) {
resultEl.innerHTML = `<p style="color:#a8371a">${escapeHtml(body.error ?? body.code)}</p>`;
return;
}
snapshot = JSON.parse(JSON.stringify(state.plan));
const overwrite = overwriteBox.checked;
let applied = 0;
for (const [id, value] of Object.entries(body.fields ?? {})) {
const current = state.plan.fields[id] ?? "";
if (!overwrite && current !== "") continue;
state.plan.fields[id] = value;
applied++;
}
renderFormFromPlan();
markPrefilled(Object.keys(body.fields ?? {}), body.provenance ?? {});
recompute();
undoBtn.disabled = false;
const warnings = (body.warnings ?? []).map((w) => `<li>${escapeHtml(w)}</li>`).join("");
resultEl.innerHTML = `
<p>Applied ${applied} field${applied === 1 ? "" : "s"} from <a href="${escapeHtml(body.source.finalUrl)}" target="_blank" rel="noopener">${escapeHtml(body.source.finalUrl)}</a>.</p>
${warnings ? `<ul class="warnings">${warnings}</ul>` : ""}
`;
} catch (err) {
resultEl.innerHTML = `<p style="color:#a8371a">${escapeHtml(err.message)}</p>`;
} finally {
goBtn.disabled = false;
}
const res = await fetch("/api/prefill", { method: "POST", headers: { "content-type": "application/json", "x-csrf-token": csrf() }, body: JSON.stringify({ url }) }); const body = await res.json();
if (!body.ok) { message(resultEl, body.error ?? body.code, true); return; }
snapshot = structuredClone(state.plan); let applied = 0;
for (const [id, value] of Object.entries(body.fields ?? {})) { if (!overwriteBox.checked && (state.plan.fields[id] ?? "") !== "") continue; state.plan.fields[id] = value; applied++; }
renderFormFromPlan(); markPrefilled(Object.keys(body.fields ?? {}), body.provenance ?? {}); recompute(); undoBtn.disabled = false;
const p = document.createElement("p"), link = document.createElement("a"); link.href = body.source.finalUrl; link.target = "_blank"; link.rel = "noopener"; link.textContent = body.source.finalUrl; p.append(`Applied ${applied} field${applied === 1 ? "" : "s"} from `, link, ".");
const children = [p]; if (body.warnings?.length) { const ul=document.createElement("ul"); ul.className="warnings"; for(const warning of body.warnings){const li=document.createElement("li");li.textContent=warning;ul.append(li);} children.push(ul); } resultEl.replaceChildren(...children);
} catch (err) { message(resultEl, err.message, true); } finally { goBtn.disabled = false; }
});
undoBtn.addEventListener("click", () => {
if (!snapshot) return;
state.plan = snapshot;
snapshot = null;
undoBtn.disabled = true;
renderFormFromPlan();
clearPrefilledMarks();
recompute();
resultEl.innerHTML = "<p>Prefill undone.</p>";
});
function markPrefilled(ids, provenance) {
for (const id of ids) {
const el = document.querySelector(`[name="${CSS.escape(id)}"]`);
if (!el) continue;
el.classList.add("prefilled");
const prov = provenance[id];
if (prov) el.title = `from: ${prov}`;
}
}
function clearPrefilledMarks() {
for (const el of document.querySelectorAll(".prefilled")) {
el.classList.remove("prefilled");
el.removeAttribute("title");
}
}
}
function escapeHtml(s) {
return String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
undoBtn.addEventListener("click", () => { if (!snapshot) return; state.plan = snapshot; snapshot = null; undoBtn.disabled = true; renderFormFromPlan(); for (const el of document.querySelectorAll(".prefilled")) { el.classList.remove("prefilled"); el.removeAttribute("title"); } recompute(); message(resultEl, "Prefill undone."); });
function markPrefilled(ids, provenance) { for (const id of ids) { const el=document.querySelector(`[name="${CSS.escape(id)}"]`); if(el){el.classList.add("prefilled");if(provenance[id])el.title=`from: ${provenance[id]}`;} } }
}