- 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
46 lines
1.8 KiB
HTML
46 lines
1.8 KiB
HTML
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>ani-web — log in</title>
|
|
<style>
|
|
:root { --bg:#0e0f13; --card:#171821; --bd:#262833; --fg:#e8e9f0; --acc:#f97316; }
|
|
body { margin:0; height:100vh; display:grid; place-items:center; background:var(--bg);
|
|
color:var(--fg); font:15px/1.5 system-ui,sans-serif; }
|
|
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); 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>
|
|
</head>
|
|
<body>
|
|
<form onsubmit="return false">
|
|
<h1>ani-web</h1>
|
|
<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 $ = 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({username: $("user").value, password: $("pw").value})});
|
|
if (r.ok) {
|
|
location.href = (next && next.startsWith("/") && !next.startsWith("//")) ? next : "/";
|
|
} else {
|
|
$("err").textContent = (await r.json().catch(() => ({}))).detail || "login failed";
|
|
}
|
|
};
|
|
</script>
|
|
</body>
|
|
</html>
|