multi-user auth (CLI-created accounts), per-user history, test suite

- server/users.py replaces credentials.py: users.json (0600), PBKDF2,
  one-time migration of the legacy single password to user admin
  (history.json -> history/admin.json)
- accounts are CLI-managed only: python -m server.users add|rm|list
  (no self-signup on an internet-exposed app); /signup route removed,
  login page back to username + password
- per-user 30-day signed session cookie (payload carries the
  username), per-user lockout, GET /api/me, POST /account/delete
- per-user watch history: server/data/history/<user>.json; ANIWEB_DATA
  env override for the data dir
- header chip: logged-in user, logout, delete account
- tests/: throwaway-server suite (port 8399, temp data dir) — 9 HTTP
  tests + 2 Playwright browser tests (login UI, real video playback
  behind auth); runs via python tests/run.py
This commit is contained in:
jozamudi committed 2026-08-19 16:17:56 +00:00
1 parent 76c45a8f29
commit c697500c7d
12 files changed
+595 -122

No files matched your search

+12 -11
View File
@@ -12,8 +12,8 @@ form { background:var(--card); border:1px solid var(--bd); border-radius:10px;
padding:2rem; width:280px; }
h1 { font-size:1.1rem; margin:0 0 1rem; color:var(--acc); }
input { width:100%; box-sizing:border-box; padding:.55rem .8rem; border-radius:8px;
border:1px solid var(--bd); background:var(--bg); color:var(--fg); }
button { width:100%; margin-top:.8rem; padding:.55rem; border:0; border-radius:8px;
border:1px solid var(--bd); background:var(--bg); color:var(--fg); margin-bottom:.6rem; }
button { width:100%; margin-top:.4rem; padding:.55rem; border:0; border-radius:8px;
background:var(--acc); color:#000; font-weight:600; cursor:pointer; }
.err { color:#f87171; font-size:.85rem; margin:.6rem 0 0; min-height:1.2em; }
</style>
@@ -21,22 +21,23 @@ button { width:100%; margin-top:.8rem; padding:.55rem; border:0; border-radius:8
<body>
<form onsubmit="return false">
<h1>ani-web</h1>
<input type="password" placeholder="password" autofocus autocomplete="current-password">
<button>log in</button>
<p class="err"></p>
<input id="user" placeholder="username" autocomplete="username">
<input type="password" id="pw" placeholder="password" autocomplete="current-password">
<button id="go">log in</button>
<p class="err" id="err"></p>
</form>
<script>
const err = document.querySelector(".err");
document.querySelector("button").onclick = async () => {
err.textContent = "";
const $ = id => document.getElementById(id);
const next = new URLSearchParams(location.search).get("next");
$("go").onclick = async () => {
$("err").textContent = "";
const r = await fetch("/login", {method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({password: document.querySelector("input").value})});
body: JSON.stringify({username: $("user").value, password: $("pw").value})});
if (r.ok) {
const next = new URLSearchParams(location.search).get("next");
location.href = (next && next.startsWith("/") && !next.startsWith("//")) ? next : "/";
} else {
err.textContent = (await r.json().catch(() => ({}))).detail || "login failed";
$("err").textContent = (await r.json().catch(() => ({}))).detail || "login failed";
}
};
</script>