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) <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
152
docs/prd/prd-3.1-creare-cont.md
Normal file
152
docs/prd/prd-3.1-creare-cont.md
Normal file
@@ -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 <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.
|
||||
174
docs/prd/prd-3.2-filtrare-cont-get.md
Normal file
174
docs/prd/prd-3.2-filtrare-cont-get.md
Normal file
@@ -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.
|
||||
413
docs/prd/prd-3.3-self-onboarding-web.md
Normal file
413
docs/prd/prd-3.3-self-onboarding-web.md
Normal file
@@ -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.
|
||||
Reference in New Issue
Block a user