"""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)