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>
35 KiB
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,activemigrat 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
userscuaccount_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(tabelausers),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 inschema.sql+ migrata idempotent in_migrate(DB existenta nu se strica).create_user(conn, account_id, email, password) -> int: stocheaza doarscrypt(salt+parola)+salt(per-user,secrets.token_bytes), niciodata parola in clar.verify_password(conn, email, password) -> int | None: intoarceaccount_idla potrivire,Nonealtfel (comparatie constant-timehmac.compare_digest).emailduplicat (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_secretinSettings(default efemer dev, persistent in prod, cacreds_key).SessionMiddlewaremontat inmain.pycu secretul; cookiehttponly,samesite=lax.current_account(request) -> int | Nonecitesterequest.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 /signuprandeaza formular (companie, CUI optional, email, parola).POST /signup:create_account(active=False)(3.1) +create_user+create_api_keyintr-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 cheiarfak_...+ banner „in asteptare";python -m tools.apikey listarata 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 /logincu 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/submissionsfiltreaza pecurrent_account(request)(NULL→cont 1, ca in 3.2).- Rutele dashboard cer
require_login(sau degradeaza la/login). _fragments/nomenclatorramane 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_IDdin rutele/_import/*+POST /mapariweb →current_account(request). - Un batch al contului A nu e accesibil din sesiunea contului B (preview/confirma/mapare → eroare).
submissions/import_batchescreate primescaccount_id-ul sesiunii.
- Toate aparitiile
- 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-credspe contul sesiunii.
- Buton „roteste cheia" →
- 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(ramanqueued, 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
activeexplicit (legacy) sunt tratate ca active (NULL/absent → activ).
- Claim-ul worker-ului nu ridica submission-uri al caror cont are
- Verificare E2E: signup → import + confirma → submission
queuedramane netrimis cat timp contul e inactiv;tools/account.py activate→ worker il trimite →FINALIZATAla 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.pysi confirma scoping (grepDEFAULT_ACCOUNT_ID= 0 rezultate la final). - Secret de sesiune — daca
AUTOPASS_session_secrete efemer in prod, cookie-urile se invalideaza la restart. Mitigare: documentat ca persistent in.env, cacreds_key. - Atomicitate US-006 —
routes.pye mare; riscul e sa ramana unDEFAULT_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).
- [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. - [REZOLVAT] Useri per cont? Schema
userscuaccount_id(permite multi-user viitor); fluxul MVP = un user creeaza un cont nou, login intra pe contul lui. - Politica de parola — minim 10 caractere, fara alte reguli (lungime > complexitate). (implicit)
- Persistenta sesiunii — cookie semnat (
SessionMiddleware), suficient pentru MVP. (implicit) - 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) - 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 viatools/account.py set-admin --account N) e admin. Guardrequire_admin(pesterequire_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 conturileactive=0(email, companie, CUI, created_at) + buton "activeaza" (set_active(.., True)din 3.1, POST cu CSRF). Optional: dezactivare, vedere chei (fara hash). Templateadmin.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 →FINALIZATAla RAR test.
- Depinde de: US-010, 3.1/US-001 (
US-012: Notificare email admin la signup. La POST /signup, trimite email catre admin(i)
("cont nou N in asteptare"). Adauga dependinta SMTP (config nou AUTOPASS_smtp_*) — reverseaza
Non-Goal-ul "niciun SMTP". Esecul trimiterii email NU blocheaza signup-ul (best-effort + log).
- Depinde de: US-003, US-010 (sa stie cui trimite).
- Intrebare deschisa: provider SMTP / adresa expeditor / fallback daca SMTP pica.
Mecanismul de descoperire e acum redundant-sigur: log
SIGNUP(C16) +list --pending(CLI) + email (US-012) + panou/admin(US-011). C16 ramane ca baseline daca SMTP/panoul intarzie.
Graf revizuit final:
Val 1: [US-001]
Val 2: [US-002] → [US-009 CSRF+rate-limit]
Val 3: [US-003, US-004, US-005, US-006a, US-006b, US-008, US-010]
Val 4: [US-007, US-011 admin panou, US-012 email] (US-007/011/012 peste US-010/US-009)
Scope mai mare decat draftul (7 → 12 stories). 3.3 e acum substantiala; daca devine prea grea pentru o singura faza EXECUTE, lead-ul o poate sparge in sub-livrabile la inceputul executiei (ex. 3.3a self-onboarding core, 3.3b admin web + email) — decizie de orchestrare, nu de scope.
Intrebari deschise nou aparute (rezolva la inceputul EXECUTE)
- Bootstrap admin (US-010): primul cont devine automat admin, SAU adminul se marcheaza manual
via
tools/account.py set-admin --account N? Propunere: manual via CLI (explicit, fara magie "primul user"); contul default id=1 nu e admin automat. - SMTP (US-012): ce provider/expeditor? Daca nu exista SMTP la momentul executiei, US-012 se
livreaza degradat (doar log +
/admin+list --pending) si email-ul devine follow-up.
Progres executie 3.3a (lead)
Sub-livrabila 3.3a (self-onboarding core). Toate stories GREEN, regresie 355 pass (de la 313 baseline).
- US-001 — schema
users(+email_verified,is_adminpregatire 3.3b) +app/users.py(scrypt n=2^14, plafon 128 char, hmac.compare_digest). 6 teste. - US-002 —
SessionMiddleware(same_site=strict, https_only config) +app/web/session.py(current_account/current_user_id/web_account/require_login→LoginRequired/set_sessionclear-inainte C3). 6 teste. - US-009 —
app/web/csrf.py(token per-sesiune,verify_csrfgateat pe MOD: prod sau sesiune autentificata) +app/web/ratelimit.py(fereastra glisanta in-proces) + handlerCsrfError→403. Gate-ul inchide login/signup CSRF in prod (C2). 8 teste. - US-003 —
GET/POST /signup(auth_routes.py+signup.html): tranzactie atomica C10, logSIGNUPC16, cheie-o-data + gate C15, banner pozitiv C17. 4 teste. - US-004 —
GET/POST /login+POST /logout: mesaj generic la esec, login peactive=0intra (C18), clear sesiune C3. 4 teste. - US-005 — dashboard scoped pe sesiune (
_status_counts/fragment_submissions/fragment_bannercu regula NULL→1, C6) + banner "cont in asteptare" (US-008 AC#3, reframe C17). nomenclator ramane global. 4 teste. - US-006a — citiri import (
upload/mapare-coloane/preview) peweb_account(request); batch cross-cont inaccesibil. - US-006b — scrieri (
confirma+/mapari) pe sesiune; propagare consecventaaccount_idlabuild_key(C8/OV-2);verify_csrf+ camp ascuns pe toate formularele; zero atribuiriDEFAULT_ACCOUNT_IDin handlere (C6). 5 teste. - US-008 — gate worker
claim_onecuLEFT JOIN accounts ... COALESCE(active,1)=1(dublu-NULL C14): cont inactiv → submission ramanequeued; 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 atingesubmissions/import_batches/column_mappings/operations_mappinge subrequire_loginSI scoped pe contul sesiunii. Publice intentionat:/signup,/login,/logout,/_fragments/nomenclator(global),/_import/reset(template gol, fara DB).fragment_maparifix confirmat. - Criterii securitate critice re-verificate in cod: CSRF enforce in prod pe
/login+/signupfaraaccount_id(US-009/C2); signup tranzactie atomica cu ROLLBACK pe email duplicat, fara cont orfan (US-003/C10);claim_oneCOALESCE(a.active,1)=1cu LEFT JOIN,account_idNULL=activ (US-008/C14); parola scrypt, niciodata in clar (US-001). - E2E HTTP mod prod (
web_auth_required=true):GET /_fragments/maparifara cookie → 303/login; signup → cheierfak_o data + contactive=0+ logSIGNUP 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_tokenlipsa pe re-randarile de eroare (routes.py): ramurile de eroare dinweb_upload_import/web_save_mapare_coloane/web_confirma_importrandau formularul faracsrf_token→ in prod (user logat, CSRF enforce) campul ascuns gol → urmatorul submit 403 (lockout dupa orice eroare). Fix: helper_ctx(request, **extra)care include mereucsrf_token+ conversia tuturor ramurilor;require_loginreordonat inainteaverify_csrf. Test nou de regresie in mod prod. - [MEDIUM]
verify_passwordignorascrypt_paramsstocat (users.py): folosea constantele curente, anuland migrarea de cost (C9) — un bump viitor denar 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.pybest-effort no-op + logSIGNUPdeja existent din 3.3a).
Valuri (fisiere disjuncte intre stories paralele):
-
Val 1: US-010 (
users.py/session.py/tools/account.py/main.pyhandler) ‖ US-007 (routes.py/_cont.html/dashboard.html) -
Val 2: US-011 (
admin_routes.pynou/admin.html/main.pyregister) ‖ US-012 (email.pynou/config.py/auth_routes.py) -
US-010 — rol admin (
is_admin) + helper-e (count_admins/set_admin/is_account_admin/list_admin_emails) +require_admin→AdminRequired→403 + CLIset-admin. 13 teste. Bootstrap (primul cont=admin) cablat in signup de US-012 (evita conflict pe auth_routes.py). -
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. -
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. -
US-012 —
app/email.pynotify_signupbest-effort (no-op farasmtp_host, prinde orice exceptie SMTP, timeout 5s) + configsmtp_*+ cablaj signup: bootstrap admin (primul cont = admin viacount_admins==0) + notificare degradata dupaset_session. 5 teste. -
Fix migrare (din VERIFY r1):
_migrateadauga defensivusers.is_admin/email_verifiedpe DB cu tabelausersfara 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_csrfpe POST;/cont/*scoped strict pe sesiune, nu acceptaaccount_iddin form;/adminnu expune hash/chei/creds in clar). DAR_migratenu adauga defensivusers.is_admin/email_verified→ o tabelausersfara ele ar ceda cuOperationalError(acelasi tip de gap ca C1 peaccounts.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
usersminima fara coloane →_migrate→ coloane prezente; idempotent). - E2E mod prod (
web_auth_required=true):GET /adminfara cookie → 303/login; non-admin logat → 403;POST /admin/activatefara CSRF → 403. Rute/cont/*scoped pe sesiune, CSRF enforce, parola RAR niciodata invalue=. - US-010 bootstrap (primul signup →
is_admin=1, al doilea → 0), CLIset-admin,require_admin→403 confirmate. - US-012
notify_signupbest-effort no-op fara SMTP + nu blocheaza signup + logSIGNUPpastrat. - 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_*.