140 lines
6.1 KiB
Python
140 lines
6.1 KiB
Python
from __future__ import annotations
|
|
|
|
import html
|
|
import os
|
|
from urllib.parse import parse_qs
|
|
|
|
from fastapi import APIRouter, HTTPException, Request
|
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
|
|
from .auth_bridge import HOCKEY_COOKIE_NAME, HOCKEY_SESSION_SECONDS, HockeyAuthDependencies
|
|
|
|
|
|
def create_hockey_auth_router(auth: HockeyAuthDependencies) -> APIRouter:
|
|
router = APIRouter(tags=["Hockey authentication"])
|
|
|
|
@router.get("/login", response_class=HTMLResponse, include_in_schema=False)
|
|
async def login_page(request: Request, reason: str = ""):
|
|
if await auth.optional_user(request):
|
|
return RedirectResponse("/", status_code=303)
|
|
messages = {
|
|
"expired": "Сессия завершена. Войдите снова.",
|
|
"logout": "Вы вышли из хоккейной панели.",
|
|
}
|
|
return _login_response(message=messages.get(reason, ""))
|
|
|
|
@router.post("/login", response_class=HTMLResponse, include_in_schema=False)
|
|
async def login_submit(request: Request):
|
|
try:
|
|
content_length = int(request.headers.get("content-length", "0") or 0)
|
|
except ValueError:
|
|
content_length = 16_385
|
|
if content_length > 16_384:
|
|
return _login_response(error="Слишком большой запрос.", status_code=413)
|
|
values = parse_qs((await request.body()).decode("utf-8", errors="replace"))
|
|
username = (values.get("username") or [""])[0]
|
|
password = (values.get("password") or [""])[0]
|
|
|
|
try:
|
|
result = auth.adapter.login(username, password)
|
|
except HTTPException as error:
|
|
return _login_response(
|
|
error=str(error.detail),
|
|
username=username,
|
|
status_code=error.status_code,
|
|
)
|
|
if result is None:
|
|
return _login_response(
|
|
error="Неверный логин или пароль.",
|
|
username=username,
|
|
status_code=401,
|
|
)
|
|
|
|
token, _user = result
|
|
response = RedirectResponse("/", status_code=303)
|
|
secure_setting = os.getenv("HOCKEY_COOKIE_SECURE", "").strip().lower()
|
|
secure = (
|
|
secure_setting in {"1", "true", "yes", "on"}
|
|
if secure_setting
|
|
else request.url.scheme == "https"
|
|
)
|
|
response.set_cookie(
|
|
HOCKEY_COOKIE_NAME,
|
|
token,
|
|
httponly=True,
|
|
secure=secure,
|
|
samesite="lax",
|
|
max_age=HOCKEY_SESSION_SECONDS,
|
|
path="/",
|
|
)
|
|
return response
|
|
|
|
@router.post("/logout", include_in_schema=False)
|
|
async def logout(request: Request):
|
|
auth.adapter.logout(request.cookies.get(HOCKEY_COOKIE_NAME, ""))
|
|
response = RedirectResponse("/login?reason=logout", status_code=303)
|
|
response.delete_cookie(HOCKEY_COOKIE_NAME, path="/")
|
|
return response
|
|
|
|
return router
|
|
|
|
|
|
def _login_response(
|
|
*,
|
|
error: str = "",
|
|
message: str = "",
|
|
username: str = "",
|
|
status_code: int = 200,
|
|
) -> HTMLResponse:
|
|
error_html = f'<div class="notice error">{html.escape(error)}</div>' if error else ""
|
|
message_html = (
|
|
f'<div class="notice success">{html.escape(message)}</div>' if message else ""
|
|
)
|
|
page = f"""<!doctype html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Вход — Хоккейная панель</title>
|
|
<style>
|
|
:root {{ color-scheme: dark; font-family: Inter, Segoe UI, Arial, sans-serif; }}
|
|
* {{ box-sizing: border-box; }}
|
|
body {{ margin: 0; min-height: 100vh; display: grid; place-items: center; color: #eef5ff;
|
|
background: radial-gradient(circle at 20% 10%, #173e69 0, transparent 35%),
|
|
linear-gradient(145deg, #06101c, #0a1c2f 55%, #07111d); }}
|
|
main {{ width: min(430px, calc(100vw - 32px)); padding: 38px; border: 1px solid #31506f;
|
|
border-radius: 22px; background: rgba(8, 24, 40, .94); box-shadow: 0 28px 90px #0009; }}
|
|
.mark {{ display: inline-flex; padding: 7px 11px; border-radius: 999px; color: #8acbff;
|
|
background: #103554; font-size: 12px; font-weight: 800; letter-spacing: .12em; }}
|
|
h1 {{ margin: 20px 0 8px; font-size: 29px; }}
|
|
p {{ margin: 0 0 26px; color: #9fb2c5; line-height: 1.5; }}
|
|
label {{ display: grid; gap: 8px; margin: 16px 0; color: #c9d8e6; font-size: 13px; font-weight: 700; }}
|
|
input {{ width: 100%; border: 1px solid #35526d; border-radius: 12px; padding: 13px 14px;
|
|
color: white; background: #071421; outline: none; font: inherit; }}
|
|
input:focus {{ border-color: #3ea6ff; box-shadow: 0 0 0 3px #168ee633; }}
|
|
button {{ width: 100%; margin-top: 12px; border: 0; border-radius: 12px; padding: 14px;
|
|
color: #031321; background: linear-gradient(135deg, #61c4ff, #35e0c1); font-weight: 900;
|
|
cursor: pointer; }}
|
|
.notice {{ margin: 16px 0; border-radius: 10px; padding: 11px 13px; font-size: 13px; }}
|
|
.error {{ color: #ffd4d4; background: #5d202b; }} .success {{ color: #caffeb; background: #174c3e; }}
|
|
footer {{ margin-top: 23px; color: #71879b; text-align: center; font-size: 12px; }}
|
|
</style>
|
|
</head>
|
|
<body><main>
|
|
<span class="mark">STAT2TV · HOCKEY</span>
|
|
<h1>Вход в хоккейную панель</h1>
|
|
<p>Используйте действующий аккаунт WFL. После входа откроется рабочий интерфейс хоккея.</p>
|
|
{message_html}{error_html}
|
|
<form method="post" action="/login" autocomplete="on">
|
|
<label>Логин<input name="username" value="{html.escape(username)}" autocomplete="username" required autofocus></label>
|
|
<label>Пароль<input name="password" type="password" autocomplete="current-password" required></label>
|
|
<button type="submit">Войти</button>
|
|
</form>
|
|
<footer>Аккаунты проверяются в WFL · пароль не сохраняется в хоккейной базе</footer>
|
|
</main></body></html>"""
|
|
return HTMLResponse(
|
|
page,
|
|
status_code=status_code,
|
|
headers={"Cache-Control": "no-store", "Pragma": "no-cache"},
|
|
)
|