76 lines
2.0 KiB
Python
76 lines
2.0 KiB
Python
from __future__ import annotations
|
||
|
||
import base64
|
||
import hmac
|
||
import hashlib
|
||
import os
|
||
import time
|
||
from dataclasses import dataclass
|
||
from typing import Optional
|
||
|
||
|
||
COOKIE_NAME = "voley_admin"
|
||
|
||
|
||
def _get_secret() -> bytes:
|
||
secret = os.getenv("SECRET_KEY", "").strip()
|
||
if not secret:
|
||
# Разрешаем запуск без секрета, но с сильно сниженной безопасностью.
|
||
# Для реального деплоя SECRET_KEY обязателен.
|
||
secret = "dev-unsafe-secret"
|
||
return secret.encode("utf-8")
|
||
|
||
|
||
def _b64url(b: bytes) -> str:
|
||
return base64.urlsafe_b64encode(b).decode("ascii").rstrip("=")
|
||
|
||
|
||
def _b64url_decode(s: str) -> bytes:
|
||
pad = "=" * (-len(s) % 4)
|
||
return base64.urlsafe_b64decode((s + pad).encode("ascii"))
|
||
|
||
|
||
def _sign(payload: bytes) -> str:
|
||
mac = hmac.new(_get_secret(), payload, hashlib.sha256).digest()
|
||
return _b64url(mac)
|
||
|
||
|
||
def make_session(ttl_seconds: int = 60 * 60 * 12) -> str:
|
||
exp = int(time.time()) + int(ttl_seconds)
|
||
payload = f"exp={exp}".encode("utf-8")
|
||
sig = _sign(payload)
|
||
return f"{_b64url(payload)}.{sig}"
|
||
|
||
|
||
def verify_session(token: str) -> bool:
|
||
try:
|
||
payload_b64, sig = token.split(".", 1)
|
||
payload = _b64url_decode(payload_b64)
|
||
except Exception:
|
||
return False
|
||
if not hmac.compare_digest(_sign(payload), sig):
|
||
return False
|
||
try:
|
||
parts = payload.decode("utf-8").split("=", 1)
|
||
if len(parts) != 2 or parts[0] != "exp":
|
||
return False
|
||
exp = int(parts[1])
|
||
except Exception:
|
||
return False
|
||
return time.time() < exp
|
||
|
||
|
||
def check_admin_password(password: str) -> bool:
|
||
expected = os.getenv("ADMIN_PASSWORD", "")
|
||
if not expected:
|
||
# Чтобы админка не осталась открытой по умолчанию.
|
||
return False
|
||
return hmac.compare_digest(password or "", expected)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class AdminAuth:
|
||
is_admin: bool
|
||
token: Optional[str] = None
|
||
|