Files
rar-autopass/app/config.py
Claude Agent b26dbb79e1 feat(5.12): modal editare + cont obligatoriu la import; design.md + PRD 5.13 revizuit (/autoplan)
5.12 (livrat): editare in modal a randurilor de preview, cont obligatoriu inainte de
import, formular editare extras (_form_editare, _editare_preview_modal), plus suita de
teste aferenta (preview edit/compact, mapare op, form editare, signup, admin panel).

Design + planificare:
- docs/design.md: sistem de design (tokeni, breakpoints, scara control, componente, a11y).
- docs/prd/prd-5.12-* si prd-5.13-* (5.13 cu raport /autoplan: CEO+Design+Eng, audit trail).

Curatare: sterse PNG-urile de test/mockup temporare din radacina.

Nota: implementarea CSS 5.13 (responsive compact + sistem butoane) NU e inca facuta —
planul revizuit cere refactorul testelor fragile din test_web_responsive.py INAINTE de CSS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 18:52:20 +00:00

138 lines
5.7 KiB
Python

"""Configurare gateway. Env vars (prefix AUTOPASS_) + valori implicite.
NU stocheaza parole RAR. Credentialele RAR vin per-cerere de la ROAAUTO.
Helper-ul `load_test_credentials` citeste blocul <test> din settings.xml DOAR
pentru dev local / probe pe mediul de test.
"""
from __future__ import annotations
import xml.etree.ElementTree as ET
from functools import lru_cache
from pathlib import Path
from pydantic_settings import BaseSettings, SettingsConfigDict
ROOT = Path(__file__).resolve().parent.parent
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="AUTOPASS_", env_file=".env", extra="ignore")
# --- Bază de date ---
db_path: Path = ROOT / "data" / "autopass.db"
# --- Observabilitate / jurnal aplicatie ---
# Nivel minim al evenimentelor scrise in app_events + log text. Sub el, evenimentul
# e ignorat (best-effort). DEBUG|INFO|WARNING|ERROR|CRITICAL.
log_level: str = "INFO"
log_retention_days: int = 90
# Director pentru log-ul text rotativ (RotatingFileHandler in aplicatie).
# Fisier per-proces (app-api.log / app-worker.log) — rotatia nu e multiproces-safe.
log_dir: Path = ROOT / ".run"
log_file_max_bytes: int = 5_000_000
log_file_backup_count: int = 5
# Retentie randuri blocate (error/needs_data/needs_mapping). Mai scurt decat 90z
# ale `sent` — un blocat n-are valoare de audit.
blocked_retention_days: int = 30
# --- Securitate ---
# Enforcement auth API-key pe /v1/* protejat. False (dev/test): fara cheie ->
# cont implicit id=1. True (prod): fara cheie valida -> 401. O cheie PREZENTA
# dar invalida da 401 indiferent de flag.
require_api_key: bool = False
# Cheie Fernet pentru criptarea creds RAR efemere in submissions (zero-storage
# at rest). Nesetata -> cheie efemera la runtime (creds nu supravietuiesc
# restartului). In productie seteaz-o persistent. Genereaza:
# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
creds_key: str | None = None
# --- Sesiuni web ---
# Secret semnat cookie sesiune. None -> efemer la fiecare restart (dev ok;
# in prod seteaza persistent ca si creds_key, altfel cookieurile se invalideaza
# la restart). Genereaza: python -c "import secrets; print(secrets.token_hex(32))"
session_secret: str | None = None
# True (IMPLICIT, sigur pentru prod): rutele web fara sesiune -> redirect /login;
# CSRF enforce. Pentru dev rapid pe contul implicit id=1,
# seteaza explicit AUTOPASS_WEB_AUTH_REQUIRED=false.
web_auth_required: bool = True
# True (prod, in spatele Cloudflare Tunnel TLS): cookie cu Secure flag.
# False (dev): cookie fara Secure, functioneaza pe HTTP.
session_https_only: bool = False
# --- Contact suport (US-001, PRD 5.12) ---
# Email/canal de contact afisat in mesaje catre utilizatori (ex. CUI duplicat la signup).
# Nesetat -> fallback la formularea generica fara canal concret.
support_email: str | None = None
# --- Notificare email admin la signup ---
# Nesetat (smtp_host None) -> notificarea e DEGRADATA (doar log SIGNUP).
smtp_host: str | None = None
smtp_port: int = 587
smtp_user: str | None = None
smtp_password: str | None = None
smtp_from: str | None = None
# --- Rate-limit signup + login ---
# Max cereri POST /signup per IP in fereastra de timp (in-proces, fara dependinta noua).
signup_rate_max: int = 5
signup_rate_window_s: int = 3600
# Max incercari POST /login per IP (brute-force parole). Fereastra impartita cu signup.
login_rate_max: int = 10
# --- RAR ---
rar_env: str = "test" # "test" | "prod"
rar_base_url_test: str = "https://apps.rarom.ro/test-rar-autopass"
rar_base_url_prod: str = "https://apps.rarom.ro/rar-autopass"
# WAF-ul RAR da 403 fara User-Agent de browser. Toate apelurile httpx il trimit.
http_user_agent: str = "Mozilla/5.0"
http_timeout_s: float = 30.0
# --- Worker ---
worker_poll_interval_s: float = 5.0
worker_heartbeat_stale_s: int = 30 # /healthz considera worker-ul mort peste atat
# Send DEZACTIVAT implicit (nu trimite la RAR). Activeaza-l explicit pentru
# proba end-to-end.
worker_send_enabled: bool = False
# Dev: foloseste creds <test> din settings.xml pt login worker. In productie
# creds vin per-cerere de la ROAAUTO — lasa False.
worker_use_test_creds: bool = False
worker_sending_lease_s: int = 120 # rand 'sending' mai vechi de atat = orfan (worker mort mid-POST)
worker_retry_base_s: int = 5 # backoff = base * 2^retry (plafonat la max)
worker_retry_max_s: int = 300
worker_max_retries: int = 8 # peste atat -> error + banner
@property
def rar_base_url(self) -> str:
return self.rar_base_url_prod if self.rar_env == "prod" else self.rar_base_url_test
@lru_cache
def get_settings() -> Settings:
return Settings()
def load_test_credentials(settings_xml: Path | None = None) -> dict | None:
"""Citeste credentialele <test> din settings.xml (dev local / probe test).
Intoarce {"email", "password"} sau None daca fisierul lipseste / e template.
NU se foloseste in productie — acolo creds vin per-cerere de la ROAAUTO.
"""
path = settings_xml or (ROOT / "settings.xml")
if not path.exists():
return None
try:
root = ET.parse(path).getroot()
node = root.find("./test/credentials")
if node is None:
return None
email = (node.findtext("email") or "").strip()
password = (node.findtext("password") or "").strip()
if not email or not password or email.startswith("EMAIL_"):
return None
return {"email": email, "password": password}
except ET.ParseError:
return None