91 lines
3.1 KiB
Python
91 lines
3.1 KiB
Python
"""Autenticação: hash de senha (bcrypt), seed do admin, CSRF e guarda de sessão."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import secrets
|
|
import time
|
|
|
|
import bcrypt
|
|
from starlette.requests import Request
|
|
|
|
from . import database as db
|
|
from .config import get_settings
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Senhas
|
|
# --------------------------------------------------------------------------- #
|
|
def hash_password(password: str) -> str:
|
|
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("ascii")
|
|
|
|
|
|
def verify_password(password: str, password_hash: str) -> bool:
|
|
try:
|
|
return bcrypt.checkpw(password.encode("utf-8"), password_hash.encode("ascii"))
|
|
except (ValueError, TypeError):
|
|
return False
|
|
|
|
|
|
def seed_admin() -> str | None:
|
|
"""Cria o usuário admin na 1ª execução. Retorna uma mensagem de aviso, se houver."""
|
|
settings = get_settings()
|
|
with db.session() as conn:
|
|
if db.count_users(conn) > 0:
|
|
return None
|
|
password = settings.admin_password
|
|
note = None
|
|
if not password:
|
|
# Sem ADMIN_PASSWORD definido: gera uma senha aleatória e a expõe UMA vez
|
|
# no log para o operador. Em produção defina ADMIN_PASSWORD no ambiente.
|
|
password = secrets.token_urlsafe(12)
|
|
note = (
|
|
f"[AVISO] Nenhum ADMIN_PASSWORD definido. Usuário '{settings.admin_username}' "
|
|
f"criado com senha temporária: {password} (defina ADMIN_PASSWORD e reinicie)"
|
|
)
|
|
db.create_user(conn, settings.admin_username, hash_password(password))
|
|
return note
|
|
|
|
|
|
def authenticate(username: str, password: str) -> bool:
|
|
with db.session() as conn:
|
|
row = db.get_user(conn, username)
|
|
if row is None:
|
|
# Compara mesmo sem usuário para não vazar tempo (mitiga user enumeration).
|
|
verify_password(password, hash_password("dummy"))
|
|
return False
|
|
return verify_password(password, row["password_hash"])
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Sessão / guarda
|
|
# --------------------------------------------------------------------------- #
|
|
def current_user(request: Request) -> str | None:
|
|
return request.session.get("user")
|
|
|
|
|
|
def login_session(request: Request, username: str) -> None:
|
|
request.session["user"] = username
|
|
request.session["logged_at"] = int(time.time())
|
|
# rotaciona o token CSRF a cada login
|
|
request.session["csrf"] = secrets.token_urlsafe(32)
|
|
|
|
|
|
def logout_session(request: Request) -> None:
|
|
request.session.clear()
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# CSRF
|
|
# --------------------------------------------------------------------------- #
|
|
def get_csrf_token(request: Request) -> str:
|
|
token = request.session.get("csrf")
|
|
if not token:
|
|
token = secrets.token_urlsafe(32)
|
|
request.session["csrf"] = token
|
|
return token
|
|
|
|
|
|
def check_csrf(request: Request, submitted: str | None) -> bool:
|
|
expected = request.session.get("csrf")
|
|
return bool(expected) and bool(submitted) and secrets.compare_digest(expected, submitted)
|