From 6515de415b4903f47352fb188ed9ca17b8811013 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Wed, 17 Jun 2026 12:22:28 +0000 Subject: [PATCH] docs(prd): PRD-uri Etapa 3 (3.1/3.2/3.3) aprobate dupa autoplan Faza PLAN pentru multi-cont / self-onboarding. Trei PRD-uri scrise, ancorate in cod, trecute prin autoplan (voci Claude independente; Codex degradat pe usage-limit) si aprobate la poarta umana. - 3.1 creare cont: CLI tools/account.py + accounts.active; CUI unic prin index partial - 3.2 filtrare GET pe cont: scope pe cheie, allowlist campuri, nomenclator global - 3.3 self-onboarding web: sesiuni + cont 'in asteptare' + CSRF + interfata admin web + email; US-007 promovat in MVP (7->12 stories) Dashboard ROADMAP actualizat (stare 'PRD aprobat', linkuri PRD). Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/ROADMAP.md | 8 +- docs/prd/prd-3.1-creare-cont.md | 152 +++++++++ docs/prd/prd-3.2-filtrare-cont-get.md | 174 ++++++++++ docs/prd/prd-3.3-self-onboarding-web.md | 413 ++++++++++++++++++++++++ 4 files changed, 743 insertions(+), 4 deletions(-) create mode 100644 docs/prd/prd-3.1-creare-cont.md create mode 100644 docs/prd/prd-3.2-filtrare-cont-get.md create mode 100644 docs/prd/prd-3.3-self-onboarding-web.md diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 213c2ac..ad0dca0 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -48,7 +48,7 @@ Reguli de contract (detalii in `docs/api-rar-contract.md`): `FINALIZATA` e termi > PRD-uri (`docs/prd/prd-X.Y-*.md`), linkate in coloana Detalii. La fiecare livrabila terminata: > schimba statusul + data + linkul PRD si actualizeaza "Ultima actualizare". -**Ultima actualizare**: 2026-06-17 — Treapta 1 + Treapta 2 LIVE pe test; urmatorul focus = Etapa 3 (multi-cont). +**Ultima actualizare**: 2026-06-17 — Etapa 3 PLANIFICATA: 3 PRD-uri scrise + autoplan (`[subagent-only]`, Codex usage-limit) + aprobate. Urmeaza EXECUTE, incepand cu 3.1. 3.3 a crescut (7→12 stories: +CSRF, +interfata admin web, +email). ### Etapa 1 — Canal API ROAAUTO (Treapta 1) @@ -72,9 +72,9 @@ Reguli de contract (detalii in `docs/api-rar-contract.md`): `FINALIZATA` e termi | # | Livrabila | Status | Data | Detalii | |---|-----------|--------|------|---------| -| 3.1 | Creare cont nou (endpoint/tool dedicat) | TODO | | Azi doar prin `INSERT` SQL manual | -| 3.2 | Filtrare pe cont a GET-urilor de listare | TODO | | `GET /v1/prezentari`, `/v1/nomenclator`, `/v1/audit/export` sunt globale acum | -| 3.3 | Self-onboarding web (login email+parola → emite cheie) | TODO | | Nu exista ruta `/login`; cheile se emit programatic (`app/auth.py`) | +| 3.1 | Creare cont nou (CLI dedicat) | TODO (PRD aprobat) | | CLI `tools/account.py` + `accounts.active`. PRD: [prd-3.1](prd/prd-3.1-creare-cont.md) | +| 3.2 | Filtrare pe cont a GET-urilor de listare | TODO (PRD aprobat) | | scope cheie pe `/v1/prezentari`, `/v1/mapari`, `/v1/audit/export`; nomenclator global. PRD: [prd-3.2](prd/prd-3.2-filtrare-cont-get.md) | +| 3.3 | Self-onboarding web + interfata admin | TODO (PRD aprobat) | | signup/login/sesiuni + cont "in asteptare" + gate worker + CSRF + panou admin web + email. 12 stories. PRD: [prd-3.3](prd/prd-3.3-self-onboarding-web.md) | ### Etapa 4 — Viitor (Treapta 3) diff --git a/docs/prd/prd-3.1-creare-cont.md b/docs/prd/prd-3.1-creare-cont.md new file mode 100644 index 0000000..5fd395e --- /dev/null +++ b/docs/prd/prd-3.1-creare-cont.md @@ -0,0 +1,152 @@ +# PRD 3.1 — Creare cont nou + +**Stare**: aprobat + +> 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). + +## 1. Obiectiv + +Inlocuieste crearea conturilor prin `INSERT` SQL manual cu un tool admin dedicat +(`tools/account.py`), simetric cu `tools/apikey.py`. Este **fundatia Etapei 3**: 3.2 +(filtrare GET pe cont) si 3.3 (self-onboarding web) au nevoie de o cale curata si testata +de a materializa conturi. Pastram filozofia din `app/auth.py`: lifecycle-ul de admin +traieste in CLI pe masina gateway, **fara suprafata HTTP de admin**. + +## 2. Non-Goals (anti scope-creep) + +- **Fara endpoint HTTP** de creare cont in 3.1. (Nota: la poarta de aprobare utilizatorul a cerut o + **interfata web de admin** pentru activarea conturilor — vezi 3.3; aceasta inverseaza stanta + initiala "fara suprafata HTTP de admin". Admin web traieste in 3.3, nu aici. CLI-ul 3.1 ramane + pentru bootstrap/suport.) +- **Fara autentificare de utilizator** (email/parola, sesiuni) — apartine 3.3. Aici contul are + doar `name` + `cui`, ca azi. +- **Fara setare creds RAR** in acest tool — exista deja `POST /v1/conturi/rar-creds` (T1). +- **Fara coloane de user/parola** — identitatea de login (email/parola) vine in 3.3. Aici adaugam + doar `accounts.active` (lifecycle de cont), de care depinde gate-ul „cont in asteptare" din 3.3. +- **Fara stergere cont** — momentan nu e nevoie; conturile sunt putine si durabile. (Dezactivarea + exista, via `active`.) + +## 3. Stories atomice + +### US-001: Coloana `accounts.active` + helper-e cont in `app/accounts.py` +**Ca** dezvoltator **vreau** functii pure de creare/listare/(de)activare cont **pentru ca** atat +CLI-ul (3.1) cat si fluxul web (3.3) sa materializeze si sa activeze conturi prin aceeasi cale testata. + +- **Depinde de**: — +- **Fisiere**: `app/schema.sql` (`accounts.active`), `app/db.py` (migrare `_migrate`), + `app/accounts.py` (nou), `tests/test_accounts.py` (nou) (~4 fisiere) +- **Test intai (RED)**: `tests/test_accounts.py` — `test_create_account_returneaza_id`, + `test_create_account_activ_implicit`, `test_create_account_name_gol_ridica_eroare`, + `test_create_account_cui_duplicat_respins`, `test_set_active_comuta`, `test_list_accounts_ordonat` +- **Acceptance criteria**: + - [ ] `accounts.active INTEGER NOT NULL DEFAULT 1` adaugata in `schema.sql` + migrata idempotent in + `_migrate` (conturi existente raman active; default id=1 activ). + - [ ] `create_account(conn, name, cui=None, active=True) -> int` insereaza si intoarce `id`-ul nou. + - [ ] `name` gol/whitespace → `ValueError` (nu insereaza). + - [ ] `cui` ne-nul duplicat → `ValueError` (un CUI = un cont — decizie confirmata §5). + `cui=None` se accepta multiplu (conturi fara CUI, ex. default). + - [ ] `set_active(conn, account_id, active: bool)` comuta starea; cont inexistent → `ValueError`. + - [ ] `list_accounts(conn) -> list[dict]` intoarce `id, name, cui, active, created_at`, ordonat dupa + `id`, **fara** `rar_creds_enc`. +- **Verificare E2E**: n/a (helper pur) — acoperit de teste unitare. + +### US-002: CLI `tools/account.py` (create/list/activate/deactivate) +**Ca** admin gateway **vreau** `python -m tools.account create|list|activate|deactivate` **pentru ca** +sa onboardez si sa **activez** un client fara SQL manual, optional emitand prima cheie API intr-un pas. + +- **Depinde de**: US-001 +- **Fisiere**: `tools/account.py` (nou), `tests/test_tools_account.py` (nou) (~2 fisiere) +- **Test intai (RED)**: `tests/test_tools_account.py` — `test_create_afiseaza_id`, + `test_create_with_key_emite_cheie`, `test_create_cui_duplicat_exit_2`, + `test_activate_comuta_starea`, `test_list_afiseaza_activ` +- **Acceptance criteria**: + - [ ] `create --name "Service X" [--cui RO123] [--inactive]` creeaza contul (implicit activ) si + tipareste `id`-ul; `--inactive` creeaza cont in asteptare. + - [ ] `--with-key` emite si o cheie API (`app.auth.create_api_key`), afisata **o singura data** + (reuseaza pattern-ul din `tools/apikey.py`). + - [ ] `activate --id N` / `deactivate --id N` comuta `active` (mesaj de confirmare). + - [ ] `name` gol, `cui` duplicat sau `id` inexistent → mesaj la stderr + exit code 2. + - [ ] `list` tipareste tabelul `id | name | cui | activ | created_at`. + - [ ] `init_db()` apelat la start (asigura schema + migrare `active`), ca in `tools/apikey.py`. +- **Verificare E2E**: `python -m tools.account create --name "Test SRL" --cui RO99 --inactive --with-key` + → cont inactiv + cheie; `python -m tools.account activate --id ` → `list` arata `activ=da`. + +## 4. Riscuri + +- **Unicitate CUI** — daca un client are mai multe puncte de lucru sub acelasi CUI, constrangerea + blocheaza al doilea cont. Mitigare: o aplicam la nivel de helper (nu schema UNIQUE) ca sa fie usor + de relaxat; decizia finala in §5. +- **Coliziune cu cont default (id=1)** — `INSERT OR IGNORE ... id=1 'default'` exista in schema. + `create_account` foloseste AUTOINCREMENT, deci nu atinge id=1. Test: primul cont creat are id≥2. + +## 5. Intrebari deschise (REZOLVATE la poarta de aprobare) + +- **CUI unic** — REZOLVAT: unic cand e prezent (un CUI = un cont), `NULL` permis multiplu. +- **`--with-key`** — REZOLVAT: optional (flag), emiterea cheii e o decizie constienta. +- **Coloana `active`** — adaugata aici (lifecycle de cont) pentru ca 3.3 sa creeze conturi „in + asteptare" (`active=0`) si gate-ul de trimitere sa le opreasca pana la activarea de catre admin. + +## 6. Valuri de executie (graful de dependente) + +``` +Val 1: [US-001] ← schema active + helper, fisiere (cvasi-)noi → fara dependente +Val 2: [US-002] ← deblocat de US-001 (CLI peste helper) +``` + +--- + +## Addendum review (autoplan, `[subagent-only]` — Codex indisponibil: usage limit) + +> Doua voci Claude independente (Eng + Produs/DX). Schimbarile de mai jos sunt **obligatorii la +> executie** (auto-decise prin principiile autoplan: completitudine, explicit, DRY). Convergenta +> mare intre voci pe primele doua. + +**A1 [HIGH, ambele voci] — Unicitatea CUI prin index partial, NU check in helper.** Check-ul +`SELECT cui → ValueError → INSERT` are o fereastra de coliziune (doi `create_account` concurenti, +relevant cand 3.3 reuseaza helperul din web). Fix: in `schema.sql` + `_migrate` +`CREATE UNIQUE INDEX IF NOT EXISTS ux_accounts_cui ON accounts(cui) WHERE cui IS NOT NULL` (SQLite +trateaza NULL ca distincte nativ → `cui=NULL` multiplu merge gratis). Helperul **normalizeaza** +(`trim` + `upper`) si prinde `IntegrityError → ValueError` pentru mesaj prietenos. Inlocuieste AC +"cui duplicat" + adauga `test_create_cui_null_multiplu_permis`, `test_create_cui_normalizat`. + +**A2 [HIGH, ambele voci] — `accounts.active` este INERT pana la 3.3.** Nimic nu citeste `active` +azi (`resolve_account_id` verifica doar `api_keys.active`; worker-ul nu se uita la cont). Pana la +3.3/US-008 (gate worker), `deactivate` NU opreste trimiterile. Documenteaza-l explicit ca lifecycle +flag consumat de 3.3; nu lasa asteptarea falsa ca dezactiveaza trimiterile. (Decizia keep-in-3.1 vs +move-to-3.3 = taste, vezi poarta.) + +**A3 [MEDIUM, Produs] — Flag consistent cu `tools/apikey.py`:** `activate`/`deactivate` folosesc +`--account N` (nu `--id N`), aliniat cu sora. `create` pastreaza `--name`/`--cui`. + +**A4 [MEDIUM, ambele] — Mesaje de eroare cu cauza+fix.** CUI duplicat → numeste contul existent: +`eroare: CUI RO123 e deja folosit de contul 4 (foloseste 'activate --account 4' sau alt CUI)`. +Specifica textul ca tinta de test. + +**A5 [LOW→MEDIUM] — `--with-key` atomic:** `create_account` + `create_api_key` in aceeasi +tranzactie (`BEGIN IMMEDIATE` → ambele → COMMIT; pe esec ROLLBACK). DB ruleaza autocommit +(`isolation_level=None`) — tranzactia trebuie explicita. Pe esec emitere cheie: mesaj clar ca +contul A fost creat. + +**A6 [MEDIUM, Produs — pentru 3.3] — adauga comenzi CLI ceruta de 3.3:** +- `list --pending` (filtreaza `active=0`) — adminul descopera conturile de activat (3.3 nu are + notificare; vezi addendum 3.3). +- `set-password --account N` — scapare de reset parola pentru 3.3 (Non-Goal SMTP), altfel "reset + prin admin" e o promisiune fara implementare. (Implementarea hash-ului = `app/users.py` din 3.3; + comanda poate astepta US din 3.3, dar planific-o aici ca sa nu se piarda.) + +**A7 [MEDIUM] — Teste RED lipsa de adaugat:** `cui=None` multiplu (A1), `list` fara +`rar_creds_enc`, `set_active` idempotent (set activ pe activ nu arunca), `create_account(active=False)` +la nivel de helper, primul cont creat are `id>=2` (nu atinge default id=1). + +**A8 [LOW] — Obiectiv corectat:** 3.1 e fundatie pentru **3.3** (care reuseaza `create_account` + +`active`); pentru 3.2 e doar suport de VERIFY (al doilea cont). 3.2 NU materializeaza conturi. + +**Deferat (P3, in afara scope-ului imediat — noteaza in ROADMAP/TODO):** `rename`/`set-cui` +(corectie typo fara SQL manual), `--if-not-exists` (provisioning idempotent). Regret probabil, dar +nu blocheaza livrabila. + +## Raport VERIFY + +> Completat de subagentul verificator (context curat) in faza VERIFY — vezi ROADMAP §5.6. +> PASS/FAIL per criteriu, cu dovezi. Lipseste pana la VERIFY. diff --git a/docs/prd/prd-3.2-filtrare-cont-get.md b/docs/prd/prd-3.2-filtrare-cont-get.md new file mode 100644 index 0000000..5cba567 --- /dev/null +++ b/docs/prd/prd-3.2-filtrare-cont-get.md @@ -0,0 +1,174 @@ +# PRD 3.2 — Filtrare pe cont a GET-urilor de listare + +**Stare**: aprobat + +> 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). + +## 1. Obiectiv + +Inchide scurgerea de date intre conturi pe **canalul API**: azi `GET /v1/prezentari`, +`/v1/prezentari/{id}`, `/v1/mapari(/pending)` si `/v1/audit/export` sunt **globale** — orice cheie +valida vede coada, maparile si auditul **tuturor** conturilor. POST-urile sunt deja account-scoped +(`resolve_account_id`); aducem GET-urile la aceeasi disciplina. Pre-cerinta de securitate inainte ca +3.3 sa aduca useri web reali. + +## 2. Non-Goals (anti scope-creep) + +- **Nomenclatorul ramane global.** `GET /v1/nomenclator` + `nomenclator_rar` sunt cache de referinta + RAR partajat (fara `account_id`, fara PII) — nu are sens sa-l filtram. Decizie explicita, nu omisiune. +- **Fara auth pe rutele web.** Rutele `app/web/routes.py` raman hardcodate pe contul default (id=1) + pana la 3.3. 3.2 priveste **doar** GET-urile API `/v1/*`. (`pending_unmapped` se parametrizeaza + compatibil — vezi US-002 — ca web-ul sa ramana neschimbat.) +- **Fara modificari la POST-uri** — sunt deja scoped. +- **Fara paginare/filtre noi** — doar adaugam dimensiunea cont la ce exista. + +## 3. Stories atomice + +> Invariant transversal: randuri **legacy cu `account_id` NULL** (OV-2) se trateaza ca apartinand +> contului default (id=1). Filtrarea foloseste `account_or_default` + match pe NULL doar pentru id=1, +> ca sa nu dispara prezentarile vechi din vederea contului 1. + +### US-001: Scope cont pe `GET /v1/prezentari` + `GET /v1/prezentari/{id}` +**Ca** detinator de cheie API **vreau** ca listarea/detaliul prezentarilor sa-mi arate **doar** +contul meu **pentru ca** un client nu trebuie sa vada coada altui client. + +- **Depinde de**: — +- **Fisiere**: `app/api/v1/router.py`, `tests/test_get_scope_prezentari.py` (nou) (~2 fisiere) +- **Test intai (RED)**: `tests/test_get_scope_prezentari.py` — `test_lista_doar_contul_cheii`, + `test_detaliu_cross_account_404`, `test_legacy_null_vizibil_pentru_cont_1`, + `test_fara_cheie_flag_off_vede_contul_1` +- **Acceptance criteria**: + - [ ] Ambele rute primesc `account_id: int = Depends(resolve_account_id)`. + - [ ] `GET /v1/prezentari` adauga `WHERE` pe cont (NULL→cont 1) la ambele ramuri (cu/fara `status`). + - [ ] `GET /v1/prezentari/{id}` al altui cont → **404** (nu 403 — nu confirmam existenta). + - [ ] Cheie A nu vede submission-uri ale contului B (lista si detaliu). + - [ ] `require_api_key=false` fara cheie → vede contul 1 (back-compat dev). +- **Verificare E2E**: doua chei (conturi distincte, via 3.1) → `POST` pe fiecare → `GET /v1/prezentari` + cu cheia A nu contine id-urile contului B. + +### US-002: Scope cont pe `GET /v1/mapari` + `GET /v1/mapari/pending` +**Ca** detinator de cheie API **vreau** ca maparile si pending-ul sa fie ale contului meu **pentru ca** +azi `/v1/mapari?account_id=` accepta `account_id` din query (spoofabil) si `/pending` e global. + +- **Depinde de**: — +- **Fisiere**: `app/api/v1/router.py`, `app/mapping.py` (parametru optional la `pending_unmapped`), + `tests/test_get_scope_mapari.py` (nou) (~3 fisiere) +- **Test intai (RED)**: `tests/test_get_scope_mapari.py` — `test_mapari_ignora_query_account_id`, + `test_mapari_doar_contul_cheii`, `test_pending_doar_contul_cheii`, `test_pending_web_global_neschimbat` +- **Acceptance criteria**: + - [ ] `GET /v1/mapari` foloseste `Depends(resolve_account_id)`; parametrul `account_id` din query + este **eliminat** (un cont nu poate citi maparile altuia trecand un id arbitrar). + - [ ] `pending_unmapped(conn, account_id=None)` capata param optional: `None` = global (web, + back-compat), valoare = filtrare pe cont. `GET /v1/mapari/pending` paseaza contul cheii. + - [ ] Apelul web `pending_unmapped(conn)` din `routes.py` ramane neatins (global) — confirmat de + `test_pending_web_global_neschimbat`. +- **Verificare E2E**: cheie A cu o mapare; cheie B → `GET /v1/mapari` (B) nu contine maparea lui A. + +### US-003: Scope cont pe `GET /v1/audit/export` +**Ca** detinator de cheie API **vreau** ca exportul de audit sa contina doar prezentarile mele +**pentru ca** CSV-ul de audit expune VIN/nr. inmatriculare (PII) si nu trebuie sa traverseze conturi. + +- **Depinde de**: — +- **Fisiere**: `app/api/v1/router.py`, `tests/test_get_scope_audit.py` (nou) (~2 fisiere) +- **Test intai (RED)**: `tests/test_get_scope_audit.py` — `test_export_doar_contul_cheii`, + `test_export_legacy_null_pentru_cont_1`, `test_export_status_all_tot_scoped` +- **Acceptance criteria**: + - [ ] `audit_export` primeste `Depends(resolve_account_id)`; `_audit_rows` filtreaza pe cont + (NULL→cont 1) pe langa filtrele de data/status existente. + - [ ] `status=all` ramane scoped pe cont (nu exporta global). + - [ ] Randurile contului B nu apar in CSV-ul cerut cu cheia A. +- **Verificare E2E**: `POST` pe doua conturi → `GET /v1/audit/export` (cheie A) → CSV fara VIN-urile B. + +## 4. Riscuri + +- **Back-compat dev** — cu `require_api_key=false` si fara cheie, totul colapseaza la contul 1; + filtrarea trebuie sa ramana transparenta acolo (testat explicit). Mitigare: `account_or_default`. +- **Randuri legacy NULL** — daca le excludem din vederea contului 1, prezentarile vechi „dispar" din + dashboard/audit. Mitigare: regula NULL→cont 1 in fiecare `WHERE`, cu test dedicat. +- **Regresie pe web** — `pending_unmapped` e partajat API/web; semnatura cu default `None` previne + ruperea editorului web (acoperit de test). + +## 5. Intrebari deschise + +> Se rezolva cu utilizatorul ÎNAINTE de executie (poarta de aprobare PRD). + +- **Nomenclatorul ramane global?** Propunere: da (referinta partajata, fara PII). Confirmi? +- **`GET /v1/mapari` — pastram parametrul `account_id` din query, ignorat, sau il scoatem complet?** + Propunere: il scoatem (semnal clar ca nu mai e configurabil din afara). Risca sa strice clienti + care il trimiteau? (Azi nu exista clienti API multi-cont — risc practic zero.) + +## 6. Valuri de executie (graful de dependente) + +``` +Val 1: [US-001, US-002, US-003] ← toate ating router.py (FISIER COMUN) → NU paralel pe acelasi fisier +``` + +> **Nota de paralelizare:** cele trei stories ating `app/api/v1/router.py` → conform §5.5 nu ruleaza +> in paralel (fisier comun). Optiuni: secvential (un teammate, US-001→002→003) sau `isolation: +> worktree` + merge de lead. Recomandare: **secvential, un singur teammate** — schimbarile sunt mici +> si inrudite (acelasi pattern `Depends(resolve_account_id)` + `WHERE account_id`). + +--- + +## Addendum review (autoplan, `[subagent-only]` — Codex indisponibil: usage limit) + +> Doua voci Claude independente (Eng + Produs/DX-API). Schimbari obligatorii la executie + decizii +> ridicate la poarta. + +**B1 [MAJOR, ambele voci] — `pending_unmapped` filtreaza in SQL, nu post-hoc in Python.** Azi +filtreaza in dict dupa `r["account_id"]`. Cu scope pe cont, filtrarea trebuie in query: +`WHERE status='needs_mapping' AND (account_id=? OR (account_id IS NULL AND ?=1))`. Pastreaza default +`account_id=None`=global DAR documenteaza-l ca **intentionat pentru web** (footgun altfel: un apelant +viitor care uita parametrul scurge cross-account — exact bug-ul de fata). AC nou: +`test_pending_filtreaza_in_sql_cu_regula_null`. + +**B2 [MEDIUM→DRY, ambele voci] — Helper unic pentru clauza de scope.** Regula NULL→cont 1 apare in +5+ query-uri (`list_prezentari` ×2, `get_prezentare`, `_audit_rows`, `pending_unmapped`). Extrage +`account_scope_clause(account_id) -> (sql_fragment, params)` care produce +`(account_id = ? OR (account_id IS NULL AND ? = 1))`. **Clarificare critica:** se aplica DOAR +tabelelor cu `account_id` nullable (`submissions`). `get_mapari` pe `operations_mapping` +(`account_id NOT NULL`) foloseste simplu `WHERE account_id=?` — fara ramura NULL. Un singur loc de +testat. + +**B3 [MAJOR, Produs] — 404 cross-account byte-identic cu 404 inexistent.** Altfel `detail` diferit +re-deschide oracolul de enumerare pe care 404 trebuia sa-l inchida. AC nou pe `get_prezentare`: +acelasi status + acelasi `detail` pentru "inexistent" si "exista dar nu e al contului tau". + +**B4 [MAJOR, ambele — leak adiacent] — `get_prezentare` foloseste `SELECT *` → expune +`rar_creds_enc`, `idempotency_key`, `rar_error` (poate contine VIN-uri).** Scope-ul rezolva *cine* +vede randul, nu *ce campuri*. Adauga allowlist de campuri in `get_prezentare` (exclude +`rar_creds_enc`, `payload_json`; pastreaza doar campurile de monitorizare). Ieftin, inchide o +suprafata fragila ("orice coloana noua scapa by default"). AC: `test_detaliu_nu_expune_creds`. + +**B5 [MEDIUM, Eng] — Index pe `account_id`.** Adauga +`CREATE INDEX IF NOT EXISTS idx_submissions_account_status ON submissions(account_id, status)` +(schema + `_migrate`). O linie; previne scan dupa ce fiecare query capata predicat pe coloana +neindexata. (Volum mic azi, dar e ieftin — P2.) + +**B6 [MAJOR, ambele — reformulare onesta] — Web-ul ramane GLOBAL, nu "pe cont 1".** +`fragment_submissions`, `_status_counts`, `fragment_banner`, `_render_mapari` afiseaza TOATE +conturile in dashboard. Non-goal-ul actual ("web hardcodat cont 1") e inselator. Reformulare: +"dashboard-ul web e tool INTERN (neexpus clientilor in Treapta 1/2); riscul rezidual global e +acceptat constient; 3.3/US-005 il inchide comportamental." Daca web-ul devine expus clientilor +inainte de 3.3, 3.2 e incomplet ca pre-cerinta de securitate. + +**B7 [MEDIUM, Produs] — Dependenta de VERIFY fata de 3.1.** Implementarea nu depinde de 3.1, dar +E2E cere un al doilea cont. Noteaza: "E2E foloseste cont #2 creat prin 3.1 (`tools/account.py`) sau +INSERT manual daca 3.1 nu e inca livrat." + +**B8 [MEDIUM, Produs — structural] — Inverseaza default-ul mental.** Cauza bug-ului: scope-ul e +opt-in via `Depends`, nu default. Adauga in `docs/api-rar-contract.md` (sau CLAUDE.md) regula: +"orice GET nou pe `/v1/*` care atinge `submissions`/`operations_mapping` PORNESTE cu +`Depends(resolve_account_id)` + clauza de scope; globalul e exceptie justificata (ca nomenclatorul)." +Trateaza cauza, nu doar cele 4 simptome. + +**TD-3.2 [REZOLVAT la poarta] — `?account_id=` pe `/v1/mapari`:** contul vine MEREU din cheia API +(nespoofabil). Param-ul din query se **ignora** ca selector; daca e prezent SI difera de contul +cheii → **400** explicit (nu schimbare tacita). AC US-002 actualizat: +`test_mapari_query_account_id_diferit_400`, `test_mapari_query_account_id_egal_ok`. + +## Raport VERIFY + +> Completat de subagentul verificator (context curat) in faza VERIFY — vezi ROADMAP §5.6. +> PASS/FAIL per criteriu, cu dovezi. Lipseste pana la VERIFY. diff --git a/docs/prd/prd-3.3-self-onboarding-web.md b/docs/prd/prd-3.3-self-onboarding-web.md new file mode 100644 index 0000000..6e5ee20 --- /dev/null +++ b/docs/prd/prd-3.3-self-onboarding-web.md @@ -0,0 +1,413 @@ +# PRD 3.3 — Self-onboarding web (login email+parola → emite cheie) + +**Stare**: aprobat + +> 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. + +## Raport VERIFY + +> Completat de subagentul verificator (context curat) in faza VERIFY — vezi ROADMAP §5.6. +> PASS/FAIL per criteriu, cu dovezi. Lipseste pana la VERIFY.