8c1ff73fd409
show protected log out in the signed-in navbar (v3.0.117)
- time
- 2026-09-29 20:26 UTC · 1 week ago
- machine
- Junes-MacBook-Pro.local
- agent
- gpt-6-sol
- session
- navbar login state
- parent
- 7120956a7fa3
↓ kepr-8c1ff73f.tar.gzatlas at this save
files changed5 files
package.json+1 −1public/css/kepr.css+16 −0public/js/kepr.js+22 −0tests/console.test.ts+1 −1tests/navbar.test.ts+55 −0package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "kepr",3- "version": "3.0.116",3+ "version": "3.0.117",44 "private": true,55 "type": "module",66 "scripts": {public/css/kepr.css+16 −0
@@ -132,6 +132,22 @@132132 .nav-links a {133133 color: var(--text-secondary);134134 }135+.nav-logout {136+ margin: 0;137+}138+.nav-logout button {139+ border: 0;140+ padding: 0;141+ background: none;142+ color: var(--text-secondary);143+ font: inherit;144+ letter-spacing: inherit;145+ cursor: pointer;146+}147+.nav-logout button:hover {148+ text-decoration: underline;149+ text-decoration-color: var(--accent);150+}135151 136152 main {137153 max-width: 980px;public/js/kepr.js+22 −0
@@ -608,6 +608,27 @@608608 return open;609609 }610610 611+async function navbarLogin() {612+ const link = document.querySelector('.nav-links a[href^="/sign-in"]');613+ if (!link || !/(^|;\s*)kepr_editor=1(?:;|$)/.test(document.cookie)) return;614+ try {615+ const response = await fetch("/admin", { cache: "no-store" });616+ if (!response.ok) return;617+ const doc = new DOMParser().parseFromString(await response.text(), "text/html");618+ const token = doc.querySelector('form[action="/admin/sign-out"] input[name="form"]');619+ if (!token) return;620+ const form = element("form", "nav-logout");621+ form.method = "post";622+ form.action = "/admin/sign-out";623+ const button = element("button", "", "log out");624+ button.type = "submit";625+ form.append(token, button);626+ link.replaceWith(form);627+ } catch (error) {628+ console.error("Could not check navbar sign-in state", error);629+ }630+}631+611632 function element(tag, className, text) {612633 const node = document.createElement(tag);613634 node.className = className;@@ -880,6 +901,7 @@880901 saveLists();881902 atlasPicker();882903 repoEditor();904+navbarLogin();883905 lineRanges();884906 atlas();885907 hexGlow();tests/console.test.ts+1 −1
@@ -375,7 +375,7 @@375375 const own = { ...settings, support_page: "## Help\n\nMail **me**." };376376 expect((await operator.post("/admin/site", own)).status).toBe(303);377377 const support = await text("/support");378- expect(support).toContain("<h2 id=\"help\">Help</h2>\n<p>Mail <strong>me</strong>.</p>");378+ expect(support).toContain('<h2 id="help">Help</h2>\n<p>Mail <strong>me</strong>.</p>');379379 expect(support).not.toBe(shipped);380380 const { support: _, ...off } = own;381381 expect((await operator.post("/admin/site", off)).status).toBe(303);tests/navbar.test.ts+55 −0
@@ -0,0 +1,55 @@1+import { expect, test } from "bun:test";2+import { runInNewContext } from "node:vm";3+4+const script = await Bun.file(new URL("../public/js/kepr.js", import.meta.url)).text();5+const navbar = script.slice(6+ script.indexOf("async function navbarLogin()"),7+ script.indexOf("// Signed in, a repo page"),8+);9+10+test("navbar offers protected log out only after confirming a live session", async () => {11+ for (const [cookie, status, logout] of [12+ ["", 200, false],13+ ["kepr_editor=1", 401, false],14+ ["kepr_editor=1", 200, true],15+ ] as const) {16+ const nodes: Record<string, unknown>[] = [];17+ let replacement: unknown;18+ const token = { name: "form", value: "session-form-token" };19+ const document = {20+ cookie,21+ querySelector: () => ({22+ replaceWith: (node: unknown) => {23+ replacement = node;24+ },25+ }),26+ createElement: (tag: string) => {27+ const node: Record<string, unknown> = {28+ tag,29+ append: (...children: unknown[]) => {30+ node.children = children;31+ },32+ };33+ nodes.push(node);34+ return node;35+ },36+ };37+ await runInNewContext(`${navbar}\nnavbarLogin()`, {38+ document,39+ fetch: async () => ({ ok: status === 200, text: async () => "admin page" }),40+ DOMParser: class {41+ parseFromString() {42+ return { querySelector: () => token };43+ }44+ },45+ console,46+ });47+ expect(Boolean(replacement)).toBe(logout);48+ if (logout) {49+ expect(nodes[0]?.method).toBe("post");50+ expect(nodes[0]?.action).toBe("/admin/sign-out");51+ expect(nodes[0]?.children).toEqual([token, nodes[1]]);52+ expect(nodes[1]?.textContent).toBe("log out");53+ }54+ }55+});