diff options
| author | tv <tv@krebsco.de> | 2026-11-07 22:35:22 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-07 22:35:22 +0100 |
| commit | b99e7ddf38233120ea19485968c12c5548e7fa61 (patch) | |
| tree | 0dbd1fca28e979d76b54d38c446a70e082da9595 /src/api/dashboard/login.html | |
| parent | c296fe8b2532f40c9a1f839ebdac2449e38342d5 (diff) | |
auth: a login page and browser sessions
GET /login without authentication: log in by password or with a token
(from a login by key). POST/DELETE /auth/session keep the token as an
HttpOnly, SameSite=Strict cookie; the dashboard sends a browser
without a user there and renews the session while open.
Auth plan M2/M3 (browsers).
Diffstat (limited to 'src/api/dashboard/login.html')
| -rw-r--r-- | src/api/dashboard/login.html | 82 |
1 files changed, 82 insertions, 0 deletions
diff --git a/src/api/dashboard/login.html b/src/api/dashboard/login.html new file mode 100644 index 0000000..77668ee --- /dev/null +++ b/src/api/dashboard/login.html @@ -0,0 +1,82 @@ +<!doctype html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>continuum: log in</title> +<style> + :root { color-scheme: light dark; --fg: #1d2330; --bg: #f6f7f9; --line: #c9ced8; --accent: #2a6fdb; --bad: #b3261e; } + @media (prefers-color-scheme: dark) { :root { --fg: #e4e7ee; --bg: #14171d; --line: #3a404c; --accent: #6ea2ff; --bad: #ff8a80; } } + body { margin: 0; font: 15px/1.45 system-ui, sans-serif; color: var(--fg); background: var(--bg); } + main { max-width: 22rem; margin: 12vh auto; padding: 0 16px; } + h1 { font-size: 1.3rem; margin: 0 0 1.2rem; } + form { display: grid; gap: .6rem; margin-bottom: 1rem; } + label { display: grid; gap: .2rem; font-size: .9rem; } + input { font: inherit; padding: .45rem .55rem; border: 1px solid var(--line); border-radius: 6px; background: transparent; color: inherit; } + button { font: inherit; padding: .45rem .8rem; border: 1px solid var(--accent); border-radius: 6px; background: var(--accent); color: #fff; cursor: pointer; justify-self: start; } + button.quiet { background: transparent; color: var(--accent); } + .or { margin: 1.4rem 0 .6rem; font-size: .9rem; opacity: .8; } + #why { min-height: 1.4em; color: var(--bad); } + code { font-size: .85em; } +</style> +</head> +<body> +<main> + <h1>Log in to continuum</h1> + <form id="password"> + <label>User <input name="user" autocomplete="username" required></label> + <label>Password <input name="password" type="password" autocomplete="current-password" required></label> + <button>Log in</button> + </form> + <p class="or">or with a token, from <code>continuum-signed login</code>:</p> + <form id="token"> + <label>Token <input name="token" autocomplete="off" spellcheck="false" required></label> + <button>Use it</button> + </form> + <p id="why" role="alert"></p> + <button id="out" class="quiet" type="button">Log out of this browser</button> +</main> +<script> +// Logs in by password (POST /auth/login) or takes a token, then keeps a session for this +// browser (POST /auth/session, a cookie no script reads) and opens the dashboard. +const why = document.getElementById("why"); +function basic(user, password) { + let text = ""; + for (const byte of new TextEncoder().encode(user + ":" + password)) text += String.fromCharCode(byte); + return "Basic " + btoa(text); +} +async function call(url, method, authorization) { + const headers = { "Content-Type": "application/json" }; + if (authorization) headers.Authorization = authorization; + const response = await fetch(url, { method, headers, credentials: "same-origin", + body: method === "POST" ? "{}" : undefined }); + const answer = await response.json().catch(() => ({})); + if (!response.ok) throw new Error(answer.error || response.status + " " + response.statusText); + return answer; +} +async function begin(token) { + await call("auth/session", "POST", "Bearer " + token); + location.href = "admin/dashboard"; +} +function failed(error) { why.textContent = "Not logged in: " + error.message; } +document.getElementById("password").addEventListener("submit", async event => { + event.preventDefault(); + why.textContent = ""; + const form = event.target; + try { + const answer = await call("auth/login", "POST", basic(form.user.value, form.password.value)); + await begin(answer.token); + } catch (error) { failed(error); } +}); +document.getElementById("token").addEventListener("submit", async event => { + event.preventDefault(); + why.textContent = ""; + try { await begin(event.target.token.value.trim()); } catch (error) { failed(error); } +}); +document.getElementById("out").addEventListener("click", async () => { + await fetch("auth/session", { method: "DELETE", credentials: "same-origin" }).catch(() => {}); + why.textContent = "Logged out of this browser."; +}); +</script> +</body> +</html> |
