US-007: rute web proprii /cont/roteste-cheie + /cont/rar-creds scoped pe sesiune (C13), sectiune "Contul meu" cu cheie afisata o data. US-010: rol admin (users.is_admin) + require_admin->403 + CLI set-admin + bootstrap primul cont=admin (count_admins in BEGIN IMMEDIATE, anti-race). US-011: panou /admin (activare/dezactivare conturi, CSRF + PRG), link admin + logout pe dashboard. US-012: app/email.py notify_signup best-effort degradat fara SMTP + config smtp_*. Fix: migrare defensiva users.is_admin/email_verified in _migrate. VERIFY x2 context curat (PASS) + /code-review high. 393 teste pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
522 lines
35 KiB
Markdown
522 lines
35 KiB
Markdown
# PRD 3.3 — Self-onboarding web (login email+parola → emite cheie)
|
|
|
|
**Stare**: inchis (3.3a + 3.3b livrate)
|
|
|
|
> **Decizii la poarta EXECUTE (2026-06-17, confirmate de utilizator):**
|
|
> - **Livrabila sparta in doua faze** (scope 12 stories prea mare pentru un singur EXECUTE):
|
|
> - **3.3a (in executie acum)** — self-onboarding core: US-001, US-002, US-009, US-003, US-004,
|
|
> US-005, US-006a, US-006b, US-008. Commit + VERIFY propriu.
|
|
> - **3.3b (urmeaza)** — admin web + email: US-010, US-011, US-012. Commit + VERIFY propriu.
|
|
> - **Bootstrap admin (US-010, 3.3b):** primul cont creat devine automat admin (`is_admin=1`).
|
|
> - **US-012 email:** livrare DEGRADATA fara SMTP — doar log `SIGNUP cont=N email=...` (C16) +
|
|
> `/admin` (US-011) + `tools/account.py list --pending`. Trimiterea efectiva = follow-up cand exista SMTP.
|
|
> - Prerechizita C1 confirmata: 3.1 livrat (`app/accounts.py`, `tools/account.py`, `active` migrat in `_migrate`).
|
|
|
|
> Proces complet: `docs/ROADMAP.md` §5. Contract RAR (sursa de adevar): `docs/api-rar-contract.md`.
|
|
> Starea trece: `draft → aprobat → in-executie → verify-pass → inchis` (actualizata de lead).
|
|
>
|
|
> **Aceasta este cea mai mare livrabila a Etapei 3.** Schimba postura de securitate a intregului
|
|
> canal web (azi 100% deschis, hardcodat pe contul 1). Intrebarile din §5 trebuie rezolvate la
|
|
> poarta de aprobare ÎNAINTE de executie — in special **cine are voie sa se inregistreze**.
|
|
|
|
## 1. Obiectiv
|
|
|
|
Un service nou se inregistreaza singur din browser (companie + email + parola), primeste pe loc o
|
|
**cheie API** (afisata o singura data) si o **sesiune web** legata de contul lui. Contul se creeaza
|
|
**„in asteptare" (`active=0`)**: userul poate naviga, configura creds RAR si pregati import, dar
|
|
**nicio prezentare nu se trimite la RAR** pana cand un admin activeaza contul (`tools/account.py
|
|
activate`, din 3.1). Inlocuieste `account_id = DEFAULT_ACCOUNT_ID` hardcodat din `app/web/routes.py`
|
|
cu contul din sesiune, facand dashboard-ul si importul **multi-tenant**. Construieste peste 3.1
|
|
(helper cont + `active`) si peste pattern-ul de scoping din 3.2 (acum aplicat si rutelor web).
|
|
|
|
Fara dependinte noi: sesiuni via `starlette.middleware.sessions.SessionMiddleware` (cookie semnat,
|
|
`itsdangerous` deja prezent), parole via `hashlib.scrypt` (stdlib).
|
|
|
|
## 2. Non-Goals (anti scope-creep)
|
|
|
|
- **Fara reset parola pe email / verificare email** — MVP: parola setata la signup, schimbata doar
|
|
prin admin (sau livrabila viitoare). Niciun SMTP.
|
|
- **Fara multi-user per cont in UI** — schema permite (tabela `users` cu `account_id`), dar fluxul
|
|
MVP creeaza **un user → un cont nou**. Invitare de colegi = viitor.
|
|
- **Fara roluri/permisiuni** — un user vede integral contul lui.
|
|
- **Fara admin web** — listarea/stergerea conturilor ramane CLI (3.1).
|
|
- **Fara OAuth/SSO**.
|
|
- **Setarea creds RAR ramane prin fluxul existent** (`POST /v1/conturi/rar-creds`); un buton web care
|
|
il apeleaza e optional (US-007), nu inima livrabilei.
|
|
|
|
## 3. Stories atomice
|
|
|
|
### US-001: Schema `users` + helper-e `app/users.py` (parole scrypt)
|
|
**Ca** dezvoltator **vreau** un model de utilizator cu parola hash-uita **pentru ca** autentificarea
|
|
web are nevoie de o identitate legata de un cont, separat de cheia API si de creds RAR.
|
|
|
|
- **Depinde de**: 3.1/US-001 (`app/accounts.py`)
|
|
- **Fisiere**: `app/schema.sql` (tabela `users`), `app/db.py` (migrare `_migrate`),
|
|
`app/users.py` (nou), `tests/test_users.py` (nou) (~4 fisiere)
|
|
- **Test intai (RED)**: `tests/test_users.py` — `test_create_user_hash_nu_e_plaintext`,
|
|
`test_verify_parola_corecta_si_gresita`, `test_email_unic_global`, `test_get_user_by_email`
|
|
- **Acceptance criteria**:
|
|
- [ ] `users(id, account_id FK, email TEXT UNIQUE COLLATE NOCASE, password_hash, salt, created_at)`
|
|
creata in `schema.sql` + migrata idempotent in `_migrate` (DB existenta nu se strica).
|
|
- [ ] `create_user(conn, account_id, email, password) -> int`: stocheaza **doar** `scrypt(salt+parola)`
|
|
+ `salt` (per-user, `secrets.token_bytes`), niciodata parola in clar.
|
|
- [ ] `verify_password(conn, email, password) -> int | None`: intoarce `account_id` la potrivire,
|
|
`None` altfel (comparatie constant-time `hmac.compare_digest`).
|
|
- [ ] `email` duplicat (case-insensitive) → `ValueError`.
|
|
- [ ] `get_user_by_email(conn, email)` intoarce metadate fara hash/salt.
|
|
- **Verificare E2E**: n/a (helper) — teste unitare.
|
|
|
|
### US-002: Middleware sesiune + guard web
|
|
**Ca** dezvoltator **vreau** sesiuni cookie semnate + un helper de contul-curent **pentru ca**
|
|
rutele web sa stie cine e logat si sa redirectioneze nelogatii.
|
|
|
|
- **Depinde de**: US-001
|
|
- **Fisiere**: `app/config.py` (`session_secret`), `app/main.py` (`add_middleware`),
|
|
`app/web/session.py` (nou: `current_account(request)`, `require_login`),
|
|
`tests/test_web_session.py` (nou) (~4 fisiere)
|
|
- **Test intai (RED)**: `tests/test_web_session.py` — `test_ruta_protejata_redirect_login`,
|
|
`test_sesiune_seteaza_si_citeste_cont`, `test_logout_curata_sesiunea`
|
|
- **Acceptance criteria**:
|
|
- [ ] `AUTOPASS_session_secret` in `Settings` (default efemer dev, persistent in prod, ca `creds_key`).
|
|
- [ ] `SessionMiddleware` montat in `main.py` cu secretul; cookie `httponly`, `samesite=lax`.
|
|
- [ ] `current_account(request) -> int | None` citeste `request.session["account_id"]`.
|
|
- [ ] `require_login` (dependency): nelogat → `RedirectResponse('/login', 303)`.
|
|
- **Verificare E2E**: GET `/` fara sesiune → redirect `/login`; cu sesiune setata → 200.
|
|
|
|
### US-003: Signup web (`GET/POST /signup`)
|
|
**Ca** service nou **vreau** sa-mi creez cont din browser **pentru ca** sa incep fara admin.
|
|
|
|
- **Depinde de**: US-001, US-002, 3.1/US-001
|
|
- **Fisiere**: `app/web/auth_routes.py` (nou), `app/web/templates/signup.html` (nou),
|
|
`tests/test_web_signup.py` (nou) (~3 fisiere)
|
|
- **Test intai (RED)**: `tests/test_web_signup.py` — `test_signup_creeaza_cont_user_si_cheie`,
|
|
`test_signup_email_duplicat_eroare`, `test_signup_parola_scurta_eroare`, `test_cheie_afisata_o_data`
|
|
- **Acceptance criteria**:
|
|
- [ ] `GET /signup` randeaza formular (companie, CUI optional, email, parola).
|
|
- [ ] `POST /signup`: `create_account(active=False)` (3.1) + `create_user` + `create_api_key`
|
|
intr-o tranzactie; seteaza sesiunea; randeaza pagina cu **cheia afisata o singura data** +
|
|
avertisment + mesaj clar „cont in asteptare — trimiterea incepe dupa activarea de catre admin".
|
|
- [ ] Email duplicat → re-randare cu eroare, **fara** a crea cont orfan (tranzactie rollback).
|
|
- [ ] Parola sub minim (10 caractere, §5) → eroare, fara creare.
|
|
- **Verificare E2E**: browser pe `/signup` (Playwright MCP) → completare → vezi cheia `rfak_...` +
|
|
banner „in asteptare"; `python -m tools.apikey list` arata cheia; sesiunea e activa.
|
|
|
|
### US-004: Login / Logout web (`GET/POST /login`, `POST /logout`)
|
|
**Ca** utilizator existent **vreau** sa ma autentific **pentru ca** sa-mi accesez contul intre sesiuni.
|
|
|
|
- **Depinde de**: US-001, US-002
|
|
- **Fisiere**: `app/web/auth_routes.py`, `app/web/templates/login.html` (nou),
|
|
`tests/test_web_login.py` (nou) (~3 fisiere)
|
|
- **Test intai (RED)**: `tests/test_web_login.py` — `test_login_corect_seteaza_sesiune`,
|
|
`test_login_gresit_401_fara_leak`, `test_logout_redirect_login`
|
|
- **Acceptance criteria**:
|
|
- [ ] `POST /login` cu credentiale corecte → sesiune setata + redirect `/`.
|
|
- [ ] Credentiale gresite → re-randare cu mesaj generic („email sau parola incorecte"), fara a
|
|
dezvalui daca emailul exista.
|
|
- [ ] `POST /logout` → `session.clear()` + redirect `/login`.
|
|
- **Verificare E2E**: browser `/login` → logare → dashboard; `/logout` → inapoi la `/login`.
|
|
|
|
### US-005: Dashboard scoped pe sesiune (citiri)
|
|
**Ca** utilizator logat **vreau** ca dashboard-ul sa-mi arate doar contul meu **pentru ca**
|
|
multi-tenant inseamna ca nu vad coada/banner-ul altui service.
|
|
|
|
- **Depinde de**: US-002
|
|
- **Fisiere**: `app/web/routes.py` (rutele GET dashboard + `_fragments/*`),
|
|
`tests/test_dashboard_scope.py` (nou) (~2 fisiere)
|
|
- **Test intai (RED)**: `tests/test_dashboard_scope.py` — `test_counts_doar_contul_sesiunii`,
|
|
`test_submissions_fragment_scoped`, `test_nelogat_redirect`
|
|
- **Acceptance criteria**:
|
|
- [ ] `_status_counts`, `_fragments/banner`, `_fragments/submissions` filtreaza pe
|
|
`current_account(request)` (NULL→cont 1, ca in 3.2).
|
|
- [ ] Rutele dashboard cer `require_login` (sau degradeaza la `/login`).
|
|
- [ ] `_fragments/nomenclator` ramane global (referinta, ca in 3.2).
|
|
- **Verificare E2E**: doi useri, doua conturi, prezentari diferite → fiecare vede doar coada lui.
|
|
|
|
### US-006: Import web legat de sesiune (scriere)
|
|
**Ca** utilizator logat **vreau** ca importul sa intre pe contul meu **pentru ca** azi toate
|
|
upload-urile aterizeaza pe contul 1, indiferent cine le face.
|
|
|
|
- **Depinde de**: US-002
|
|
- **Fisiere**: `app/web/routes.py` (rutele `/_import/*` + `/mapari`),
|
|
`tests/test_import_web_scope.py` (nou) (~2 fisiere)
|
|
- **Test intai (RED)**: `tests/test_import_web_scope.py` — `test_upload_pe_contul_sesiunii`,
|
|
`test_commit_creeaza_submissions_pe_cont`, `test_batch_alt_cont_inaccesibil`
|
|
- **Acceptance criteria**:
|
|
- [ ] Toate aparitiile `account_id = DEFAULT_ACCOUNT_ID` din rutele `/_import/*` + `POST /mapari`
|
|
web → `current_account(request)`.
|
|
- [ ] Un batch al contului A nu e accesibil din sesiunea contului B (preview/confirma/mapare → eroare).
|
|
- [ ] `submissions`/`import_batches` create primesc `account_id`-ul sesiunii.
|
|
- **Verificare E2E**: import complet prin browser ca user B → submissions au `account_id=B`;
|
|
`./start.sh test finalizate` (cu send) sau dashboard-ul lui B le arata, al lui A nu.
|
|
|
|
### US-007 (optional): Sectiune „Cheia mea API" + creds RAR in dashboard
|
|
**Ca** utilizator logat **vreau** sa-mi rotesc cheia si sa-mi setez creds RAR din UI **pentru ca**
|
|
sa fiu complet self-service fara CLI.
|
|
|
|
- **Depinde de**: US-002, US-005
|
|
- **Fisiere**: `app/web/routes.py`, `app/web/templates/_cont.html` (nou),
|
|
`tests/test_web_cont.py` (nou) (~3 fisiere)
|
|
- **Test intai (RED)**: `tests/test_web_cont.py` — `test_roteste_cheie_afisata_o_data`,
|
|
`test_set_creds_rar_din_sesiune`
|
|
- **Acceptance criteria**:
|
|
- [ ] Buton „roteste cheia" → `rotate_api_key(conn, current_account)`, cheie noua afisata o data.
|
|
- [ ] Formular creds RAR → reuseaza logica `POST /v1/conturi/rar-creds` pe contul sesiunii.
|
|
- **Verificare E2E**: rotire din UI → cheia veche respinsa pe `/v1/*`, cea noua acceptata.
|
|
|
|
### US-008: Gate de trimitere pentru conturi „in asteptare"
|
|
**Ca** operator RAR **vreau** ca prezentarile unui cont neactivat sa **nu** ajunga la RAR **pentru ca**
|
|
self-signup nu trebuie sa permita trimiteri reale inainte de validarea de catre admin.
|
|
|
|
- **Depinde de**: 3.1/US-001 (`accounts.active`)
|
|
- **Fisiere**: `app/worker/__main__.py` (claim), `app/web/templates/_banner.html` (+ `routes.py`),
|
|
`tests/test_worker_active_gate.py` (nou) (~3 fisiere)
|
|
- **Test intai (RED)**: `tests/test_worker_active_gate.py` — `test_claim_sare_cont_inactiv`,
|
|
`test_claim_ia_cont_activ`, `test_activare_deblocheaza_trimiterea`
|
|
- **Acceptance criteria**:
|
|
- [ ] Claim-ul worker-ului **nu** ridica submission-uri al caror cont are `active=0` (raman `queued`,
|
|
fara retry/eroare — pur si simplu nealese pana la activare).
|
|
- [ ] Dupa `set_active(.., True)`, la urmatorul poll submission-urile devin eligibile, fara re-enqueue.
|
|
- [ ] Dashboard-ul contului in asteptare arata un banner „cont in asteptare — trimiterea e oprita".
|
|
- [ ] Conturile fara `active` explicit (legacy) sunt tratate ca active (NULL/absent → activ).
|
|
- **Verificare E2E**: signup → import + confirma → submission `queued` ramane netrimis cat timp contul
|
|
e inactiv; `tools/account.py activate` → worker il trimite → `FINALIZATA` la RAR test.
|
|
|
|
## 4. Riscuri
|
|
|
|
- **Signup deschis = abuz** — oricine isi face cont. Pentru un gateway B2B legat de RAR, asta poate
|
|
fi nedorit. Mitigare posibila: cod de invitatie / aprobare admin / rate-limit. **Decizie §5 — blocant.**
|
|
- **Suprafata de securitate noua** — canalul web trece de la „deschis" la „autentificat"; orice ruta
|
|
web nescoped ramasa = leak. Mitigare: US-005+US-006 acopera **toate** rutele; VERIFY enumera explicit
|
|
fiecare ruta `app/web/routes.py` si confirma scoping (grep `DEFAULT_ACCOUNT_ID` = 0 rezultate la final).
|
|
- **Secret de sesiune** — daca `AUTOPASS_session_secret` e efemer in prod, cookie-urile se invalideaza
|
|
la restart. Mitigare: documentat ca persistent in `.env`, ca `creds_key`.
|
|
- **Atomicitate US-006** — `routes.py` e mare; riscul e sa ramana un `DEFAULT_ACCOUNT_ID`. Mitigare:
|
|
test care face grep in sursa + verificare comportamentala per ruta.
|
|
- **Interactiune cu 3.2** — daca 3.2 nu e inca livrat, scoping-ul web (US-005/006) introduce pattern-ul
|
|
separat; e ok (web nu trece prin `resolve_account_id`), dar mentinem aceeasi regula NULL→cont 1.
|
|
|
|
## 5. Intrebari deschise
|
|
|
|
> Primele doua REZOLVATE la poarta de aprobare; restul = propuneri implicite (confirmabile la review).
|
|
|
|
1. **[REZOLVAT] Cine se poate inregistra?** Signup creeaza **cont „in asteptare" (`active=0`)** pe care
|
|
adminul il activeaza inainte de prima trimitere (optiunea (c)). Vezi US-003 + US-008.
|
|
2. **[REZOLVAT] Useri per cont?** Schema `users` cu `account_id` (permite multi-user viitor); fluxul
|
|
MVP = **un user creeaza un cont nou**, login intra pe contul lui.
|
|
3. **Politica de parola** — minim 10 caractere, fara alte reguli (lungime > complexitate). (implicit)
|
|
4. **Persistenta sesiunii** — cookie semnat (`SessionMiddleware`), suficient pentru MVP. (implicit)
|
|
5. **Contul default (id=1)** — ramane „contul dev fara login", neatins; userii reali primesc id≥2.
|
|
Dashboard fara sesiune in dev (`require_api_key=false`) → cont 1, ca azi. (implicit)
|
|
6. **US-007 in scope?** Optional; se poate taia pentru o livrabila mai mica. (implicit: il pastram)
|
|
|
|
## 6. Valuri de executie (graful de dependente)
|
|
|
|
```
|
|
Val 1: [US-001] ← schema + helper users (depinde de 3.1/US-001 livrat)
|
|
Val 2: [US-002] ← sesiune + guard (peste users)
|
|
Val 3: [US-003, US-004, US-005, US-006, US-008] ← deblocate de US-002 (US-008 doar de 3.1/US-001).
|
|
US-005 si US-006 ating ambele routes.py (FISIER COMUN) →
|
|
nu paralel intre ele. US-003/US-004 (auth_routes.py),
|
|
US-008 (worker) ating fisiere distincte → pot merge paralel.
|
|
Val 4: [US-007] ← optional, peste US-005
|
|
```
|
|
|
|
> **Nota de paralelizare:** US-005 + US-006 modifica acelasi `app/web/routes.py` → secvential sau
|
|
> worktree+merge (§5.5). Un grupaj sigur: teammate A = US-003+US-004 (`auth_routes.py`), teammate B =
|
|
> US-005 apoi US-006 (`routes.py`), serializat. Lead-ul ruleaza regresia dupa fiecare.
|
|
|
|
---
|
|
|
|
## Addendum review (autoplan, `[subagent-only]` — Codex indisponibil: usage limit)
|
|
|
|
> Doua voci Claude independente (Eng + Produs/Design/DX). Securitate notata 4/10 — cea mai slaba
|
|
> dimensiune, fiindca livrabila *introduce* suprafata cookie-auth. Doua blocante critice. Schimbari
|
|
> obligatorii la executie; trei decizii de scope ridicate la poarta.
|
|
|
|
### Blocante (rezolvabile inainte de executie)
|
|
|
|
**C1 [CRITICAL, Eng] — Dependinta 3.1 e fantoma in repo.** `app/accounts.py`, `tools/account.py`,
|
|
`create_account`, `set_active` **nu exista azi**; `accounts.active` e in `schema.sql` dar **NU in
|
|
`_migrate`** → orice DB veche da `OperationalError: no such column` la US-008. Fix: 3.1 e
|
|
prerechizita HARD; la poarta de executie a lui 3.3 verifica livrarea 3.1 (inclusiv migrarea
|
|
`active` in `_migrate`). Nu porni 3.3 fara 3.1 inchis.
|
|
|
|
**C2 [CRITICAL, ambele voci] — CSRF complet absent → STORY NOU US-009.** Dupa 3.3 auth web e
|
|
exclusiv cookie de sesiune; fiecare POST de stare (`/signup`, `/login`, `/logout`, `/_import/*`,
|
|
`/mapari`, rotire cheie) devine vulnerabil CSRF — regresie introdusa DE aceasta livrabila.
|
|
`SameSite=Lax` (US-002) NU acopera POST-uri top-level. US-009: token CSRF in sesiune + camp ascuns
|
|
in toate formularele + validare server-side; `SameSite=Strict` pe cookie-ul de sesiune ca aparare
|
|
in adancime. Depinde de US-002; blocheaza US-003/004/006b/007.
|
|
|
|
### Securitate (obligatoriu in stories existente)
|
|
|
|
**C3 [HIGH, Eng] — Fixare sesiune:** la login (US-004) `request.session.clear()` INAINTE de a seta
|
|
`account_id` (+ nonce nou). AC nou.
|
|
|
|
**C4 [MEDIUM, Eng] — Cookie `secure`/`https_only`:** US-002 adauga `https_only=True` (config-abil,
|
|
default on in prod; in spatele Cloudflare Tunnel e TLS). Plus `samesite` aliniat cu C2.
|
|
|
|
**C5 [MEDIUM, ambele — rate-limit signup] → in US-009.** `active=0` opreste trimiterea, NU crearea
|
|
nelimitata de conturi/useri/chei (DoS storage + zgomot admin). Adauga rate-limit pe IP la
|
|
`POST /signup` (in-proces, fara dependinta noua). Reformuleaza §5.1: nu "REZOLVAT" ci "DECIS:
|
|
cont in asteptare + activare admin; risc de recon acceptat pentru MVP + rate-limit".
|
|
|
|
### Atomicitate scoping (redefineste "done")
|
|
|
|
**C6 [CRITICAL, Eng] — Testul `grep DEFAULT_ACCOUNT_ID = 0` e fals-linistitor.** Rutele GET
|
|
periculoase NU contin acel literal: `_status_counts` (l.59), `fragment_submissions` (l.146-149,
|
|
`SELECT ... FROM submissions` fara `WHERE account_id`), `fragment_banner`. Grep=0 ar declara "done"
|
|
in timp ce dashboard-ul lui B arata coada lui A. Fix US-005: adauga `account_id` +
|
|
`WHERE account_id` (clauza scope din 3.2/B2) la `_status_counts`/`fragment_submissions`/
|
|
`fragment_banner`. "Done" = comportamental (test pe 2 conturi), grep ramane doar smoke.
|
|
|
|
**C7 [HIGH, Eng] — Sparge US-006** in **US-006a** (citiri: upload/preview/reset/mapare-coloane) +
|
|
**US-006b** (scrieri: `confirma` ~250 linii cu tranzactie/atestare + `POST /mapari`). Un
|
|
`account_id` ramas in `confirma` → submissions pe contul gresit, irecuperabil dupa send
|
|
(`FINALIZATA` terminal). 006b primeste E2E dedicat pe 2 conturi.
|
|
|
|
**C8 [HIGH, Eng — OV-2] — Propagare consecventa `current_account`.** `build_key` /
|
|
`_build_idempotency_key` / `_already_sent_lookup` / `_web_compute_preview` trebuie sa primeasca
|
|
ACELASI `account_id` (sesiunea). Test: un rand importat web sub cont N primeste aceeasi cheie ca
|
|
prin API sub cont N (altfel reapare OV-2). AC in US-006b.
|
|
|
|
### Corectitudine helper-e / model
|
|
|
|
**C9 [MEDIUM, ambele] — Parametri scrypt ficsi (US-001):** `hashlib.scrypt(password=parola,
|
|
salt=salt, n=2**14, r=8, p=1, maxmem=64*1024*1024, dklen=32)` — `salt`/`parola` argumente separate
|
|
(NU concatenate). Stocheaza si o eticheta de versiune a parametrilor (migrare cost viitoare). Plafon
|
|
lungime parola (ex. 128 char) — scrypt pe 1MB = DoS.
|
|
|
|
**C10 [HIGH, Eng] — Tranzactie signup atomica (US-003):** `BEGIN IMMEDIATE` → `create_account`
|
|
(active=False) → `create_user` (poate ridica pe `email UNIQUE` AICI) → `create_api_key` → COMMIT;
|
|
orice exceptie → ROLLBACK. Test `test_signup_email_duplicat` asereaza `COUNT(accounts)` neschimbat
|
|
(fara cont orfan). DB e autocommit → tranzactia trebuie explicita.
|
|
|
|
**C11 [MEDIUM, Eng] — `require_login` ca dependency care intoarce `RedirectResponse` NU
|
|
scurtcircuiteaza handler-ul.** Trebuie sa RIDICE o exceptie prinsa de un handler care
|
|
redirectioneaza (sau middleware pe prefixele web). Clarifica mecanismul in US-002.
|
|
|
|
**C12 [MEDIUM, Eng] — `require_login` vs cont-1-in-dev.** §5.5 zice "dev fara sesiune → cont 1" dar
|
|
US-005 cere `require_login`. Bypass cand `require_api_key=false` (sau flag `web_auth_required`),
|
|
cazand pe cont 1. Specifica conditia exacta. Pe scrieri web `current_account is None` → redirect
|
|
login, NICIODATA fallback `account_or_default` (leak silentios pe cont 1).
|
|
|
|
**C13 [HIGH, Eng] — US-007 vs `require_api_key`.** `POST /v1/conturi/rar-creds` cere cheie API; un
|
|
user web logat fara cheie in header → 401 in prod. US-007 are nevoie de **ruta web proprie** scoped
|
|
pe sesiune (nu reuseste endpoint-ul API), SAU un dependency hibrid "sesiune SAU cheie".
|
|
|
|
### Gate worker (US-008)
|
|
|
|
**C14 [MEDIUM, Eng] — Query claim cu `LEFT JOIN` + dublu-NULL.**
|
|
`LEFT JOIN accounts a ON a.id=s.account_id WHERE COALESCE(a.active,1)=1`. Atentie la DOUA NULL-uri:
|
|
(a) cont legacy fara `active` → `COALESCE`; (b) `submissions.account_id IS NULL` (FK
|
|
`ON DELETE SET NULL`) → ar fi blocat pe veci cu JOIN intern; `LEFT JOIN` + `s.account_id IS NULL` =
|
|
tratat ca default/activ. Specifica query-ul exact in AC.
|
|
|
|
### UX / produs (Produs voce)
|
|
|
|
**C15 [HIGH] — Pagina "cheia o data" (US-003)** subspecificata. AC noi: buton copy-to-clipboard +
|
|
confirmare; gate "am salvat cheia" inainte de CTA dashboard; comportament la refresh (POST→render,
|
|
cheia NU e in sesiune → refresh = pierdere; recuperare doar prin rotire US-007 / CLI admin) — scris
|
|
explicit in UI.
|
|
|
|
**C16 [HIGH] — Activarea admin nu are mecanism de descoperire/notificare.** Fara admin web, adminul
|
|
ar rula `tools/account.py list` din proprie initiativa. Fix: la signup, linie de log dedicata
|
|
(`SIGNUP cont=N email=...`) + `tools/account.py list --pending` (din addendum 3.1/A6). Dashboard-ul
|
|
contului arata starea explicit ("in asteptare de la {created_at}"). Documenteaza procesul+SLA de
|
|
activare in PRD (vezi user challenge UC-2 la poarta).
|
|
|
|
**C17 [MEDIUM] — Reframe pozitiv banner "in asteptare"** + stari HTMX: submit dezactivat + indicator
|
|
pe POST (signup face 3 operatii); erorile re-randeaza formularul cu valorile pastrate (Mai putin
|
|
parola — confirma ca nu re-pui parola in `value=`). Banner: "Contul e creat. Configureaza creds RAR
|
|
si pregateste importul ACUM; trimiterea catre RAR porneste automat dupa activare."
|
|
|
|
**C18 [MEDIUM] — Stari/erori lipsa:** login pe cont `active=0` intra (gate-ul e doar pe trimitere —
|
|
confirma explicit); cont activat intre timp → banner dispare (clarifica ca e alt banner decat cel de
|
|
submissions blocate); email normalizat (`trim` + `COLLATE NOCASE`); sesiune cu `account_id` pe cont
|
|
sters → redirect login, nu query gol tacut.
|
|
|
|
**C19 [MEDIUM, P2 ieftin — pregatire viitor]:** `users.email_verified INTEGER DEFAULT 0` de la
|
|
inceput (evita migrare dureroasa cand apare verificarea email). Pune si `user_id` in sesiune (nu
|
|
doar `account_id`) — leaga `import_attestations.confirmed_by` de user mai tarziu fara migrare.
|
|
|
|
### Decizii ridicate la poarta (taste + user challenges)
|
|
|
|
- **UC-1 [user challenge, ambele voci]** — NU lansa signup fara rate-limit + NU marca §5.1
|
|
"REZOLVAT". (Auto-adaugat in C5/US-009; confirma la poarta.)
|
|
- **UC-2 [user challenge, Produs]** — Defineste PROCESUL de activare admin (descoperire + SLA)
|
|
inainte de aprobare, nu ca detaliu de implementare. Acesta e arcul care va genera cele mai multe
|
|
tichete. Intrebare de produs pentru tine.
|
|
- **UC-3 [user challenge, Produs]** — **Promoveaza US-007 din "optional" in scope** (cel putin
|
|
rotire cheie + formular creds RAR). Fara el livrabila nu-si tine numele "self-onboarding": userul
|
|
e dependent de admin/CLI exact pentru pasii self-service. Decizie de scope pentru tine.
|
|
- **Taste:** scrypt stdlib vs argon2id (zero-dependinta vs purist securitate); gate-activare vs
|
|
auto-activ+monitorizare reactiva.
|
|
|
|
### Graf revizuit (stories adaugate/sparte)
|
|
|
|
```
|
|
Val 1: [US-001] (peste 3.1 livrat — C1)
|
|
Val 2: [US-002] → [US-009 CSRF+rate-limit, C2/C5]
|
|
Val 3: [US-003, US-004, US-005, US-006a, US-006b, US-008]
|
|
US-005 + US-006a/b ating routes.py (comun) → secvential; US-003/004 (auth_routes.py),
|
|
US-008 (worker), US-009 (cross-cutting) — fisiere distincte, paralelizabile cu grija.
|
|
Val 4: [US-007] ← promovat la poarta (UC-3): rotire cheie + creds RAR web (ruta proprie, C13)
|
|
```
|
|
|
|
## Decizii poarta (pliate — aprobat de utilizator)
|
|
|
|
> Acestea EXTIND scope-ul fata de draftul initial. Confirma graful revizuit la inceputul EXECUTE.
|
|
|
|
**G1 — US-007 PROMOVAT in MVP (nu mai e optional).** Rotire cheie (recuperare cheie pierduta) +
|
|
formular creds RAR pe **ruta web proprie** scoped pe sesiune (C13, nu reuseste endpoint-ul API care
|
|
cere cheie). Devine obligatoriu pentru ca livrabila sa fie cu adevarat self-service.
|
|
|
|
**G2 — INTERFATA WEB DE ADMIN ceruta explicit ("chiar vreau interfata admin").** Inverseaza
|
|
Non-Goal-ul "fara suprafata HTTP de admin" (3.1). Stories noi:
|
|
|
|
- **US-010: Rol admin + bootstrap.** `users.is_admin INTEGER DEFAULT 0` (migrare). Primul cont
|
|
(sau marcat via `tools/account.py set-admin --account N`) e admin. Guard `require_admin` (peste
|
|
`require_login` + CSRF). Test: non-admin pe ruta admin → 403/redirect.
|
|
- *Depinde de*: US-001, US-002, US-009 (CSRF).
|
|
- *Intrebare deschisa (vezi §5)*: cum se bootstrapeaza primul admin?
|
|
|
|
- **US-011: Panou admin web `/admin` — conturi in asteptare + activare.** Listeaza conturile
|
|
`active=0` (email, companie, CUI, created_at) + buton "activeaza" (`set_active(.., True)` din 3.1,
|
|
POST cu CSRF). Optional: dezactivare, vedere chei (fara hash). Template `admin.html`.
|
|
- *Depinde de*: US-010, 3.1/US-001 (`set_active`).
|
|
- *E2E*: signup (cont nou inactiv) → admin se logheaza → vede contul in `/admin` → activeaza →
|
|
worker il trimite → `FINALIZATA` la RAR test.
|
|
|
|
**US-012: Notificare email admin la signup.** La `POST /signup`, trimite email catre admin(i)
|
|
("cont nou N in asteptare"). **Adauga dependinta SMTP** (config nou `AUTOPASS_smtp_*`) — reverseaza
|
|
Non-Goal-ul "niciun SMTP". Esecul trimiterii email NU blocheaza signup-ul (best-effort + log).
|
|
- *Depinde de*: US-003, US-010 (sa stie cui trimite).
|
|
- *Intrebare deschisa*: provider SMTP / adresa expeditor / fallback daca SMTP pica.
|
|
|
|
> Mecanismul de descoperire e acum redundant-sigur: log `SIGNUP` (C16) + `list --pending` (CLI) +
|
|
> email (US-012) + panou `/admin` (US-011). C16 ramane ca baseline daca SMTP/panoul intarzie.
|
|
|
|
**Graf revizuit final:**
|
|
```
|
|
Val 1: [US-001]
|
|
Val 2: [US-002] → [US-009 CSRF+rate-limit]
|
|
Val 3: [US-003, US-004, US-005, US-006a, US-006b, US-008, US-010]
|
|
Val 4: [US-007, US-011 admin panou, US-012 email] (US-007/011/012 peste US-010/US-009)
|
|
```
|
|
|
|
> **Scope mai mare decat draftul** (7 → 12 stories). 3.3 e acum substantiala; daca devine prea grea
|
|
> pentru o singura faza EXECUTE, lead-ul o poate sparge in sub-livrabile la inceputul executiei
|
|
> (ex. 3.3a self-onboarding core, 3.3b admin web + email) — decizie de orchestrare, nu de scope.
|
|
|
|
## Intrebari deschise nou aparute (rezolva la inceputul EXECUTE)
|
|
|
|
- **Bootstrap admin (US-010):** primul cont devine automat admin, SAU adminul se marcheaza manual
|
|
via `tools/account.py set-admin --account N`? Propunere: manual via CLI (explicit, fara magie
|
|
"primul user"); contul default id=1 nu e admin automat.
|
|
- **SMTP (US-012):** ce provider/expeditor? Daca nu exista SMTP la momentul executiei, US-012 se
|
|
livreaza degradat (doar log + `/admin` + `list --pending`) si email-ul devine follow-up.
|
|
|
|
## Progres executie 3.3a (lead)
|
|
|
|
> Sub-livrabila 3.3a (self-onboarding core). Toate stories GREEN, regresie 355 pass (de la 313 baseline).
|
|
|
|
- [x] **US-001** — schema `users` (+ `email_verified`, `is_admin` pregatire 3.3b) + `app/users.py` (scrypt n=2^14, plafon 128 char, hmac.compare_digest). 6 teste.
|
|
- [x] **US-002** — `SessionMiddleware` (same_site=strict, https_only config) + `app/web/session.py` (`current_account`/`current_user_id`/`web_account`/`require_login`→`LoginRequired`/`set_session` clear-inainte C3). 6 teste.
|
|
- [x] **US-009** — `app/web/csrf.py` (token per-sesiune, `verify_csrf` gateat pe MOD: prod sau sesiune autentificata) + `app/web/ratelimit.py` (fereastra glisanta in-proces) + handler `CsrfError`→403. Gate-ul inchide login/signup CSRF in prod (C2). 8 teste.
|
|
- [x] **US-003** — `GET/POST /signup` (`auth_routes.py` + `signup.html`): tranzactie atomica C10, log `SIGNUP` C16, cheie-o-data + gate C15, banner pozitiv C17. 4 teste.
|
|
- [x] **US-004** — `GET/POST /login` + `POST /logout`: mesaj generic la esec, login pe `active=0` intra (C18), clear sesiune C3. 4 teste.
|
|
- [x] **US-005** — dashboard scoped pe sesiune (`_status_counts`/`fragment_submissions`/`fragment_banner` cu regula NULL→1, C6) + banner "cont in asteptare" (US-008 AC#3, reframe C17). nomenclator ramane global. 4 teste.
|
|
- [x] **US-006a** — citiri import (`upload`/`mapare-coloane`/`preview`) pe `web_account(request)`; batch cross-cont inaccesibil.
|
|
- [x] **US-006b** — scrieri (`confirma` + `/mapari`) pe sesiune; propagare consecventa `account_id` la `build_key` (C8/OV-2); `verify_csrf` + camp ascuns pe toate formularele; zero atribuiri `DEFAULT_ACCOUNT_ID` in handlere (C6). 5 teste.
|
|
- [x] **US-008** — gate worker `claim_one` cu `LEFT JOIN accounts ... COALESCE(active,1)=1` (dublu-NULL C14): cont inactiv → submission ramane `queued`; activare → eligibil fara re-enqueue. 5 teste.
|
|
|
|
3.3b (US-007 self-service cheie/creds + US-010/011/012 admin web + email) ramane livrabila separata.
|
|
|
|
## Raport VERIFY (3.3a)
|
|
|
|
> Doua runde de verificare independenta (subagent context curat, §5.6).
|
|
|
|
**Runda 1 — FAIL (1 criteriu):** suita 355 pass, dar sweep-ul anti-leak a gasit `GET /_fragments/mapari`
|
|
nescoped: `pending_unmapped(conn)` fara `account_id` + fara `require_login` → expune `cod_op_service`/
|
|
`denumire` cross-account (Risc #2/C6). Specul US-005 enumerase doar `_status_counts`/`fragment_submissions`/
|
|
`fragment_banner` si omisese acest fragment. → inapoi la EXECUTE (task fix).
|
|
|
|
**Fix (task #7):** `fragment_mapari` → `require_login(request)`; `_render_mapari(account_id)` →
|
|
`pending_unmapped(conn, account_id)`; `post_mapare` paseaza consecvent contul sesiunii. 2 teste noi de
|
|
izolare pe 2 conturi (`tests/test_mapari_scope.py`).
|
|
|
|
**Runda 2 — PASS global (subagent NOU):**
|
|
- Suita: **357 passed**, 0 fail.
|
|
- Sweep anti-leak complet (toate rutele `routes.py` + `auth_routes.py`): fiecare ruta care atinge
|
|
`submissions`/`import_batches`/`column_mappings`/`operations_mapping` e sub `require_login` SI scoped pe
|
|
contul sesiunii. Publice intentionat: `/signup`, `/login`, `/logout`, `/_fragments/nomenclator` (global),
|
|
`/_import/reset` (template gol, fara DB). `fragment_mapari` fix confirmat.
|
|
- Criterii securitate critice re-verificate in cod: CSRF enforce in prod pe `/login`+`/signup` fara
|
|
`account_id` (US-009/C2); signup tranzactie atomica cu ROLLBACK pe email duplicat, fara cont orfan
|
|
(US-003/C10); `claim_one` `COALESCE(a.active,1)=1` cu LEFT JOIN, `account_id` NULL=activ (US-008/C14);
|
|
parola scrypt, niciodata in clar (US-001).
|
|
- E2E HTTP mod prod (`web_auth_required=true`): `GET /_fragments/mapari` fara cookie → 303 `/login`;
|
|
signup → cheie `rfak_` o data + cont `active=0` + log `SIGNUP cont=N`; cu sesiune → 200 doar contul propriu.
|
|
- Regresia de aur: `test_import_e2e.py` + `test_api.py` = 26 pass. Send live RAR neverificat (fara creds/retea
|
|
in mediul de VERIFY), dar acoperit de teste.
|
|
|
|
**Verdict: PASS.** Send live la RAR test ramane de confirmat manual la deploy (canal API + import → `FINALIZATA`).
|
|
|
|
### Code-review (CLOSE, /code-review high) — 3 findings reparate
|
|
|
|
- **[HIGH] `csrf_token` lipsa pe re-randarile de eroare** (`routes.py`): ramurile de eroare din
|
|
`web_upload_import`/`web_save_mapare_coloane`/`web_confirma_import` randau formularul fara `csrf_token`
|
|
→ in prod (user logat, CSRF enforce) campul ascuns gol → urmatorul submit 403 (lockout dupa orice eroare).
|
|
Fix: helper `_ctx(request, **extra)` care include mereu `csrf_token` + conversia tuturor ramurilor;
|
|
`require_login` reordonat inaintea `verify_csrf`. Test nou de regresie in mod prod.
|
|
- **[MEDIUM] `verify_password` ignora `scrypt_params` stocat** (`users.py`): folosea constantele curente,
|
|
anuland migrarea de cost (C9) — un bump viitor de `n` ar fi blocat toti userii existenti. Fix:
|
|
`_parse_scrypt_params` + verify cu parametrii din DB (eticheta corupta → `None`, fara crash). Test de migrare cost.
|
|
- **[MEDIUM] login fara rate-limit** (`auth_routes.py`): brute-force parole + DoS CPU (scrypt/cerere).
|
|
Fix: `check_rate_limit("login:"+ip, login_rate_max=10, ...)` → 429. (Extinde C5 dincolo de signup.)
|
|
|
|
Suita finala: **361 passed, 0 fail.** Findings low/by-design neactionate (documentate): dev-fallback cont 1
|
|
cand `web_auth_required=False` (C12, intentionat — atentie ops la deploy prod), 500 rar la DB-locked in signup,
|
|
`request.client is None` → bucket rate-limit 'unknown' partajat.
|
|
|
|
## Progres executie 3.3b (lead)
|
|
|
|
> Sub-livrabila 3.3b (self-service cheie/creds + admin web + email). Decizii confirmate la poarta:
|
|
> primul cont care se inregistreaza devine admin (bootstrap automat); US-012 livrare DEGRADATA fara
|
|
> SMTP (helper `app/email.py` best-effort no-op + log `SIGNUP` deja existent din 3.3a).
|
|
|
|
Valuri (fisiere disjuncte intre stories paralele):
|
|
- **Val 1:** US-010 (`users.py`/`session.py`/`tools/account.py`/`main.py` handler) ‖ US-007 (`routes.py`/`_cont.html`/`dashboard.html`)
|
|
- **Val 2:** US-011 (`admin_routes.py` nou/`admin.html`/`main.py` register) ‖ US-012 (`email.py` nou/`config.py`/`auth_routes.py`)
|
|
|
|
- [x] **US-010** — rol admin (`is_admin`) + helper-e (`count_admins`/`set_admin`/`is_account_admin`/`list_admin_emails`) + `require_admin`→`AdminRequired`→403 + CLI `set-admin`. 13 teste. Bootstrap (primul cont=admin) cablat in signup de US-012 (evita conflict pe auth_routes.py).
|
|
- [x] **US-007** — sectiune "Contul meu" (`/_fragments/cont`): rotire cheie (afisata o data) + creds RAR pe ruta web proprie scoped pe sesiune (`POST /cont/roteste-cheie`, `POST /cont/rar-creds`, C13, NU endpointul API). 5 teste.
|
|
- [x] **US-011** — panou `/admin` (`admin_routes.py`): conturi in asteptare/active + activare/dezactivare (require_admin + CSRF + PRG); contul dev id=1 fara butoane. Link "Panou admin" pe dashboard doar pentru admini + buton logout. 5 + 2 teste.
|
|
- [x] **US-012** — `app/email.py` `notify_signup` best-effort (no-op fara `smtp_host`, prinde orice exceptie SMTP, timeout 5s) + config `smtp_*` + cablaj signup: bootstrap admin (primul cont = admin via `count_admins==0`) + notificare degradata dupa `set_session`. 5 teste.
|
|
- [x] **Fix migrare (din VERIFY r1):** `_migrate` adauga defensiv `users.is_admin`/`email_verified` pe DB cu tabela `users` fara ele (idempotent, guard pe existenta tabelei). 2 teste.
|
|
|
|
## Raport VERIFY (3.3b)
|
|
|
|
> Doua runde de verificare independenta (subagent context curat, §5.6).
|
|
|
|
**Runda 1 — FAIL (1 criteriu):** suita 391 pass, toate criteriile US-007/010/011/012 confirmate + sweep
|
|
securitate complet (toate rutele noi `/cont/*`, `/_fragments/cont`, `/admin*` sub `require_login`/`require_admin`
|
|
+ `verify_csrf` pe POST; `/cont/*` scoped strict pe sesiune, nu accepta `account_id` din form; `/admin` nu
|
|
expune hash/chei/creds in clar). DAR `_migrate` nu adauga defensiv `users.is_admin`/`email_verified` →
|
|
o tabela `users` fara ele ar ceda cu `OperationalError` (acelasi tip de gap ca C1 pe `accounts.active`). → fix.
|
|
|
|
**Fix:** bloc `# Coloane users` in `app/db.py::_migrate` (guard pe existenta tabelei + ALTER idempotent). 2 teste.
|
|
|
|
**Runda 2 — PASS global (subagent NOU):**
|
|
- Suita: **393 passed**, 0 fail.
|
|
- Fix migrare confirmat (test pe `users` minima fara coloane → `_migrate` → coloane prezente; idempotent).
|
|
- E2E mod prod (`web_auth_required=true`): `GET /admin` fara cookie → 303 `/login`; non-admin logat → 403;
|
|
`POST /admin/activate` fara CSRF → 403. Rute `/cont/*` scoped pe sesiune, CSRF enforce, parola RAR niciodata in `value=`.
|
|
- US-010 bootstrap (primul signup → `is_admin=1`, al doilea → 0), CLI `set-admin`, `require_admin`→403 confirmate.
|
|
- US-012 `notify_signup` best-effort no-op fara SMTP + nu blocheaza signup + log `SIGNUP` pastrat.
|
|
- Regresia de aur: `test_import_e2e` + `test_api` + `test_worker_active_gate` = 31 pass.
|
|
|
|
**Verdict: PASS.** Send live RAR ramane de confirmat manual la deploy (fara creds/retea in mediul VERIFY).
|
|
La deploy prod: `AUTOPASS_session_secret` persistent, `AUTOPASS_WEB_AUTH_REQUIRED=true`, optional `AUTOPASS_smtp_*`.
|