single-user login screen
- server/credentials.py: PBKDF2 password + session secret, 0600 json, one-time setup command (server refuses to start without it) - app.py: signed 30-day cookie (itsdangerous), middleware 401s API / redirects pages to /login?next=, 5-fail/15-min lockout, /logout - static/login.html: dark-themed form, inline error display - app.js: api() 401 -> redirect to /login preserving current URL - docs: README quick start + features, SPEC auth section
This commit is contained in:
1 parent
2c203990bc
commit
76c45a8f29
7 files changed
+181
-6
No files matched your search
@@ -4,6 +4,10 @@ let hls = null, detail = null, lang = "eng", seasonId = null;
|
||||
|
||||
async function api(path) {
|
||||
const r = await fetch(path);
|
||||
if (r.status === 401) {
|
||||
location.href = "/login?next=" + encodeURIComponent(location.pathname + location.search);
|
||||
throw new Error("not authenticated");
|
||||
}
|
||||
if (!r.ok) {
|
||||
let msg = r.statusText;
|
||||
try { msg = (await r.json()).detail || msg; } catch {}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<!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); }
|
||||
button { width:100%; margin-top:.8rem; 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 type="password" placeholder="password" autofocus autocomplete="current-password">
|
||||
<button>log in</button>
|
||||
<p class="err"></p>
|
||||
</form>
|
||||
<script>
|
||||
const err = document.querySelector(".err");
|
||||
document.querySelector("button").onclick = async () => {
|
||||
err.textContent = "";
|
||||
const r = await fetch("/login", {method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({password: document.querySelector("input").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";
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user