Claude Agent b92055eb01 feat(web): self-service cheie/creds + admin web + email signup (PRD 3.3b)
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>
2026-06-18 17:19:06 +00:00
2026-06-17 07:23:58 +00:00

Gateway RAR AUTOPASS

Gateway web (Python / FastAPI) care preia prezentarile de service si le declara la RAR AUTOPASS (Legea 142/2023, OM 210/2024). Inlocuieste integrarea Visual FoxPro existenta (ROAAUTO). Sursa de adevar pentru contractul RAR este docs/api-rar-contract.md.

Arhitectura pe scurt

Doua procese peste acelasi SQLite persistent:

Proces Rol Pornire
API (app.main:app) API v1 (/v1/*), dashboard web (/), /healthz, /metrics, import fisiere (/v1/import/*) uvicorn app.main:app
Worker (app.worker) login RAR + JWT, refresh nomenclator, trimite prezentarile din coada, retry/backoff, heartbeat python3 -m app.worker

Worker-ul ruleaza ca proces separat (nu task in API) — un worker mort nu trebuie sa lase containerul "sanatos". Comunicarea API <-> worker se face exclusiv prin tabela submissions din SQLite. Send-ul catre RAR este dezactivat implicit (AUTOPASS_WORKER_SEND_ENABLED=false) — sigur pentru probe.

Cerinte

  • Python 3.12+
  • Dependintele din requirements.txt
pip3 install -r requirements.txt

(Optional, pentru deploy: Docker + Docker Compose — vezi sectiunea Docker.)

Configurare

Variabilele de mediu folosesc prefixul AUTOPASS_. Pentru dev local valorile implicite sunt suficiente — nu ai nevoie de .env sau de credentiale RAR ca sa testezi UI-ul si API-ul. Copiaza .env.example -> .env doar cand vrei sa rulezi end-to-end.

Variabila Implicit Rol
AUTOPASS_DB_PATH ./data/autopass.db calea fisierului SQLite
AUTOPASS_RAR_ENV test test sau prod
AUTOPASS_REQUIRE_API_KEY false false = dev (fara cheie -> cont id=1); true = prod (cere cheie)
AUTOPASS_CREDS_KEY (efemera) cheie Fernet pt criptarea creds RAR. Trebuie partajata intre API si worker. Genereaza: python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
AUTOPASS_WORKER_SEND_ENABLED false true = trimite efectiv la RAR (proba end-to-end)
AUTOPASS_WORKER_USE_TEST_CREDS false dev: foloseste blocul <test> din settings.xml pt login worker

Pentru proba reala cu RAR: copiaza settings.xml.example -> settings.xml si completeaza credentialele de test (fisierul nu se comite).

Rulare locala (dezvoltare)

1. Porneste API-ul

uvicorn app.main:app --reload --port 8000
# sau, daca uvicorn nu e pe PATH:
python3 -m uvicorn app.main:app --reload --port 8000

La prima pornire se creeaza schema SQLite si se face seed la nomenclatorul RAR (18 coduri din contract), astfel incat dashboard-ul si maparile functioneaza imediat, offline.

2. (Optional) Porneste worker-ul

Necesar doar pentru a procesa coada / a trimite la RAR. Pentru testarea UI-ului si a import-ului nu e necesar.

python3 -m app.worker

Pornire rapida cu start.sh

start.sh ambaleaza pornirea pe mediu (test / prod) si rol (api / worker / both):

./start.sh test api                 # API pe :8000, mediu test
./start.sh test worker --send       # worker care TRIMITE la RAR test
./start.sh test both --send         # API + worker impreuna (dev end-to-end, loguri in .run/)
./start.sh prod api --port 8000     # API mediu prod
./start.sh prod worker --send       # worker prod (NU foloseste creds de test)
./start.sh status                   # stare procese + /healthz
./start.sh stop                     # opreste procesele pornite cu "both"
./start.sh test finalizate          # ce prezentari sunt inregistrate la RAR (vezi mai jos)

Optiuni: --port N, --host H, --reload (dev), --send (activeaza trimiterea la RAR), --test-creds / --no-test-creds (forteaza folosirea creds <test> din settings.xml). Pe test cu --send, creds <test> se folosesc automat. Pentru productie reala foloseste docker compose (vezi sectiunea Docker).

Doua wrappere fixeaza mediul si forwardeaza rolul + optiunile:

./start-test.sh                  # = start.sh test both --send (API + worker, trimite la RAR test)
./start-test.sh worker --send    # = start.sh test worker --send
./start-test.sh finalizate       # = start.sh test finalizate
./start-prod.sh both --send      # = start.sh prod both --send
./start-prod.sh api              # = start.sh prod api

Pe test, ./start-test.sh fara argumente porneste end-to-end (sandbox RAR e sigur). Pe prod, ./start-prod.sh cere rolul explicit si trimiterea trebuie ceruta cu --send (evita trimiteri accidentale in productie).

Testare in browser

Cu API-ul pornit, deschide in browser:

URL Ce vezi
http://localhost:8000/ Dashboard — stare coada, banner prezentari blocate, stare worker / ultim login RAR, editor mapari operatii, browser nomenclator, sectiune import fisier
http://localhost:8000/docs Swagger UI — API v1 interactiv (incearca endpointurile direct din browser)
http://localhost:8000/healthz JSON sanatate: worker viu, ultim login RAR, adancime coada
http://localhost:8000/metrics metrici text (submissions pe status)

Fluxul de import fisier (xlsx / csv) din browser

Pe dashboard, in sectiunea de import:

  1. Incarca un fisier .xlsx sau .csv (drag & drop sau selectare).
  2. Mapeaza coloanele — gateway-ul sugereaza automat (fuzzy) maparea coloana fisier -> camp canonic (VIN, data prestatie, odometru, operatie etc.). Maparea se retine pe semnatura coloanelor: la urmatorul fisier cu aceleasi coloane se aplica automat.
  3. Preview — fiecare rand primeste o stare: ok, needs_mapping, needs_data, needs_review, already_sent, duplicate_in_file.
  4. Confirma — gate dur: retastezi numarul exact de randuri ok de trimis. Randurile confirmate intra in coada (submissions), apoi le urmaresti in tabelul de jos.

Coloane recunoscute (cu sinonime): VIN, Nr inmatriculare, Data prestatie, Odometru final, Odometru initial, Operatie, Observatii.

Genereaza un fisier de test pentru import

Repo-ul nu contine fisiere sample. Creeaza unul rapid:

python3 - <<'PY'
import openpyxl
wb = openpyxl.Workbook()
ws = wb.active
ws.append(["VIN", "Nr inmatriculare", "Data prestatie", "Odometru final", "Operatie"])
ws.append(["WAUZZZ8K0AA000001", "B123ABC", "2026-06-15", 120000, "REVIZIE PERIODICA"])
ws.append(["WAUZZZ8K0AA000002", "B456DEF", "2026-06-16", 85000,  "REPARATIE"])
wb.save("sample_import.xlsx")
print("scris sample_import.xlsx")
PY

Sau un CSV echivalent:

printf 'VIN,Nr inmatriculare,Data prestatie,Odometru final,Operatie\nWAUZZZ8K0AA000001,B123ABC,2026-06-15,120000,REVIZIE PERIODICA\n' > sample_import.csv

Incarca apoi fisierul prin sectiunea de import a dashboard-ului.

Proba trimitere la RAR (mediu test) + verificare ca au ajuns

Implicit worker-ul nu trimite (AUTOPASS_WORKER_SEND_ENABLED=false). Pentru proba end-to-end pe contul de test RAR:

  1. Pune credentialele de test in settings.xml (copiaza din settings.xml.example, completeaza blocul <test>). Acestea nu se comit.

  2. Bag-a prezentari in coada — fie prin import fisier din dashboard, fie prin API (POST /v1/prezentari, vezi mai jos).

  3. Porneste worker-ul cu trimiterea activa:

    ./start.sh test worker --send
    

    Worker-ul face login la RAR test, ia randurile queued, trimite si trece fiecare rand in sent cu id_prezentare (id-ul intors de RAR — dovada ca a ajuns) sau in needs_data / error cu motivul.

  4. Vizualizeaza prezentarile trimise — trei feluri:

    • Dashboard (http://localhost:8000/) — tabelul de jos arata fiecare submission cu status (sent/error/...), id_prezentare, cod RAR si eroare. Se actualizeaza singur.

    • API: curl -s http://localhost:8000/v1/prezentari — coada locala cu statusuri.

    • Direct de la RAR (confirmare independenta ca au ajuns):

      ./start.sh test finalizate
      

      Face login la RAR test si listeaza prezentarile inregistrate acolo (id, VIN, data, odometru). Compari id-urile cu id_prezentare din coada locala: daca se regasesc, prezentarea a ajuns la RAR.

Status sent + id_prezentare completat = RAR a acceptat prezentarea. Worker-ul are si reconciliere anti-duplicat: daca raspunsul RAR se pierde, la urmatorul ciclu cauta prezentarea in finalizate si o marcheaza sent fara a o re-trimite.

Import fisier pentru mai multi utilizatori (service-uri) cu formate diferite

Da — fiecare service auto poate avea propriul format de fisier (alte denumiri de coloane, alta ordine, alt format de data). Sistemul tine minte maparea per cont, deci nu o refaci la fiecare upload:

  • Cont (account_id) — fiecare service e un cont. In productie contul se identifica prin cheia API (X-API-Key) trimisa la upload/cerere (AUTOPASS_REQUIRE_API_KEY=true). In dev, fara cheie, totul merge pe contul implicit id=1.

  • Semnatura coloanelor — la upload, gateway-ul calculeaza o semnatura din lista (sortata) a denumirilor de coloane din fisier. Maparea coloana-fisier -> camp-canonic se salveaza in tabela column_mappings, cheie unica (account_id, signature_coloane), impreuna cu formatul de data.

  • Re-aplicare automata — la urmatorul fisier cu aceleasi coloane (aceeasi semnatura), pentru acelasi cont, maparea retinuta se aplica automat si sari direct la preview. Daca un service schimba formatul (alte coloane) se creeaza o semnatura noua, deci o mapare noua — fara sa o strice pe cea veche. Astfel un cont poate avea mai multe formate memorate simultan.

Pe scurt: cine = account_id (din cheia API), care format = signature_coloane (setul de coloane al fisierului). Combinatia lor selecteaza maparea corecta.

Conturi (service-uri) si chei API

Un cont (accounts) = un service auto care foloseste gateway-ul. Cererile /v1/* se autentifica printr-o cheie API (header X-API-Key: <cheie> sau Authorization: Bearer <cheie>) care identifica contul. Cheia e separata de credentialele RAR ale service-ului.

Enforcement-ul e controlat de AUTOPASS_REQUIRE_API_KEY:

  • false (dev/test, implicit): cerere fara cheie -> contul implicit id=1; o cheie prezenta dar invalida -> 401.
  • true (productie): orice /v1/* protejat cere o cheie valida, altfel 401.

Auth-ul se aplica pe endpointurile care scriu/sunt legate de cont (au dependinta de cheie): POST /v1/prezentari, POST /v1/mapari, POST|DELETE /v1/conturi/rar-creds si toate rutele de import (POST /v1/import, .../column-mapping, .../preview, .../commit, .../export-failed) — acestea ruleaza pe account_id-ul cheii. GET-urile de monitorizare (/v1/prezentari, /v1/prezentari/{id}, /v1/nomenclator, /v1/mapari, /v1/audit/export) sunt momentan neprotejate si globale (nu filtreaza pe cont). Filtrarea pe cont a listarilor

  • protejarea lor raman de adaugat (vezi tabelul de mai jos).

Stare implementare

Capabilitate Stare Cum
Emitere / rotire / revocare / listare chei API Implementat CLI python3 -m tools.apikey
Auth pe cheie (X-API-Key / Bearer) pe POST-uri + import Implementat app/auth.py + flag AUTOPASS_REQUIRE_API_KEY
Ingestie + import account-scoped (din cheie) Implementat POST /v1/prezentari, POST /v1/import
Creds RAR durabile per cont Implementat POST /v1/conturi/rar-creds
Creare cont nou (service) De facut / manual momentan prin INSERT SQL (vezi mai jos); nu exista tool/endpoint dedicat
Protejare + filtrare pe cont a GET-urilor de listare De facut GET /v1/prezentari, /v1/nomenclator, /v1/audit/export sunt globale acum
Self-onboarding web (login email+parola -> emite cheie) De facut docs/ROADMAP.md (Etapa 3.3) — neimplementat

Lifecycle-ul cheilor se face DOAR din CLI, pe masina gateway-ului (admin) — nu exista suprafata HTTP de administrare de securizat. Cheia in clar se afiseaza o singura data la creare/rotire; in DB se pastreaza doar hash-ul SHA-256.

Creare cont + cheie pentru un service nou

Pana la onboarding-ul web, un cont nou se creeaza direct in DB, apoi i se emite o cheie:

# 1. Creeaza contul (numele + CUI sunt informative)
python3 -c "
from app.db import get_connection, init_db
init_db()
c = get_connection()
cur = c.execute(\"INSERT INTO accounts (name, cui) VALUES ('Service Auto SRL', 'RO12345678')\")
print('account_id nou =', cur.lastrowid); c.commit(); c.close()
"

# 2. Emite o cheie API pentru cont (afisata O SINGURA DATA)
python3 -m tools.apikey create --account 2
# -> rfak_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# Alte operatii
python3 -m tools.apikey list                 # toate cheile
python3 -m tools.apikey list --account 2     # cheile unui cont
python3 -m tools.apikey rotate --account 2   # revoca cele active + emite una noua
python3 -m tools.apikey revoke --key-id 3    # revoca o cheie dupa id

Creds RAR per cont

Ca worker-ul sa poata trimite pentru un service fara ca fiecare cerere sa-i poarte parola RAR, seteaza credentialele RAR durabile pe cont (criptate Fernet at-rest):

curl -s -X POST http://localhost:8000/v1/conturi/rar-creds \
  -H 'X-API-Key: rfak_...' -H 'Content-Type: application/json' \
  -d '{"email": "service@exemplu.ro", "password": "parola-rar"}'

Testare prin API (curl)

Exemplele de mai jos arata atat varianta dev (fara cheie -> cont id=1), cat si varianta service cu cheie API (header X-API-Key). Cand AUTOPASS_REQUIRE_API_KEY=true, cheia e obligatorie.

# Sanatate (neprotejat)
curl -s http://localhost:8000/healthz | python3 -m json.tool

# Nomenclator RAR (cache local)
curl -s http://localhost:8000/v1/nomenclator

# Coada de prezentari (monitorizare; momentan globala + neprotejata, vezi nota de mai sus)
curl -s http://localhost:8000/v1/prezentari

# Trimite o prezentare -- dev (fara cheie API -> cont id=1)
curl -s -X POST http://localhost:8000/v1/prezentari \
  -H 'Content-Type: application/json' \
  -d '{
    "rar_credentials": {"email": "test@example.ro", "password": "secret"},
    "prezentari": [{
      "vin": "WAUZZZ8K0AA000001",
      "nr_inmatriculare": "B123ABC",
      "data_prestatie": "2026-06-15",
      "odometru_final": "120000",
      "prestatii": [{"cod_op_service": "REVIZIE PERIODICA", "denumire": "REVIZIE PERIODICA"}]
    }]
  }'

# Trimite o prezentare -- service cu cheie API (account_id curge din cheie)
curl -s -X POST http://localhost:8000/v1/prezentari \
  -H 'X-API-Key: rfak_...' -H 'Content-Type: application/json' \
  -d '{
    "rar_credentials": {"email": "service@exemplu.ro", "password": "parola-rar"},
    "prezentari": [{
      "vin": "WAUZZZ8K0AA000002",
      "nr_inmatriculare": "B456DEF",
      "data_prestatie": "2026-06-16",
      "odometru_final": "85000",
      "prestatii": [{"cod_op_service": "REPARATIE", "denumire": "REPARATIE"}]
    }]
  }'

# Import fisier prin API pentru un service (multi-tenant: contul vine din cheie)
curl -s -X POST http://localhost:8000/v1/import \
  -H 'X-API-Key: rfak_...' -F 'file=@sample_import.xlsx'

Endpointurile complete sunt vizibile si testabile in /docs (Swagger UI). In Swagger, pune cheia prin butonul "Authorize" sau adauga header-ul X-API-Key.

# Sanatate
curl -s http://localhost:8000/healthz | python3 -m json.tool

# Nomenclator RAR (cache local)
curl -s http://localhost:8000/v1/nomenclator

# Coada de prezentari
curl -s http://localhost:8000/v1/prezentari

# Trimite o prezentare (dev: fara cheie API -> cont id=1)
curl -s -X POST http://localhost:8000/v1/prezentari \
  -H 'Content-Type: application/json' \
  -d '{
    "rar_credentials": {"email": "test@example.ro", "password": "secret"},
    "prezentari": [{
      "vin": "WAUZZZ8K0AA000001",
      "nr_inmatriculare": "B123ABC",
      "data_prestatie": "2026-06-15",
      "odometru_final": "120000",
      "prestatii": [{"cod_op_service": "REVIZIE PERIODICA", "denumire": "REVIZIE PERIODICA"}]
    }]
  }'

Endpointurile complete sunt vizibile si testabile in /docs (Swagger UI).

Rularea testelor

python3 -m pytest -q

Suita acopera fundatia, securitatea, validarea, parserul de import, masina de stari a worker-ului si fluxul UI de import (E2E cu RAR mock).

Docker / deploy

# 1. Pregateste .env (CRITIC: AUTOPASS_CREDS_KEY partajata intre api si worker)
cp .env.example .env
# completeaza AUTOPASS_CREDS_KEY (vezi comanda de generare de mai sus)

# 2. Porneste API + worker + autoheal
docker compose up --build

docker-compose.yml porneste trei containere: api (port 8000), worker si autoheal (restarteaza worker-ul cand heartbeat-ul devine invechit). Ambele servicii folosesc acelasi image si acelasi volum SQLite persistent.

Structura

app/
  main.py            # FastAPI: API v1 + dashboard + /healthz + /metrics
  api/v1/            # router.py (prezentari, nomenclator, mapari) + import_router.py
  web/               # routes.py (dashboard + import UI HTMX) + templates/ + static/
  worker/            # proces separat: login RAR, send, retry, heartbeat
  rar_client.py      # client HTTP RAR (login/JWT, postPrezentare, nomenclator)
  validation.py      # validare continut (T3)
  mapping.py         # mapare operatie -> cod prestatie + fuzzy lookup
  crypto.py          # criptare Fernet creds RAR efemere (zero-storage at rest)
  schema.sql         # schema SQLite
docs/                # contract RAR (sursa de adevar) + ROADMAP (progres + proces)
tests/               # suita pytest
legacy-vfp/          # arhiva Visual FoxPro ROAAUTO (legacy, doar referinta/migrare)

Contract RAR (sursa de adevar): docs/api-rar-contract.md. Roadmap + proces de dezvoltare: docs/ROADMAP.md.

Description
No description provided
Readme 5.9 MiB
Languages
Python 83%
HTML 13.1%
xBase 3.1%
Shell 0.8%