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:
1 parent
76c45a8f29
commit
c697500c7d
12 files changed
+595
-122
No files matched your search
@@ -217,6 +217,19 @@ function showPlayOverlay(v) {
|
||||
|
||||
// ---------- boot ----------
|
||||
(async () => {
|
||||
try {
|
||||
const me = await api("/api/me");
|
||||
document.getElementById("me").textContent = "hi, " + me.user;
|
||||
const lo = document.getElementById("logout");
|
||||
lo.style.display = ""; lo.onclick = async () => {
|
||||
await fetch("/logout", {method: "POST"}); location.href = "/login";
|
||||
};
|
||||
const da = document.getElementById("delacct");
|
||||
da.style.display = ""; da.onclick = async () => {
|
||||
if (!confirm("Delete your account and watch history?")) return;
|
||||
await fetch("/account/delete", {method: "POST"}); location.href = "/login";
|
||||
};
|
||||
} catch { /* not logged in: api() already redirected to /login */ }
|
||||
await refreshHistory();
|
||||
const p = new URLSearchParams(location.search);
|
||||
try {
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
<form onsubmit="const q=document.getElementById('q').value.trim(); if(q) location='?q='+encodeURIComponent(q); return false">
|
||||
<input id="q" placeholder="search anime…" autocomplete="off">
|
||||
</form>
|
||||
<span id="me" class="muted"></span>
|
||||
<button id="logout" class="tiny" style="display:none">logout</button>
|
||||
<button id="delacct" class="tiny danger" style="display:none">delete</button>
|
||||
</header>
|
||||
<main id="main"><p class="muted">Search for an anime to get started.</p></main>
|
||||
<script src="hls.min.js"></script>
|
||||
|
||||
+12
-11
@@ -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>
|
||||
|
||||
@@ -6,6 +6,10 @@ header { display: flex; gap: 1rem; align-items: center; padding: 0.8rem 1.2rem;
|
||||
border-bottom: 1px solid var(--bd); position: sticky; top: 0; background: var(--bg); z-index: 1; }
|
||||
.logo { font-weight: 700; font-size: 1.2rem; color: var(--acc); }
|
||||
header form { flex: 1; max-width: 420px; }
|
||||
button.tiny { padding: .2rem .55rem; font-size: .8rem; border-radius: 6px;
|
||||
border: 1px solid var(--bd); background: var(--card); color: var(--fg); cursor: pointer; }
|
||||
button.tiny.danger { border-color: #7f1d1d; color: #f87171; }
|
||||
header #me { margin-left: auto; }
|
||||
#q { width: 100%; padding: 0.5rem 0.8rem; border-radius: 8px; border: 1px solid var(--bd);
|
||||
background: var(--card); color: var(--fg); }
|
||||
main { padding: 1.2rem; max-width: 1100px; margin: 0 auto; }
|
||||
|
||||
Reference in new issue
Block a user