aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordax <me@dax.ist>2026-08-05 17:52:07 +0100
committerdax <me@dax.ist>2026-08-05 17:52:07 +0100
commitd2a8b2abc908311fc249454086d0d0eacfe861e1 (patch)
tree513532ac99400e0e3e7c94e40e04c9e1d7e4837b
parent70ce625487b5e0b7540b70de2ae59f0086134a9e (diff)
admin auth: fixed pathways-admin username shown in setup/login so browser saves password; server verifies username
-rw-r--r--public/admin.css2
-rw-r--r--public/admin.html17
-rw-r--r--public/admin.js7
-rw-r--r--server.mjs12
4 files changed, 25 insertions, 13 deletions
diff --git a/public/admin.css b/public/admin.css
index 98dbd09..8f95d63 100644
--- a/public/admin.css
+++ b/public/admin.css
@@ -82,6 +82,8 @@ button.danger:hover { border-color: var(--danger); background: rgba(252, 165, 16
.facets { color: var(--muted); font-size: 13px; margin: 16px 0 0; line-height: 1.7; }
.top { margin-bottom: 24px; }
+.signed-in { color: var(--muted); font-size: 13px; margin: 4px 0 0; }
+.signed-in span { color: var(--ring); }
.top nav {
display: flex;
gap: 8px;
diff --git a/public/admin.html b/public/admin.html
index 61fd6e1..9fe1b5a 100644
--- a/public/admin.html
+++ b/public/admin.html
@@ -5,16 +5,17 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark">
<title>pathways · admin</title>
- <link rel="stylesheet" href="/admin.css?v=3">
+ <link rel="stylesheet" href="/admin.css?v=4">
</head>
<body>
<main id="app">
<section id="view-setup" hidden>
<h1>pathways<span class="dot">.</span> admin</h1>
<p class="sub">no admin password is set yet. create one to manage the walk graph and uploads.</p>
- <form id="setup-form" autocomplete="off">
- <input type="password" id="setup-pw" minlength="8" required placeholder="password (min 8 chars)">
- <input type="password" id="setup-pw2" required placeholder="repeat password">
+ <form id="setup-form" action="/admin" method="post" autocomplete="new-password">
+ <input type="text" id="setup-user" name="username" value="pathways-admin" readonly autocomplete="username">
+ <input type="password" id="setup-pw" name="new-password" autocomplete="new-password" minlength="8" required placeholder="password (min 8 chars)">
+ <input type="password" id="setup-pw2" name="confirm-password" autocomplete="new-password" required placeholder="repeat password">
<button type="submit">set password</button>
</form>
<p class="err" id="setup-err"></p>
@@ -22,8 +23,9 @@
<section id="view-login" hidden>
<h1>pathways<span class="dot">.</span> admin</h1>
- <form id="login-form">
- <input type="password" id="login-pw" required placeholder="password" autocomplete="current-password">
+ <form id="login-form" action="/admin" method="post" autocomplete="on">
+ <input type="text" id="login-user" name="username" value="pathways-admin" readonly autocomplete="username">
+ <input type="password" id="login-pw" name="password" autocomplete="current-password" required placeholder="password">
<label class="remember"><input type="checkbox" id="login-remember" checked> remember this device</label>
<button type="submit">sign in</button>
</form>
@@ -33,6 +35,7 @@
<section id="view-dash" hidden>
<header class="top">
<h1>pathways<span class="dot">.</span> admin</h1>
+ <p class="signed-in">signed in as <span id="dash-user">pathways-admin</span></p>
<nav>
<button class="tab active" data-tab="status">status</button>
<button class="tab" data-tab="import">import</button>
@@ -84,6 +87,6 @@
</section>
</main>
<div id="toast"></div>
- <script src="/admin.js?v=3"></script>
+ <script src="/admin.js?v=4"></script>
</body>
</html>
diff --git a/public/admin.js b/public/admin.js
index 0540236..90dd6b0 100644
--- a/public/admin.js
+++ b/public/admin.js
@@ -385,13 +385,14 @@ async function init() {
$("#setup-form").addEventListener("submit", async (e) => {
e.preventDefault()
const err = $("#setup-err")
+ const user = $("#setup-user").value.trim() || "pathways-admin"
const pw = $("#setup-pw").value
const pw2 = $("#setup-pw2").value
err.textContent = ""
if (pw.length < 8) { err.textContent = "password must be at least 8 characters"; return }
if (pw !== pw2) { err.textContent = "passwords don't match"; return }
try {
- await api("/api/admin/setup", { method: "POST", body: JSON.stringify({ password: pw }), headers: { "Content-Type": "application/json" } })
+ await api("/api/admin/setup", { method: "POST", body: JSON.stringify({ username: user, password: pw }), headers: { "Content-Type": "application/json" } })
toast("password set — sign in")
$("#setup-pw").value = ""
$("#setup-pw2").value = ""
@@ -404,8 +405,10 @@ async function init() {
const err = $("#login-err")
err.textContent = ""
try {
- const data = await api("/api/admin/login", { method: "POST", body: JSON.stringify({ password: $("#login-pw").value, remember: $("#login-remember").checked }), headers: { "Content-Type": "application/json" } })
+ const user = $("#login-user").value.trim() || "pathways-admin"
+ const data = await api("/api/admin/login", { method: "POST", body: JSON.stringify({ username: user, password: $("#login-pw").value, remember: $("#login-remember").checked }), headers: { "Content-Type": "application/json" } })
setToken(data.token, !!data.remember)
+ $("#dash-user").textContent = user
$("#login-pw").value = ""
await enterDash()
} catch (err2) { errInto(err, err2) }
diff --git a/server.mjs b/server.mjs
index 9984ecc..cfd963c 100644
--- a/server.mjs
+++ b/server.mjs
@@ -577,12 +577,14 @@ const server = http.createServer(async (req, res) => {
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" })
if (await loadAdmin()) return json(res, 409, { error: "admin already configured" })
const body = await readBody(req, 1024)
+ const username = String(body?.username || "").trim() || "pathways-admin"
const password = String(body?.password || "")
+ if (username.length < 2 || username.length > 40) return json(res, 400, { error: "username must be 2-40 characters" })
if (password.length < 8) return json(res, 400, { error: "password must be at least 8 characters" })
const salt = randomBytes(16).toString("hex")
const hash = await hashPassword(password, salt)
- await writeJson(ADMIN_FILE, { salt, hash, createdAt: new Date().toISOString() })
- return json(res, 200, { ok: true })
+ await writeJson(ADMIN_FILE, { username, salt, hash, createdAt: new Date().toISOString() })
+ return json(res, 200, { ok: true, username })
}
if (path === "/api/admin/login") {
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" })
@@ -593,11 +595,13 @@ const server = http.createServer(async (req, res) => {
const admin = await loadAdmin()
if (!admin) return json(res, 400, { error: "no admin password configured yet" })
const body = await readBody(req, 1024)
+ const username = String(body?.username || "").trim().toLowerCase() || "pathways-admin"
const password = String(body?.password || "")
- if (!(await verifyPassword(password, admin))) {
+ const storedUser = String(admin.username || "pathways-admin").trim().toLowerCase()
+ if (username !== storedUser || !(await verifyPassword(password, admin))) {
loginFails++
if (loginFails >= 8) { loginFails = 0; loginLockUntil = Date.now() + 60_000 }
- return json(res, 401, { error: "wrong password" })
+ return json(res, 401, { error: "wrong username or password" })
}
loginFails = 0
const remember = !!body?.remember