Files
rar-autopass/docs/prd/prd-3.3-self-onboarding-web.md
Claude Agent 504b490d3b feat(web): self-onboarding multi-tenant + auth sesiune (PRD 3.3a)
Canalul web trece de la 100% deschis (hardcodat cont 1) la autentificat si
multi-tenant. Un service nou se inregistreaza din browser, primeste o cheie API
(o singura data) si o sesiune; contul se creeaza "in asteptare" (active=0) si nu
trimite la RAR pana la activarea de catre admin (tools/account.py activate).

- users + app/users.py: parole scrypt (salt per-user, eticheta parametri onorata
  la verify pentru migrare cost), email unic case-insensitive
- sesiune: SessionMiddleware (same_site=strict, https_only config) + app/web/session.py
  (current_account/web_account/require_login->LoginRequired, set_session clear-inainte)
- CSRF (app/web/csrf.py) enforce in prod inclusiv pe login/signup + rate-limit
  in-proces (app/web/ratelimit.py) pe signup si login
- signup/login/logout (app/web/auth_routes.py): signup tranzactie atomica,
  cheie-o-data, log SIGNUP pentru descoperire admin
- dashboard + import scoped pe contul sesiunii (regula NULL->cont 1); toate rutele
  web care ating date sensibile sub require_login; nomenclator ramane global
- banner "cont in asteptare" pentru conturi active=0
- gate worker: claim_one LEFT JOIN accounts COALESCE(active,1)=1 (account_id NULL=activ)

VERIFY context curat (2 runde): leak cross-account /_fragments/mapari prins+reparat.
/code-review high: csrf_token lipsa pe re-randari de eroare, scrypt_params ignorat,
login fara rate-limit -- toate reparate. 361 teste pass (de la 313).

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

32 KiB

PRD 3.3 — Self-onboarding web (login email+parola → emite cheie)

Stare: verify-pass (3.3a) — 3.3b deschis

Decizii la poarta EXECUTE (2026-06-17, confirmate de utilizator):

  • Livrabila sparta in doua faze (scope 12 stories prea mare pentru un singur EXECUTE):
    • 3.3a (in executie acum) — self-onboarding core: US-001, US-002, US-009, US-003, US-004, US-005, US-006a, US-006b, US-008. Commit + VERIFY propriu.
    • 3.3b (urmeaza) — admin web + email: US-010, US-011, US-012. Commit + VERIFY propriu.
  • Bootstrap admin (US-010, 3.3b): primul cont creat devine automat admin (is_admin=1).
  • US-012 email: livrare DEGRADATA fara SMTP — doar log SIGNUP cont=N email=... (C16) + /admin (US-011) + tools/account.py list --pending. Trimiterea efectiva = follow-up cand exista SMTP.
  • Prerechizita C1 confirmata: 3.1 livrat (app/accounts.py, tools/account.py, active migrat in _migrate).

Proces complet: docs/ROADMAP.md §5. Contract RAR (sursa de adevar): docs/api-rar-contract.md. Starea trece: draft → aprobat → in-executie → verify-pass → inchis (actualizata de lead).

Aceasta este cea mai mare livrabila a Etapei 3. Schimba postura de securitate a intregului canal web (azi 100% deschis, hardcodat pe contul 1). Intrebarile din §5 trebuie rezolvate la poarta de aprobare ÎNAINTE de executie — in special cine are voie sa se inregistreze.

1. Obiectiv

Un service nou se inregistreaza singur din browser (companie + email + parola), primeste pe loc o cheie API (afisata o singura data) si o sesiune web legata de contul lui. Contul se creeaza „in asteptare" (active=0): userul poate naviga, configura creds RAR si pregati import, dar nicio prezentare nu se trimite la RAR pana cand un admin activeaza contul (tools/account.py activate, din 3.1). Inlocuieste account_id = DEFAULT_ACCOUNT_ID hardcodat din app/web/routes.py cu contul din sesiune, facand dashboard-ul si importul multi-tenant. Construieste peste 3.1 (helper cont + active) si peste pattern-ul de scoping din 3.2 (acum aplicat si rutelor web).

Fara dependinte noi: sesiuni via starlette.middleware.sessions.SessionMiddleware (cookie semnat, itsdangerous deja prezent), parole via hashlib.scrypt (stdlib).

2. Non-Goals (anti scope-creep)

  • Fara reset parola pe email / verificare email — MVP: parola setata la signup, schimbata doar prin admin (sau livrabila viitoare). Niciun SMTP.
  • Fara multi-user per cont in UI — schema permite (tabela users cu account_id), dar fluxul MVP creeaza un user → un cont nou. Invitare de colegi = viitor.
  • Fara roluri/permisiuni — un user vede integral contul lui.
  • Fara admin web — listarea/stergerea conturilor ramane CLI (3.1).
  • Fara OAuth/SSO.
  • Setarea creds RAR ramane prin fluxul existent (POST /v1/conturi/rar-creds); un buton web care il apeleaza e optional (US-007), nu inima livrabilei.

3. Stories atomice

US-001: Schema users + helper-e app/users.py (parole scrypt)

Ca dezvoltator vreau un model de utilizator cu parola hash-uita pentru ca autentificarea web are nevoie de o identitate legata de un cont, separat de cheia API si de creds RAR.

  • Depinde de: 3.1/US-001 (app/accounts.py)
  • Fisiere: app/schema.sql (tabela users), app/db.py (migrare _migrate), app/users.py (nou), tests/test_users.py (nou) (~4 fisiere)
  • Test intai (RED): tests/test_users.pytest_create_user_hash_nu_e_plaintext, test_verify_parola_corecta_si_gresita, test_email_unic_global, test_get_user_by_email
  • Acceptance criteria:
    • users(id, account_id FK, email TEXT UNIQUE COLLATE NOCASE, password_hash, salt, created_at) creata in schema.sql + migrata idempotent in _migrate (DB existenta nu se strica).
    • create_user(conn, account_id, email, password) -> int: stocheaza doar scrypt(salt+parola) + salt (per-user, secrets.token_bytes), niciodata parola in clar.
    • verify_password(conn, email, password) -> int | None: intoarce account_id la potrivire, None altfel (comparatie constant-time hmac.compare_digest).
    • email duplicat (case-insensitive) → ValueError.
    • get_user_by_email(conn, email) intoarce metadate fara hash/salt.
  • Verificare E2E: n/a (helper) — teste unitare.

US-002: Middleware sesiune + guard web

Ca dezvoltator vreau sesiuni cookie semnate + un helper de contul-curent pentru ca rutele web sa stie cine e logat si sa redirectioneze nelogatii.

  • Depinde de: US-001
  • Fisiere: app/config.py (session_secret), app/main.py (add_middleware), app/web/session.py (nou: current_account(request), require_login), tests/test_web_session.py (nou) (~4 fisiere)
  • Test intai (RED): tests/test_web_session.pytest_ruta_protejata_redirect_login, test_sesiune_seteaza_si_citeste_cont, test_logout_curata_sesiunea
  • Acceptance criteria:
    • AUTOPASS_session_secret in Settings (default efemer dev, persistent in prod, ca creds_key).
    • SessionMiddleware montat in main.py cu secretul; cookie httponly, samesite=lax.
    • current_account(request) -> int | None citeste request.session["account_id"].
    • require_login (dependency): nelogat → RedirectResponse('/login', 303).
  • Verificare E2E: GET / fara sesiune → redirect /login; cu sesiune setata → 200.

US-003: Signup web (GET/POST /signup)

Ca service nou vreau sa-mi creez cont din browser pentru ca sa incep fara admin.

  • Depinde de: US-001, US-002, 3.1/US-001
  • Fisiere: app/web/auth_routes.py (nou), app/web/templates/signup.html (nou), tests/test_web_signup.py (nou) (~3 fisiere)
  • Test intai (RED): tests/test_web_signup.pytest_signup_creeaza_cont_user_si_cheie, test_signup_email_duplicat_eroare, test_signup_parola_scurta_eroare, test_cheie_afisata_o_data
  • Acceptance criteria:
    • GET /signup randeaza formular (companie, CUI optional, email, parola).
    • POST /signup: create_account(active=False) (3.1) + create_user + create_api_key intr-o tranzactie; seteaza sesiunea; randeaza pagina cu cheia afisata o singura data + avertisment + mesaj clar „cont in asteptare — trimiterea incepe dupa activarea de catre admin".
    • Email duplicat → re-randare cu eroare, fara a crea cont orfan (tranzactie rollback).
    • Parola sub minim (10 caractere, §5) → eroare, fara creare.
  • Verificare E2E: browser pe /signup (Playwright MCP) → completare → vezi cheia rfak_... + banner „in asteptare"; python -m tools.apikey list arata cheia; sesiunea e activa.

US-004: Login / Logout web (GET/POST /login, POST /logout)

Ca utilizator existent vreau sa ma autentific pentru ca sa-mi accesez contul intre sesiuni.

  • Depinde de: US-001, US-002
  • Fisiere: app/web/auth_routes.py, app/web/templates/login.html (nou), tests/test_web_login.py (nou) (~3 fisiere)
  • Test intai (RED): tests/test_web_login.pytest_login_corect_seteaza_sesiune, test_login_gresit_401_fara_leak, test_logout_redirect_login
  • Acceptance criteria:
    • POST /login cu credentiale corecte → sesiune setata + redirect /.
    • Credentiale gresite → re-randare cu mesaj generic („email sau parola incorecte"), fara a dezvalui daca emailul exista.
    • POST /logoutsession.clear() + redirect /login.
  • Verificare E2E: browser /login → logare → dashboard; /logout → inapoi la /login.

US-005: Dashboard scoped pe sesiune (citiri)

Ca utilizator logat vreau ca dashboard-ul sa-mi arate doar contul meu pentru ca multi-tenant inseamna ca nu vad coada/banner-ul altui service.

  • Depinde de: US-002
  • Fisiere: app/web/routes.py (rutele GET dashboard + _fragments/*), tests/test_dashboard_scope.py (nou) (~2 fisiere)
  • Test intai (RED): tests/test_dashboard_scope.pytest_counts_doar_contul_sesiunii, test_submissions_fragment_scoped, test_nelogat_redirect
  • Acceptance criteria:
    • _status_counts, _fragments/banner, _fragments/submissions filtreaza pe current_account(request) (NULL→cont 1, ca in 3.2).
    • Rutele dashboard cer require_login (sau degradeaza la /login).
    • _fragments/nomenclator ramane global (referinta, ca in 3.2).
  • Verificare E2E: doi useri, doua conturi, prezentari diferite → fiecare vede doar coada lui.

US-006: Import web legat de sesiune (scriere)

Ca utilizator logat vreau ca importul sa intre pe contul meu pentru ca azi toate upload-urile aterizeaza pe contul 1, indiferent cine le face.

  • Depinde de: US-002
  • Fisiere: app/web/routes.py (rutele /_import/* + /mapari), tests/test_import_web_scope.py (nou) (~2 fisiere)
  • Test intai (RED): tests/test_import_web_scope.pytest_upload_pe_contul_sesiunii, test_commit_creeaza_submissions_pe_cont, test_batch_alt_cont_inaccesibil
  • Acceptance criteria:
    • Toate aparitiile account_id = DEFAULT_ACCOUNT_ID din rutele /_import/* + POST /mapari web → current_account(request).
    • Un batch al contului A nu e accesibil din sesiunea contului B (preview/confirma/mapare → eroare).
    • submissions/import_batches create primesc account_id-ul sesiunii.
  • Verificare E2E: import complet prin browser ca user B → submissions au account_id=B; ./start.sh test finalizate (cu send) sau dashboard-ul lui B le arata, al lui A nu.

US-007 (optional): Sectiune „Cheia mea API" + creds RAR in dashboard

Ca utilizator logat vreau sa-mi rotesc cheia si sa-mi setez creds RAR din UI pentru ca sa fiu complet self-service fara CLI.

  • Depinde de: US-002, US-005
  • Fisiere: app/web/routes.py, app/web/templates/_cont.html (nou), tests/test_web_cont.py (nou) (~3 fisiere)
  • Test intai (RED): tests/test_web_cont.pytest_roteste_cheie_afisata_o_data, test_set_creds_rar_din_sesiune
  • Acceptance criteria:
    • Buton „roteste cheia" → rotate_api_key(conn, current_account), cheie noua afisata o data.
    • Formular creds RAR → reuseaza logica POST /v1/conturi/rar-creds pe contul sesiunii.
  • Verificare E2E: rotire din UI → cheia veche respinsa pe /v1/*, cea noua acceptata.

US-008: Gate de trimitere pentru conturi „in asteptare"

Ca operator RAR vreau ca prezentarile unui cont neactivat sa nu ajunga la RAR pentru ca self-signup nu trebuie sa permita trimiteri reale inainte de validarea de catre admin.

  • Depinde de: 3.1/US-001 (accounts.active)
  • Fisiere: app/worker/__main__.py (claim), app/web/templates/_banner.html (+ routes.py), tests/test_worker_active_gate.py (nou) (~3 fisiere)
  • Test intai (RED): tests/test_worker_active_gate.pytest_claim_sare_cont_inactiv, test_claim_ia_cont_activ, test_activare_deblocheaza_trimiterea
  • Acceptance criteria:
    • Claim-ul worker-ului nu ridica submission-uri al caror cont are active=0 (raman queued, fara retry/eroare — pur si simplu nealese pana la activare).
    • Dupa set_active(.., True), la urmatorul poll submission-urile devin eligibile, fara re-enqueue.
    • Dashboard-ul contului in asteptare arata un banner „cont in asteptare — trimiterea e oprita".
    • Conturile fara active explicit (legacy) sunt tratate ca active (NULL/absent → activ).
  • Verificare E2E: signup → import + confirma → submission queued ramane netrimis cat timp contul e inactiv; tools/account.py activate → worker il trimite → FINALIZATA la RAR test.

4. Riscuri

  • Signup deschis = abuz — oricine isi face cont. Pentru un gateway B2B legat de RAR, asta poate fi nedorit. Mitigare posibila: cod de invitatie / aprobare admin / rate-limit. Decizie §5 — blocant.
  • Suprafata de securitate noua — canalul web trece de la „deschis" la „autentificat"; orice ruta web nescoped ramasa = leak. Mitigare: US-005+US-006 acopera toate rutele; VERIFY enumera explicit fiecare ruta app/web/routes.py si confirma scoping (grep DEFAULT_ACCOUNT_ID = 0 rezultate la final).
  • Secret de sesiune — daca AUTOPASS_session_secret e efemer in prod, cookie-urile se invalideaza la restart. Mitigare: documentat ca persistent in .env, ca creds_key.
  • Atomicitate US-006routes.py e mare; riscul e sa ramana un DEFAULT_ACCOUNT_ID. Mitigare: test care face grep in sursa + verificare comportamentala per ruta.
  • Interactiune cu 3.2 — daca 3.2 nu e inca livrat, scoping-ul web (US-005/006) introduce pattern-ul separat; e ok (web nu trece prin resolve_account_id), dar mentinem aceeasi regula NULL→cont 1.

5. Intrebari deschise

Primele doua REZOLVATE la poarta de aprobare; restul = propuneri implicite (confirmabile la review).

  1. [REZOLVAT] Cine se poate inregistra? Signup creeaza cont „in asteptare" (active=0) pe care adminul il activeaza inainte de prima trimitere (optiunea (c)). Vezi US-003 + US-008.
  2. [REZOLVAT] Useri per cont? Schema users cu account_id (permite multi-user viitor); fluxul MVP = un user creeaza un cont nou, login intra pe contul lui.
  3. Politica de parola — minim 10 caractere, fara alte reguli (lungime > complexitate). (implicit)
  4. Persistenta sesiunii — cookie semnat (SessionMiddleware), suficient pentru MVP. (implicit)
  5. Contul default (id=1) — ramane „contul dev fara login", neatins; userii reali primesc id≥2. Dashboard fara sesiune in dev (require_api_key=false) → cont 1, ca azi. (implicit)
  6. US-007 in scope? Optional; se poate taia pentru o livrabila mai mica. (implicit: il pastram)

6. Valuri de executie (graful de dependente)

Val 1: [US-001]                         ← schema + helper users (depinde de 3.1/US-001 livrat)
Val 2: [US-002]                         ← sesiune + guard (peste users)
Val 3: [US-003, US-004, US-005, US-006, US-008] ← deblocate de US-002 (US-008 doar de 3.1/US-001).
                                          US-005 si US-006 ating ambele routes.py (FISIER COMUN) →
                                          nu paralel intre ele. US-003/US-004 (auth_routes.py),
                                          US-008 (worker) ating fisiere distincte → pot merge paralel.
Val 4: [US-007]                         ← optional, peste US-005

Nota de paralelizare: US-005 + US-006 modifica acelasi app/web/routes.py → secvential sau worktree+merge (§5.5). Un grupaj sigur: teammate A = US-003+US-004 (auth_routes.py), teammate B = US-005 apoi US-006 (routes.py), serializat. Lead-ul ruleaza regresia dupa fiecare.


Addendum review (autoplan, [subagent-only] — Codex indisponibil: usage limit)

Doua voci Claude independente (Eng + Produs/Design/DX). Securitate notata 4/10 — cea mai slaba dimensiune, fiindca livrabila introduce suprafata cookie-auth. Doua blocante critice. Schimbari obligatorii la executie; trei decizii de scope ridicate la poarta.

Blocante (rezolvabile inainte de executie)

C1 [CRITICAL, Eng] — Dependinta 3.1 e fantoma in repo. app/accounts.py, tools/account.py, create_account, set_active nu exista azi; accounts.active e in schema.sql dar NU in _migrate → orice DB veche da OperationalError: no such column la US-008. Fix: 3.1 e prerechizita HARD; la poarta de executie a lui 3.3 verifica livrarea 3.1 (inclusiv migrarea active in _migrate). Nu porni 3.3 fara 3.1 inchis.

C2 [CRITICAL, ambele voci] — CSRF complet absent → STORY NOU US-009. Dupa 3.3 auth web e exclusiv cookie de sesiune; fiecare POST de stare (/signup, /login, /logout, /_import/*, /mapari, rotire cheie) devine vulnerabil CSRF — regresie introdusa DE aceasta livrabila. SameSite=Lax (US-002) NU acopera POST-uri top-level. US-009: token CSRF in sesiune + camp ascuns in toate formularele + validare server-side; SameSite=Strict pe cookie-ul de sesiune ca aparare in adancime. Depinde de US-002; blocheaza US-003/004/006b/007.

Securitate (obligatoriu in stories existente)

C3 [HIGH, Eng] — Fixare sesiune: la login (US-004) request.session.clear() INAINTE de a seta account_id (+ nonce nou). AC nou.

C4 [MEDIUM, Eng] — Cookie secure/https_only: US-002 adauga https_only=True (config-abil, default on in prod; in spatele Cloudflare Tunnel e TLS). Plus samesite aliniat cu C2.

C5 [MEDIUM, ambele — rate-limit signup] → in US-009. active=0 opreste trimiterea, NU crearea nelimitata de conturi/useri/chei (DoS storage + zgomot admin). Adauga rate-limit pe IP la POST /signup (in-proces, fara dependinta noua). Reformuleaza §5.1: nu "REZOLVAT" ci "DECIS: cont in asteptare + activare admin; risc de recon acceptat pentru MVP + rate-limit".

Atomicitate scoping (redefineste "done")

C6 [CRITICAL, Eng] — Testul grep DEFAULT_ACCOUNT_ID = 0 e fals-linistitor. Rutele GET periculoase NU contin acel literal: _status_counts (l.59), fragment_submissions (l.146-149, SELECT ... FROM submissions fara WHERE account_id), fragment_banner. Grep=0 ar declara "done" in timp ce dashboard-ul lui B arata coada lui A. Fix US-005: adauga account_id + WHERE account_id (clauza scope din 3.2/B2) la _status_counts/fragment_submissions/ fragment_banner. "Done" = comportamental (test pe 2 conturi), grep ramane doar smoke.

C7 [HIGH, Eng] — Sparge US-006 in US-006a (citiri: upload/preview/reset/mapare-coloane) + US-006b (scrieri: confirma ~250 linii cu tranzactie/atestare + POST /mapari). Un account_id ramas in confirma → submissions pe contul gresit, irecuperabil dupa send (FINALIZATA terminal). 006b primeste E2E dedicat pe 2 conturi.

C8 [HIGH, Eng — OV-2] — Propagare consecventa current_account. build_key / _build_idempotency_key / _already_sent_lookup / _web_compute_preview trebuie sa primeasca ACELASI account_id (sesiunea). Test: un rand importat web sub cont N primeste aceeasi cheie ca prin API sub cont N (altfel reapare OV-2). AC in US-006b.

Corectitudine helper-e / model

C9 [MEDIUM, ambele] — Parametri scrypt ficsi (US-001): hashlib.scrypt(password=parola, salt=salt, n=2**14, r=8, p=1, maxmem=64*1024*1024, dklen=32)salt/parola argumente separate (NU concatenate). Stocheaza si o eticheta de versiune a parametrilor (migrare cost viitoare). Plafon lungime parola (ex. 128 char) — scrypt pe 1MB = DoS.

C10 [HIGH, Eng] — Tranzactie signup atomica (US-003): BEGIN IMMEDIATEcreate_account (active=False) → create_user (poate ridica pe email UNIQUE AICI) → create_api_key → COMMIT; orice exceptie → ROLLBACK. Test test_signup_email_duplicat asereaza COUNT(accounts) neschimbat (fara cont orfan). DB e autocommit → tranzactia trebuie explicita.

C11 [MEDIUM, Eng] — require_login ca dependency care intoarce RedirectResponse NU scurtcircuiteaza handler-ul. Trebuie sa RIDICE o exceptie prinsa de un handler care redirectioneaza (sau middleware pe prefixele web). Clarifica mecanismul in US-002.

C12 [MEDIUM, Eng] — require_login vs cont-1-in-dev. §5.5 zice "dev fara sesiune → cont 1" dar US-005 cere require_login. Bypass cand require_api_key=false (sau flag web_auth_required), cazand pe cont 1. Specifica conditia exacta. Pe scrieri web current_account is None → redirect login, NICIODATA fallback account_or_default (leak silentios pe cont 1).

C13 [HIGH, Eng] — US-007 vs require_api_key. POST /v1/conturi/rar-creds cere cheie API; un user web logat fara cheie in header → 401 in prod. US-007 are nevoie de ruta web proprie scoped pe sesiune (nu reuseste endpoint-ul API), SAU un dependency hibrid "sesiune SAU cheie".

Gate worker (US-008)

C14 [MEDIUM, Eng] — Query claim cu LEFT JOIN + dublu-NULL. LEFT JOIN accounts a ON a.id=s.account_id WHERE COALESCE(a.active,1)=1. Atentie la DOUA NULL-uri: (a) cont legacy fara activeCOALESCE; (b) submissions.account_id IS NULL (FK ON DELETE SET NULL) → ar fi blocat pe veci cu JOIN intern; LEFT JOIN + s.account_id IS NULL = tratat ca default/activ. Specifica query-ul exact in AC.

UX / produs (Produs voce)

C15 [HIGH] — Pagina "cheia o data" (US-003) subspecificata. AC noi: buton copy-to-clipboard + confirmare; gate "am salvat cheia" inainte de CTA dashboard; comportament la refresh (POST→render, cheia NU e in sesiune → refresh = pierdere; recuperare doar prin rotire US-007 / CLI admin) — scris explicit in UI.

C16 [HIGH] — Activarea admin nu are mecanism de descoperire/notificare. Fara admin web, adminul ar rula tools/account.py list din proprie initiativa. Fix: la signup, linie de log dedicata (SIGNUP cont=N email=...) + tools/account.py list --pending (din addendum 3.1/A6). Dashboard-ul contului arata starea explicit ("in asteptare de la {created_at}"). Documenteaza procesul+SLA de activare in PRD (vezi user challenge UC-2 la poarta).

C17 [MEDIUM] — Reframe pozitiv banner "in asteptare" + stari HTMX: submit dezactivat + indicator pe POST (signup face 3 operatii); erorile re-randeaza formularul cu valorile pastrate (Mai putin parola — confirma ca nu re-pui parola in value=). Banner: "Contul e creat. Configureaza creds RAR si pregateste importul ACUM; trimiterea catre RAR porneste automat dupa activare."

C18 [MEDIUM] — Stari/erori lipsa: login pe cont active=0 intra (gate-ul e doar pe trimitere — confirma explicit); cont activat intre timp → banner dispare (clarifica ca e alt banner decat cel de submissions blocate); email normalizat (trim + COLLATE NOCASE); sesiune cu account_id pe cont sters → redirect login, nu query gol tacut.

C19 [MEDIUM, P2 ieftin — pregatire viitor]: users.email_verified INTEGER DEFAULT 0 de la inceput (evita migrare dureroasa cand apare verificarea email). Pune si user_id in sesiune (nu doar account_id) — leaga import_attestations.confirmed_by de user mai tarziu fara migrare.

Decizii ridicate la poarta (taste + user challenges)

  • UC-1 [user challenge, ambele voci] — NU lansa signup fara rate-limit + NU marca §5.1 "REZOLVAT". (Auto-adaugat in C5/US-009; confirma la poarta.)
  • UC-2 [user challenge, Produs] — Defineste PROCESUL de activare admin (descoperire + SLA) inainte de aprobare, nu ca detaliu de implementare. Acesta e arcul care va genera cele mai multe tichete. Intrebare de produs pentru tine.
  • UC-3 [user challenge, Produs]Promoveaza US-007 din "optional" in scope (cel putin rotire cheie + formular creds RAR). Fara el livrabila nu-si tine numele "self-onboarding": userul e dependent de admin/CLI exact pentru pasii self-service. Decizie de scope pentru tine.
  • Taste: scrypt stdlib vs argon2id (zero-dependinta vs purist securitate); gate-activare vs auto-activ+monitorizare reactiva.

Graf revizuit (stories adaugate/sparte)

Val 1: [US-001]                                  (peste 3.1 livrat — C1)
Val 2: [US-002] → [US-009 CSRF+rate-limit, C2/C5]
Val 3: [US-003, US-004, US-005, US-006a, US-006b, US-008]
        US-005 + US-006a/b ating routes.py (comun) → secvential; US-003/004 (auth_routes.py),
        US-008 (worker), US-009 (cross-cutting) — fisiere distincte, paralelizabile cu grija.
Val 4: [US-007]  ← promovat la poarta (UC-3): rotire cheie + creds RAR web (ruta proprie, C13)

Decizii poarta (pliate — aprobat de utilizator)

Acestea EXTIND scope-ul fata de draftul initial. Confirma graful revizuit la inceputul EXECUTE.

G1 — US-007 PROMOVAT in MVP (nu mai e optional). Rotire cheie (recuperare cheie pierduta) + formular creds RAR pe ruta web proprie scoped pe sesiune (C13, nu reuseste endpoint-ul API care cere cheie). Devine obligatoriu pentru ca livrabila sa fie cu adevarat self-service.

G2 — INTERFATA WEB DE ADMIN ceruta explicit ("chiar vreau interfata admin"). Inverseaza Non-Goal-ul "fara suprafata HTTP de admin" (3.1). Stories noi:

  • US-010: Rol admin + bootstrap. users.is_admin INTEGER DEFAULT 0 (migrare). Primul cont (sau marcat via tools/account.py set-admin --account N) e admin. Guard require_admin (peste require_login + CSRF). Test: non-admin pe ruta admin → 403/redirect.

    • Depinde de: US-001, US-002, US-009 (CSRF).
    • Intrebare deschisa (vezi §5): cum se bootstrapeaza primul admin?
  • US-011: Panou admin web /admin — conturi in asteptare + activare. Listeaza conturile active=0 (email, companie, CUI, created_at) + buton "activeaza" (set_active(.., True) din 3.1, POST cu CSRF). Optional: dezactivare, vedere chei (fara hash). Template admin.html.

    • Depinde de: US-010, 3.1/US-001 (set_active).
    • E2E: signup (cont nou inactiv) → admin se logheaza → vede contul in /admin → activeaza → worker il trimite → FINALIZATA la RAR test.

US-012: Notificare email admin la signup. La POST /signup, trimite email catre admin(i) ("cont nou N in asteptare"). Adauga dependinta SMTP (config nou AUTOPASS_smtp_*) — reverseaza Non-Goal-ul "niciun SMTP". Esecul trimiterii email NU blocheaza signup-ul (best-effort + log).

  • Depinde de: US-003, US-010 (sa stie cui trimite).
  • Intrebare deschisa: provider SMTP / adresa expeditor / fallback daca SMTP pica.

Mecanismul de descoperire e acum redundant-sigur: log SIGNUP (C16) + list --pending (CLI) + email (US-012) + panou /admin (US-011). C16 ramane ca baseline daca SMTP/panoul intarzie.

Graf revizuit final:

Val 1: [US-001]
Val 2: [US-002] → [US-009 CSRF+rate-limit]
Val 3: [US-003, US-004, US-005, US-006a, US-006b, US-008, US-010]
Val 4: [US-007, US-011 admin panou, US-012 email]   (US-007/011/012 peste US-010/US-009)

Scope mai mare decat draftul (7 → 12 stories). 3.3 e acum substantiala; daca devine prea grea pentru o singura faza EXECUTE, lead-ul o poate sparge in sub-livrabile la inceputul executiei (ex. 3.3a self-onboarding core, 3.3b admin web + email) — decizie de orchestrare, nu de scope.

Intrebari deschise nou aparute (rezolva la inceputul EXECUTE)

  • Bootstrap admin (US-010): primul cont devine automat admin, SAU adminul se marcheaza manual via tools/account.py set-admin --account N? Propunere: manual via CLI (explicit, fara magie "primul user"); contul default id=1 nu e admin automat.
  • SMTP (US-012): ce provider/expeditor? Daca nu exista SMTP la momentul executiei, US-012 se livreaza degradat (doar log + /admin + list --pending) si email-ul devine follow-up.

Progres executie 3.3a (lead)

Sub-livrabila 3.3a (self-onboarding core). Toate stories GREEN, regresie 355 pass (de la 313 baseline).

  • US-001 — schema users (+ email_verified, is_admin pregatire 3.3b) + app/users.py (scrypt n=2^14, plafon 128 char, hmac.compare_digest). 6 teste.
  • US-002SessionMiddleware (same_site=strict, https_only config) + app/web/session.py (current_account/current_user_id/web_account/require_loginLoginRequired/set_session clear-inainte C3). 6 teste.
  • US-009app/web/csrf.py (token per-sesiune, verify_csrf gateat pe MOD: prod sau sesiune autentificata) + app/web/ratelimit.py (fereastra glisanta in-proces) + handler CsrfError→403. Gate-ul inchide login/signup CSRF in prod (C2). 8 teste.
  • US-003GET/POST /signup (auth_routes.py + signup.html): tranzactie atomica C10, log SIGNUP C16, cheie-o-data + gate C15, banner pozitiv C17. 4 teste.
  • US-004GET/POST /login + POST /logout: mesaj generic la esec, login pe active=0 intra (C18), clear sesiune C3. 4 teste.
  • US-005 — dashboard scoped pe sesiune (_status_counts/fragment_submissions/fragment_banner cu regula NULL→1, C6) + banner "cont in asteptare" (US-008 AC#3, reframe C17). nomenclator ramane global. 4 teste.
  • US-006a — citiri import (upload/mapare-coloane/preview) pe web_account(request); batch cross-cont inaccesibil.
  • US-006b — scrieri (confirma + /mapari) pe sesiune; propagare consecventa account_id la build_key (C8/OV-2); verify_csrf + camp ascuns pe toate formularele; zero atribuiri DEFAULT_ACCOUNT_ID in handlere (C6). 5 teste.
  • US-008 — gate worker claim_one cu LEFT JOIN accounts ... COALESCE(active,1)=1 (dublu-NULL C14): cont inactiv → submission ramane queued; activare → eligibil fara re-enqueue. 5 teste.

3.3b (US-007 self-service cheie/creds + US-010/011/012 admin web + email) ramane livrabila separata.

Raport VERIFY (3.3a)

Doua runde de verificare independenta (subagent context curat, §5.6).

Runda 1 — FAIL (1 criteriu): suita 355 pass, dar sweep-ul anti-leak a gasit GET /_fragments/mapari nescoped: pending_unmapped(conn) fara account_id + fara require_login → expune cod_op_service/ denumire cross-account (Risc #2/C6). Specul US-005 enumerase doar _status_counts/fragment_submissions/ fragment_banner si omisese acest fragment. → inapoi la EXECUTE (task fix).

Fix (task #7): fragment_maparirequire_login(request); _render_mapari(account_id)pending_unmapped(conn, account_id); post_mapare paseaza consecvent contul sesiunii. 2 teste noi de izolare pe 2 conturi (tests/test_mapari_scope.py).

Runda 2 — PASS global (subagent NOU):

  • Suita: 357 passed, 0 fail.
  • Sweep anti-leak complet (toate rutele routes.py + auth_routes.py): fiecare ruta care atinge submissions/import_batches/column_mappings/operations_mapping e sub require_login SI scoped pe contul sesiunii. Publice intentionat: /signup, /login, /logout, /_fragments/nomenclator (global), /_import/reset (template gol, fara DB). fragment_mapari fix confirmat.
  • Criterii securitate critice re-verificate in cod: CSRF enforce in prod pe /login+/signup fara account_id (US-009/C2); signup tranzactie atomica cu ROLLBACK pe email duplicat, fara cont orfan (US-003/C10); claim_one COALESCE(a.active,1)=1 cu LEFT JOIN, account_id NULL=activ (US-008/C14); parola scrypt, niciodata in clar (US-001).
  • E2E HTTP mod prod (web_auth_required=true): GET /_fragments/mapari fara cookie → 303 /login; signup → cheie rfak_ o data + cont active=0 + log SIGNUP cont=N; cu sesiune → 200 doar contul propriu.
  • Regresia de aur: test_import_e2e.py + test_api.py = 26 pass. Send live RAR neverificat (fara creds/retea in mediul de VERIFY), dar acoperit de teste.

Verdict: PASS. Send live la RAR test ramane de confirmat manual la deploy (canal API + import → FINALIZATA).

Code-review (CLOSE, /code-review high) — 3 findings reparate

  • [HIGH] csrf_token lipsa pe re-randarile de eroare (routes.py): ramurile de eroare din web_upload_import/web_save_mapare_coloane/web_confirma_import randau formularul fara csrf_token → in prod (user logat, CSRF enforce) campul ascuns gol → urmatorul submit 403 (lockout dupa orice eroare). Fix: helper _ctx(request, **extra) care include mereu csrf_token + conversia tuturor ramurilor; require_login reordonat inaintea verify_csrf. Test nou de regresie in mod prod.
  • [MEDIUM] verify_password ignora scrypt_params stocat (users.py): folosea constantele curente, anuland migrarea de cost (C9) — un bump viitor de n ar fi blocat toti userii existenti. Fix: _parse_scrypt_params + verify cu parametrii din DB (eticheta corupta → None, fara crash). Test de migrare cost.
  • [MEDIUM] login fara rate-limit (auth_routes.py): brute-force parole + DoS CPU (scrypt/cerere). Fix: check_rate_limit("login:"+ip, login_rate_max=10, ...) → 429. (Extinde C5 dincolo de signup.)

Suita finala: 361 passed, 0 fail. Findings low/by-design neactionate (documentate): dev-fallback cont 1 cand web_auth_required=False (C12, intentionat — atentie ops la deploy prod), 500 rar la DB-locked in signup, request.client is None → bucket rate-limit 'unknown' partajat.