diff --git a/app/web/routes.py b/app/web/routes.py index a11295f..55fff6a 100644 --- a/app/web/routes.py +++ b/app/web/routes.py @@ -292,10 +292,14 @@ def _get_acasa_context(request: Request, conn, account_id: int) -> dict: acct = account_or_default(account_id) # Pas 1: are credentiale RAR configurate? + metadate cont (pentru banner incomplet) + # Verifica atat coloana legacy rar_creds_enc cat si sloturile per-env (US-008, PRD 5.20). row = conn.execute( - "SELECT id, name, cui, email, rar_creds_enc FROM accounts WHERE id=?", (acct,) + "SELECT id, name, cui, email, rar_creds_enc, rar_creds_test_enc, rar_creds_prod_enc " + "FROM accounts WHERE id=?", (acct,) ).fetchone() - are_creds = bool(row and row["rar_creds_enc"]) + are_creds = bool(row and ( + row["rar_creds_enc"] or row["rar_creds_test_enc"] or row["rar_creds_prod_enc"] + )) # Banner cont incomplet (US-002): contul nu are companie + email + CUI complete cont_incomplet = not _acct_is_complete(row) if row else False @@ -385,22 +389,25 @@ def _render_panel_cont(request: Request, conn, account_id: int) -> str: """Randeaza panoul Cont ca string HTML.""" from ..mapping import account_or_default acct = account_or_default(account_id) - row = conn.execute( - "SELECT id, name, cui, email, rar_creds_enc FROM accounts WHERE id=?", (acct,) - ).fetchone() - are_creds = bool(row and row["rar_creds_enc"]) account_meta = _fetch_account_meta(conn, acct) + env_ctx = _fetch_cont_env_state(conn, acct) cont_ctx = { "request": request, "csrf_token": get_csrf_token(request), "api_key": None, - "are_creds": are_creds, "creds_mesaj": None, "creds_eroare": None, "rot_eroare": None, "account_meta": account_meta, "date_firma_mesaj": None, "date_firma_eroare": None, + "creds_test_mesaj": None, + "creds_test_eroare": None, + "creds_prod_mesaj": None, + "creds_prod_eroare": None, + "creds_default_eroare": None, + "creds_default_mesaj": None, + **env_ctx, } # US-006 (5.17): context plan pentru sectiunea Plan din _cont.html. cont_ctx.update(_plan_ctx(conn, account_id)) @@ -4106,6 +4113,19 @@ def _render_cont( account_meta: dict | None = None, date_firma_mesaj: str | None = None, date_firma_eroare: str | None = None, + # Per-env (US-008, PRD 5.20): starea mediilor RAR Testare + Productie. + test_enabled: bool = False, + prod_enabled: bool = True, + test_disponibil: bool = False, + prod_disponibil: bool = False, + rar_env_default: str = "prod", + medii_disponibile: list | None = None, + creds_test_mesaj: str | None = None, + creds_test_eroare: str | None = None, + creds_prod_mesaj: str | None = None, + creds_prod_eroare: str | None = None, + creds_default_eroare: str | None = None, + creds_default_mesaj: str | None = None, ) -> HTMLResponse: """Randeaza cardul 'Contul meu'. Parola niciodata in value=.""" return templates.TemplateResponse( @@ -4120,6 +4140,18 @@ def _render_cont( account_meta=account_meta or {}, date_firma_mesaj=date_firma_mesaj, date_firma_eroare=date_firma_eroare, + test_enabled=test_enabled, + prod_enabled=prod_enabled, + test_disponibil=test_disponibil, + prod_disponibil=prod_disponibil, + rar_env_default=rar_env_default, + medii_disponibile=medii_disponibile or [], + creds_test_mesaj=creds_test_mesaj, + creds_test_eroare=creds_test_eroare, + creds_prod_mesaj=creds_prod_mesaj, + creds_prod_eroare=creds_prod_eroare, + creds_default_eroare=creds_default_eroare, + creds_default_mesaj=creds_default_mesaj, ), ) @@ -4139,6 +4171,56 @@ def _fetch_account_meta(conn, acct: int) -> dict: } +def _fetch_cont_env_state(conn, acct: int) -> dict: + """Starea mediilor RAR per env pentru sectiunea 'Credentiale RAR' din _cont.html (US-008). + + Returneaza un dict compatibil cu parametrii per-env ai _render_cont: + are_creds -- True daca ORICE credentiale RAR sunt configurate (legacy SAU per-env) + test_enabled -- bifa activare Testare + prod_enabled -- bifa activare Productie + test_disponibil -- Testare activata SI cu creds (poate trimite) + prod_disponibil -- Productie activata SI cu creds (poate trimite) + rar_env_default -- mediul implicit al contului + medii_disponibile -- lista mediilor disponibile (subset din ['test','prod']) + """ + row = conn.execute( + "SELECT rar_test_enabled, rar_prod_enabled, " + "rar_creds_test_enc, rar_creds_prod_enc, rar_env_default, rar_creds_enc " + "FROM accounts WHERE id=?", (acct,) + ).fetchone() + if not row: + return { + "are_creds": False, + "test_enabled": False, + "prod_enabled": True, + "test_disponibil": False, + "prod_disponibil": False, + "rar_env_default": "prod", + "medii_disponibile": [], + } + test_enabled = bool(row["rar_test_enabled"]) + prod_enabled = bool(row["rar_prod_enabled"]) + test_disponibil = test_enabled and bool(row["rar_creds_test_enc"]) + prod_disponibil = prod_enabled and bool(row["rar_creds_prod_enc"]) + medii: list[str] = [] + if test_disponibil: + medii.append("test") + if prod_disponibil: + medii.append("prod") + are_creds = bool( + row["rar_creds_enc"] or row["rar_creds_test_enc"] or row["rar_creds_prod_enc"] + ) + return { + "are_creds": are_creds, + "test_enabled": test_enabled, + "prod_enabled": prod_enabled, + "test_disponibil": test_disponibil, + "prod_disponibil": prod_disponibil, + "rar_env_default": row["rar_env_default"] or "prod", + "medii_disponibile": medii, + } + + @router.get("/_fragments/cont", response_class=HTMLResponse) def fragment_cont(request: Request) -> HTMLResponse: """Fragment HTMX card 'Contul meu': stare cheie + creds RAR + date firma.""" @@ -4146,12 +4228,9 @@ def fragment_cont(request: Request) -> HTMLResponse: acct = account_or_default(account_id) conn = get_connection() try: - row = conn.execute( - "SELECT id, name, cui, email, rar_creds_enc FROM accounts WHERE id=?", (acct,) - ).fetchone() - are_creds = bool(row and row["rar_creds_enc"]) account_meta = _fetch_account_meta(conn, acct) - return _render_cont(request, are_creds=are_creds, account_meta=account_meta) + env_ctx = _fetch_cont_env_state(conn, acct) + return _render_cont(request, account_meta=account_meta, **env_ctx) finally: conn.close() @@ -4168,12 +4247,9 @@ def cont_roteste_cheie( conn = get_connection() try: new_key = rotate_api_key(conn, acct) - row = conn.execute( - "SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,) - ).fetchone() - are_creds = bool(row and row["rar_creds_enc"]) account_meta = _fetch_account_meta(conn, acct) - return _render_cont(request, api_key=new_key, are_creds=are_creds, account_meta=account_meta) + env_ctx = _fetch_cont_env_state(conn, acct) + return _render_cont(request, api_key=new_key, account_meta=account_meta, **env_ctx) finally: conn.close() @@ -4202,15 +4278,14 @@ async def cont_date_firma(request: Request) -> HTMLResponse: conn = get_connection() try: account_meta = _fetch_account_meta(conn, acct) - row_cr = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)).fetchone() - are_creds = bool(row_cr and row_cr["rar_creds_enc"]) + env_ctx = _fetch_cont_env_state(conn, acct) finally: conn.close() return _render_cont( request, - are_creds=are_creds, account_meta=account_meta, date_firma_eroare="Compania (numele firmei) este obligatorie.", + **env_ctx, ) # Normalizare si validare email @@ -4219,31 +4294,27 @@ async def cont_date_firma(request: Request) -> HTMLResponse: except ValueError as exc: conn = get_connection() try: - account_meta = _fetch_account_meta(conn, acct) - row_cr = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)).fetchone() - are_creds = bool(row_cr and row_cr["rar_creds_enc"]) + env_ctx = _fetch_cont_env_state(conn, acct) finally: conn.close() return _render_cont( request, - are_creds=are_creds, account_meta={"name": companie_raw, "email": email_raw, "cui": cui_raw}, date_firma_eroare=f"Email invalid: {exc}", + **env_ctx, ) if not email_norm: conn = get_connection() try: - account_meta = _fetch_account_meta(conn, acct) - row_cr = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)).fetchone() - are_creds = bool(row_cr and row_cr["rar_creds_enc"]) + env_ctx = _fetch_cont_env_state(conn, acct) finally: conn.close() return _render_cont( request, - are_creds=are_creds, account_meta={"name": companie_raw, "email": email_raw, "cui": cui_raw}, date_firma_eroare="Email-ul de contact este obligatoriu.", + **env_ctx, ) # Normalizare si validare CUI @@ -4252,31 +4323,27 @@ async def cont_date_firma(request: Request) -> HTMLResponse: except ValueError as exc: conn = get_connection() try: - account_meta = _fetch_account_meta(conn, acct) - row_cr = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)).fetchone() - are_creds = bool(row_cr and row_cr["rar_creds_enc"]) + env_ctx = _fetch_cont_env_state(conn, acct) finally: conn.close() return _render_cont( request, - are_creds=are_creds, account_meta={"name": companie_raw, "email": email_norm, "cui": cui_raw}, date_firma_eroare=f"CUI invalid: {exc}", + **env_ctx, ) if not cui_norm: conn = get_connection() try: - account_meta = _fetch_account_meta(conn, acct) - row_cr = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)).fetchone() - are_creds = bool(row_cr and row_cr["rar_creds_enc"]) + env_ctx = _fetch_cont_env_state(conn, acct) finally: conn.close() return _render_cont( request, - are_creds=are_creds, account_meta={"name": companie_raw, "email": email_norm, "cui": cui_raw}, date_firma_eroare="CUI-ul firmei este obligatoriu.", + **env_ctx, ) # Actualizare in DB @@ -4294,26 +4361,24 @@ async def cont_date_firma(request: Request) -> HTMLResponse: ).fetchone() owner = existing["id"] if existing else "?" account_meta = _fetch_account_meta(conn, acct) - row_cr = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)).fetchone() - are_creds = bool(row_cr and row_cr["rar_creds_enc"]) + env_ctx = _fetch_cont_env_state(conn, acct) return _render_cont( request, - are_creds=are_creds, account_meta={"name": companie_raw, "email": email_norm, "cui": cui_norm}, date_firma_eroare=( f"CUI-ul {cui_norm} este deja folosit de alt cont (id={owner}). " "Foloseste un CUI diferit sau contacteaza administratorul." ), + **env_ctx, ) account_meta = _fetch_account_meta(conn, acct) - row_cr = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)).fetchone() - are_creds = bool(row_cr and row_cr["rar_creds_enc"]) + env_ctx = _fetch_cont_env_state(conn, acct) return _render_cont( request, - are_creds=are_creds, account_meta=account_meta, date_firma_mesaj="Datele firmei au fost salvate.", + **env_ctx, ) finally: conn.close() @@ -4396,18 +4461,15 @@ def cont_rar_creds( if not email or not parola: conn = get_connection() try: - row = conn.execute( - "SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,) - ).fetchone() - are_creds = bool(row and row["rar_creds_enc"]) account_meta = _fetch_account_meta(conn, acct) + env_ctx = _fetch_cont_env_state(conn, acct) finally: conn.close() return _render_cont( request, - are_creds=are_creds, creds_eroare="Email si parola sunt obligatorii.", account_meta=account_meta, + **env_ctx, ) enc = encrypt_creds({"email": email, "password": parola}) @@ -4418,11 +4480,12 @@ def cont_rar_creds( (enc, acct), ) account_meta = _fetch_account_meta(conn, acct) + env_ctx = _fetch_cont_env_state(conn, acct) return _render_cont( request, - are_creds=True, creds_mesaj="Credentialele RAR au fost salvate cu succes.", account_meta=account_meta, + **env_ctx, ) finally: conn.close() @@ -4484,3 +4547,155 @@ def cont_test_rar_creds( "_integrare_test_rezultat.html", {"request": request, "succes": False, "mesaj": mesaj_eroare}, ) + + +# =========================================================================== # +# US-008 (PRD 5.20): Configurare medii RAR per cont — Testare + Productie. # +# Ruta noua /cont/rar-medii: gestioneaza bifa activare, credentiale si # +# mediul implicit separat pentru fiecare din cele doua medii RAR. # +# =========================================================================== # + +@router.post("/cont/rar-medii", response_class=HTMLResponse) +async def cont_rar_medii(request: Request) -> HTMLResponse: + """Salveaza configuratia mediilor RAR per cont (US-008, PRD 5.20). + + Doua sectiuni independente (Testare / Productie): fiecare cu bifa de activare + si campuri email/parola. La salvare, pentru fiecare mediu activat cu creds noi: + - valideaza prin login pe acel env (US-007) — RAR test si prod sunt sisteme separate; + - OK -> cripteaza cu Fernet si scrie in rar_creds_{env}_enc + enabled=1; + - esec login -> eroare per-env, mediul NU devine disponibil (creds nesalvate). + + Activarea Productie pentru prima oara necesita checkbox-ul de confirmare + (constientizare L.142 — trimiterile sunt declaratii oficiale, finale si fara anulare). + + Mediul implicit (rar_env_default) poate fi setat DOAR pe un mediu disponibil + (validat server-side post-update; altfel eroare, valoarea veche ramane). + + Parolele NICIODATA reflectate inapoi in pagina (camp gol cu placeholder). + """ + account_id = require_login(request) + form = await request.form() + verify_csrf(request, str(form.get("csrf_token") or "")) + acct = account_or_default(account_id) + + test_enabled_form = form.get("test_enabled") == "1" + prod_enabled_form = form.get("prod_enabled") == "1" + prod_confirmare = form.get("prod_confirmare") == "1" + test_email = str(form.get("test_email") or "").strip() + test_parola = str(form.get("test_parola") or "").strip() + prod_email = str(form.get("prod_email") or "").strip() + prod_parola = str(form.get("prod_parola") or "").strip() + rar_env_default_form = str(form.get("rar_env_default") or "").strip() + + settings = get_settings() + creds_test_eroare: str | None = None + creds_test_mesaj: str | None = None + creds_prod_eroare: str | None = None + creds_prod_mesaj: str | None = None + creds_default_eroare: str | None = None + creds_default_mesaj: str | None = None + + conn = get_connection() + try: + # Starea curenta din DB (inainte de update) — necesara pt logica de confirmare prod. + row_before = conn.execute( + "SELECT rar_prod_enabled FROM accounts WHERE id=?", (acct,) + ).fetchone() + was_prod_enabled = bool(row_before["rar_prod_enabled"]) if row_before else False + + # Confirmare obligatorie la PRIMA activare Productie (constientizare L.142). + # Nu se cere daca Productie era deja activata (confirmare unica per-activare). + if prod_enabled_form and not was_prod_enabled and not prod_confirmare: + creds_prod_eroare = ( + "Bifati confirmarea de mai jos pentru a activa mediul Productie: " + "\"Inteleg ca trimiterile pe Productie sunt declaratii reale (L.142)\"." + ) + account_meta = _fetch_account_meta(conn, acct) + env_ctx = _fetch_cont_env_state(conn, acct) + return _render_cont( + request, + account_meta=account_meta, + creds_prod_eroare=creds_prod_eroare, + **env_ctx, + ) + + # --- Procesare Testare --- + if test_enabled_form: + if test_email and test_parola: + # Ambele campuri completate -> valideaza prin login pe RAR Testare. + ok, mesaj = _valideaza_login_rar(settings, test_email, test_parola, "test") + if ok: + enc = encrypt_creds({"email": test_email, "password": test_parola}) + conn.execute( + "UPDATE accounts SET rar_test_enabled=1, rar_creds_test_enc=? WHERE id=?", + (enc, acct), + ) + creds_test_mesaj = "Credentiale Testare salvate si validate." + else: + # Login esuat: nu schimbam creds sau enabled; eroare per-env. + creds_test_eroare = mesaj + elif test_email or test_parola: + # Doar unul din campuri completat -> eroare (nu pot fi partial). + creds_test_eroare = "Email si parola Testare trebuie completate impreuna." + else: + # Activat fara creds noi -> marcheaza enabled (creds existente, daca sunt, raman). + conn.execute("UPDATE accounts SET rar_test_enabled=1 WHERE id=?", (acct,)) + else: + # Dezactivat -> disabled=0; creds raman pentru posibila re-activare ulterioara. + conn.execute("UPDATE accounts SET rar_test_enabled=0 WHERE id=?", (acct,)) + + # --- Procesare Productie --- + if prod_enabled_form: + if prod_email and prod_parola: + ok, mesaj = _valideaza_login_rar(settings, prod_email, prod_parola, "prod") + if ok: + enc = encrypt_creds({"email": prod_email, "password": prod_parola}) + conn.execute( + "UPDATE accounts SET rar_prod_enabled=1, rar_creds_prod_enc=? WHERE id=?", + (enc, acct), + ) + creds_prod_mesaj = "Credentiale Productie salvate si validate." + else: + creds_prod_eroare = mesaj + elif prod_email or prod_parola: + creds_prod_eroare = "Email si parola Productie trebuie completate impreuna." + else: + conn.execute("UPDATE accounts SET rar_prod_enabled=1 WHERE id=?", (acct,)) + else: + conn.execute("UPDATE accounts SET rar_prod_enabled=0 WHERE id=?", (acct,)) + + # --- Mediu implicit (validat post-update contra mediilor disponibile) --- + if rar_env_default_form and rar_env_default_form in ("test", "prod"): + from ..rar_env import medii_disponibile as _medii_disponibile_fn + row_after = conn.execute( + "SELECT rar_test_enabled, rar_prod_enabled, " + "rar_creds_test_enc, rar_creds_prod_enc " + "FROM accounts WHERE id=?", (acct,) + ).fetchone() + medii_post = _medii_disponibile_fn(row_after) + if rar_env_default_form in medii_post: + conn.execute( + "UPDATE accounts SET rar_env_default=? WHERE id=?", + (rar_env_default_form, acct), + ) + creds_default_mesaj = "Mediu implicit actualizat." + else: + creds_default_eroare = ( + "Mediul ales nu e disponibil — activeaza-l si adauga credentiale valide intai." + ) + + account_meta = _fetch_account_meta(conn, acct) + env_ctx = _fetch_cont_env_state(conn, acct) + return _render_cont( + request, + account_meta=account_meta, + creds_test_mesaj=creds_test_mesaj, + creds_test_eroare=creds_test_eroare, + creds_prod_mesaj=creds_prod_mesaj, + creds_prod_eroare=creds_prod_eroare, + creds_default_eroare=creds_default_eroare, + creds_default_mesaj=creds_default_mesaj, + **env_ctx, + ) + finally: + conn.close() diff --git a/app/web/templates/_cont.html b/app/web/templates/_cont.html index 0623940..8b24ec3 100644 --- a/app/web/templates/_cont.html +++ b/app/web/templates/_cont.html @@ -114,36 +114,106 @@