feat(web): require login only for candidates and system
This commit is contained in:
1 parent
55c40db047
commit
6aefe9ce87
6 files changed
+130
-32
No files matched your search
@@ -18,7 +18,7 @@ from http.cookies import SimpleCookie
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Mapping
|
||||
from urllib.parse import parse_qs, unquote, urlsplit
|
||||
from urllib.parse import parse_qs, quote, unquote, urlsplit
|
||||
|
||||
from ..analytics import is_loopback
|
||||
from ..config import ConfigStore
|
||||
@@ -28,6 +28,8 @@ from .queries import DashboardQueries
|
||||
STATIC_ROOT = Path(__file__).resolve().parent / "static"
|
||||
COOKIE_NAME = "rb_session"
|
||||
SESSION_TTL = 7 * 86400
|
||||
PROTECTED_API = {"/api/candidates", "/api/system"}
|
||||
PROTECTED_PAGES = {"/candidates", "/system"}
|
||||
|
||||
|
||||
class ApiError(Exception):
|
||||
@@ -117,18 +119,26 @@ class DashboardHandler(BaseHTTPRequestHandler):
|
||||
})
|
||||
return
|
||||
|
||||
if path == "/api/session":
|
||||
self._send_json(HTTPStatus.OK, {
|
||||
"auth_enabled": bool(self.context.auth_token),
|
||||
"authenticated": self._authorized(),
|
||||
"preview": self.context.preview,
|
||||
})
|
||||
return
|
||||
|
||||
if path == "/login":
|
||||
if method == "POST":
|
||||
self._login_post()
|
||||
self._login_post(query)
|
||||
else:
|
||||
self._login_page()
|
||||
self._login_page(query)
|
||||
return
|
||||
if path == "/logout":
|
||||
self._logout()
|
||||
return
|
||||
|
||||
if path.startswith("/api/"):
|
||||
if not self._authorized():
|
||||
if path in PROTECTED_API and not self._authorized():
|
||||
self._send_error(HTTPStatus.UNAUTHORIZED, "unauthorized", "需要登录")
|
||||
return
|
||||
if method == "POST":
|
||||
@@ -141,8 +151,8 @@ class DashboardHandler(BaseHTTPRequestHandler):
|
||||
self._api(path, query, method)
|
||||
return
|
||||
|
||||
if not self._authorized():
|
||||
self._redirect("/login")
|
||||
if path in PROTECTED_PAGES and not self._authorized():
|
||||
self._redirect(f"/login?next={quote(path)}")
|
||||
return
|
||||
self._static(path, method)
|
||||
except ApiError as exc:
|
||||
@@ -203,10 +213,11 @@ class DashboardHandler(BaseHTTPRequestHandler):
|
||||
morsel = cookie.get(COOKIE_NAME)
|
||||
return morsel is not None and self._valid_session(morsel.value)
|
||||
|
||||
def _login_page(self) -> None:
|
||||
def _login_page(self, query: Mapping[str, list[str]]) -> None:
|
||||
if not self.context.auth_token:
|
||||
self._redirect("/")
|
||||
return
|
||||
next_path = _safe_next(_param(query, "next", ""))
|
||||
html = """<!doctype html>
|
||||
<html lang="zh-CN"><head><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
@@ -230,9 +241,12 @@ border-color:#3a2c22;color:#f3e9dc}button{background:#e9b97c;color:#2a1a10}}
|
||||
<input type="password" name="token" autocomplete="current-password" placeholder="访问令牌" required autofocus>
|
||||
<button type="submit">进入看板</button>
|
||||
</form></main></body></html>"""
|
||||
html = html.replace(
|
||||
'action="/login"', f'action="/login?next={quote(next_path)}"'
|
||||
)
|
||||
self._send_html(HTTPStatus.OK, html)
|
||||
|
||||
def _login_post(self) -> None:
|
||||
def _login_post(self, query: Mapping[str, list[str]]) -> None:
|
||||
if not self.context.auth_token:
|
||||
self._redirect("/")
|
||||
return
|
||||
@@ -250,8 +264,9 @@ border-color:#3a2c22;color:#f3e9dc}button{background:#e9b97c;color:#2a1a10}}
|
||||
return
|
||||
expires_at = int(self.context.clock()) + SESSION_TTL
|
||||
value = self._session_value(expires_at)
|
||||
target = _safe_next(_param(query, "next", ""))
|
||||
self.send_response(HTTPStatus.SEE_OTHER)
|
||||
self.send_header("Location", "/")
|
||||
self.send_header("Location", target)
|
||||
self.send_header("Set-Cookie", (
|
||||
f"{COOKIE_NAME}={value}; Path=/; HttpOnly; SameSite=Lax; "
|
||||
f"Max-Age={SESSION_TTL}"
|
||||
@@ -481,6 +496,13 @@ border-color:#3a2c22;color:#f3e9dc}button{background:#e9b97c;color:#2a1a10}}
|
||||
# ------------------------------------------------------------ helpers
|
||||
|
||||
|
||||
def _safe_next(value: str | None) -> str:
|
||||
text = (value or "").strip()
|
||||
if not text.startswith("/") or text.startswith("//") or text.startswith("/login"):
|
||||
return "/"
|
||||
return text
|
||||
|
||||
|
||||
def _param(query: Mapping[str, list[str]], name: str, default: str) -> str:
|
||||
values = query.get(name)
|
||||
return default if not values else str(values[0])
|
||||
|
||||
Reference in new issue
Block a user