# PRD 3.3 — Self-onboarding web (login email+parola → emite cheie) **Stare**: inchis (3.3a + 3.3b livrate) > **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.py` — `test_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.py` — `test_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.py` — `test_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.py` — `test_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 /logout` → `session.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.py` — `test_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.py` — `test_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.py` — `test_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.py` — `test_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-006** — `routes.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 IMMEDIATE` → `create_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 `active` → `COALESCE`; (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). - [x] **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. - [x] **US-002** — `SessionMiddleware` (same_site=strict, https_only config) + `app/web/session.py` (`current_account`/`current_user_id`/`web_account`/`require_login`→`LoginRequired`/`set_session` clear-inainte C3). 6 teste. - [x] **US-009** — `app/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. - [x] **US-003** — `GET/POST /signup` (`auth_routes.py` + `signup.html`): tranzactie atomica C10, log `SIGNUP` C16, cheie-o-data + gate C15, banner pozitiv C17. 4 teste. - [x] **US-004** — `GET/POST /login` + `POST /logout`: mesaj generic la esec, login pe `active=0` intra (C18), clear sesiune C3. 4 teste. - [x] **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. - [x] **US-006a** — citiri import (`upload`/`mapare-coloane`/`preview`) pe `web_account(request)`; batch cross-cont inaccesibil. - [x] **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. - [x] **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_mapari` → `require_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. ## Progres executie 3.3b (lead) > Sub-livrabila 3.3b (self-service cheie/creds + admin web + email). Decizii confirmate la poarta: > primul cont care se inregistreaza devine admin (bootstrap automat); US-012 livrare DEGRADATA fara > SMTP (helper `app/email.py` best-effort no-op + log `SIGNUP` deja existent din 3.3a). Valuri (fisiere disjuncte intre stories paralele): - **Val 1:** US-010 (`users.py`/`session.py`/`tools/account.py`/`main.py` handler) ‖ US-007 (`routes.py`/`_cont.html`/`dashboard.html`) - **Val 2:** US-011 (`admin_routes.py` nou/`admin.html`/`main.py` register) ‖ US-012 (`email.py` nou/`config.py`/`auth_routes.py`) - [x] **US-010** — rol admin (`is_admin`) + helper-e (`count_admins`/`set_admin`/`is_account_admin`/`list_admin_emails`) + `require_admin`→`AdminRequired`→403 + CLI `set-admin`. 13 teste. Bootstrap (primul cont=admin) cablat in signup de US-012 (evita conflict pe auth_routes.py). - [x] **US-007** — sectiune "Contul meu" (`/_fragments/cont`): rotire cheie (afisata o data) + creds RAR pe ruta web proprie scoped pe sesiune (`POST /cont/roteste-cheie`, `POST /cont/rar-creds`, C13, NU endpointul API). 5 teste. - [x] **US-011** — panou `/admin` (`admin_routes.py`): conturi in asteptare/active + activare/dezactivare (require_admin + CSRF + PRG); contul dev id=1 fara butoane. Link "Panou admin" pe dashboard doar pentru admini + buton logout. 5 + 2 teste. - [x] **US-012** — `app/email.py` `notify_signup` best-effort (no-op fara `smtp_host`, prinde orice exceptie SMTP, timeout 5s) + config `smtp_*` + cablaj signup: bootstrap admin (primul cont = admin via `count_admins==0`) + notificare degradata dupa `set_session`. 5 teste. - [x] **Fix migrare (din VERIFY r1):** `_migrate` adauga defensiv `users.is_admin`/`email_verified` pe DB cu tabela `users` fara ele (idempotent, guard pe existenta tabelei). 2 teste. ## Raport VERIFY (3.3b) > Doua runde de verificare independenta (subagent context curat, §5.6). **Runda 1 — FAIL (1 criteriu):** suita 391 pass, toate criteriile US-007/010/011/012 confirmate + sweep securitate complet (toate rutele noi `/cont/*`, `/_fragments/cont`, `/admin*` sub `require_login`/`require_admin` + `verify_csrf` pe POST; `/cont/*` scoped strict pe sesiune, nu accepta `account_id` din form; `/admin` nu expune hash/chei/creds in clar). DAR `_migrate` nu adauga defensiv `users.is_admin`/`email_verified` → o tabela `users` fara ele ar ceda cu `OperationalError` (acelasi tip de gap ca C1 pe `accounts.active`). → fix. **Fix:** bloc `# Coloane users` in `app/db.py::_migrate` (guard pe existenta tabelei + ALTER idempotent). 2 teste. **Runda 2 — PASS global (subagent NOU):** - Suita: **393 passed**, 0 fail. - Fix migrare confirmat (test pe `users` minima fara coloane → `_migrate` → coloane prezente; idempotent). - E2E mod prod (`web_auth_required=true`): `GET /admin` fara cookie → 303 `/login`; non-admin logat → 403; `POST /admin/activate` fara CSRF → 403. Rute `/cont/*` scoped pe sesiune, CSRF enforce, parola RAR niciodata in `value=`. - US-010 bootstrap (primul signup → `is_admin=1`, al doilea → 0), CLI `set-admin`, `require_admin`→403 confirmate. - US-012 `notify_signup` best-effort no-op fara SMTP + nu blocheaza signup + log `SIGNUP` pastrat. - Regresia de aur: `test_import_e2e` + `test_api` + `test_worker_active_gate` = 31 pass. **Verdict: PASS.** Send live RAR ramane de confirmat manual la deploy (fara creds/retea in mediul VERIFY). La deploy prod: `AUTOPASS_session_secret` persistent, `AUTOPASS_WEB_AUTH_REQUIRED=true`, optional `AUTOPASS_smtp_*`.