Files
rar-autopass/app/api/v1/integrare_router.py
Claude Agent b1d825e66b feat(5.20): US-013 retragere accounts.rar_creds_enc -> per-env + DROP cu garda
Toate citirile pe coloana legacy accounts.rar_creds_enc mutate pe sloturile
per-env (rar_creds_test_enc/rar_creds_prod_enc): worker fallback+keepalive,
are_creds (web) si are_creds_rar (integrare, +are_creds_test/_prod), write-back
API la reactivare, purjare la stergere cont, _get_acasa_context/_fetch_cont_env_state.

Contract API (aditiv): POST /v1/conturi/rar-creds primeste rar_target optional
(test/prod), scrie in slotul corect + activeaza mediul; DELETE primeste ?env
(sterge un slot sau ambele). Documentat in docs/api-rar-contract.md.

DROP cu garda in db.py (schema.sql fara coloana pe DB fresh):
- 6a: eliminat ADD COLUMN rar_creds_enc (fara ping-pong re-ADD dupa DROP)
- 6b: try/except fail-safe (nu crapa boot-ul) + garda sqlite_version >= 3.35
- 6c: re-backfill old->new imediat inainte de assert (ancora globala)
- garda orfane: DROP anulat daca vreun creds legacy nu a aterizat in slot per-env
- backup criptat accounts_rar_creds_enc_backup inainte de DROP
- 6d: verificare prin PRAGMA table_info (NU grep — submissions are aceeasi coloana)
Garda one-way, idempotenta la boot repetat (verificat). submissions.rar_creds_enc
ramane neatinsa.

tests/test_retragere_creds_enc.py: niciun read pe coloana veche, conturi rar-creds
env-aware, are_creds per-env, DROP blocat de garda la lipsa copiere. 9 teste
existente actualizate pe sloturi per-env.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:03:08 +00:00

187 lines
6.6 KiB
Python

"""Router integrare — endpoint-uri de integrare externe.
Endpointuri:
GET /v1/ping — readiness check per cont (autentificat sau dev fallback)
GET /v1/integrare/postman.json — export colectie Postman v2.1.0
Ruta /v1/ping foloseste `resolve_account_id` (dependinta standard) pentru 401
pe cheie invalida / prod fara cheie. Flag-ul `autentificat_cu_cheie` e derivat
separat citind header-ele brute si verificand cheia real-time (fara sa dubleze
logica de 401 — aceea ramane in `resolve_account_id`).
"""
from __future__ import annotations
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, Header
from fastapi.responses import JSONResponse
from ...auth import _extract_key, account_for_key, resolve_account_id
from ...config import get_settings
from ...db import get_connection
from ...mapping import account_or_default
router = APIRouter(prefix="/v1", tags=["integrare"])
@router.get("/ping")
def ping(
account_id: int = Depends(resolve_account_id),
x_api_key: str | None = Header(default=None, alias="X-API-Key"),
authorization: str | None = Header(default=None),
) -> JSONResponse:
"""Readiness check per cont.
Intoarce:
account_id — contul rezolvat din cheie (sau 1 in dev fara cheie)
mediu — "test" / "prod" (settings.rar_env)
autentificat_cu_cheie — True daca cererea a venit cu o cheie API reala valida
are_creds_rar — True daca contul are creds RAR stocate pe cel putin un mediu (test sau prod)
are_creds_test — True daca contul are creds RAR pentru mediul Testare
are_creds_prod — True daca contul are creds RAR pentru mediul Productie
ts — timestamp ISO UTC al cererii
"""
settings = get_settings()
# Detectam daca s-a folosit o cheie reala (nu fallback dev).
# `resolve_account_id` a garantat deja ca nu e cheie invalida (ar fi dat 401).
# Acum verificam doar daca exista o cheie extrasa si daca e valida pentru cont.
cheie_bruta = _extract_key(x_api_key, authorization)
autentificat_cu_cheie = False
if cheie_bruta:
conn = get_connection()
try:
acct = account_for_key(conn, cheie_bruta)
finally:
conn.close()
autentificat_cu_cheie = acct is not None
# Verificam daca contul are creds RAR stocate (per-env, US-013).
aid = account_or_default(account_id)
conn = get_connection()
try:
row = conn.execute(
"SELECT rar_creds_test_enc, rar_creds_prod_enc FROM accounts WHERE id=?", (aid,)
).fetchone()
finally:
conn.close()
are_creds_test = bool(row and row["rar_creds_test_enc"])
are_creds_prod = bool(row and row["rar_creds_prod_enc"])
are_creds_rar = are_creds_test or are_creds_prod
return JSONResponse({
"account_id": aid,
"mediu": settings.rar_env,
"autentificat_cu_cheie": autentificat_cu_cheie,
"are_creds_rar": are_creds_rar,
"are_creds_test": are_creds_test,
"are_creds_prod": are_creds_prod,
"ts": datetime.now(timezone.utc).isoformat(),
})
# Allowlist hardcodat (NU derivat din app.routes) — cele 3 rute de integrare expuse extern.
_POSTMAN_ITEMS = [
{
"name": "Trimite prezentari",
"request": {
"method": "POST",
"header": [
{"key": "X-API-Key", "value": "{{api_key}}"},
{"key": "Content-Type", "value": "application/json"},
],
"url": {
"raw": "{{base_url}}/v1/prezentari",
"host": ["{{base_url}}"],
"path": ["v1", "prezentari"],
},
"body": {
"mode": "raw",
"options": {"raw": {"language": "json"}},
# rar_credentials e optional: cererea trimite doar cheia API + datele
# prezentarii; worker-ul foloseste creds-urile RAR salvate pe cont.
"raw": (
'{\n'
' "prezentari": [\n'
' {\n'
' "vin": "WVWZZZ1KZAW000123",\n'
' "nr_inmatriculare": "B999TST",\n'
' "data_prestatie": "2026-06-15",\n'
' "odometru_final": "123456",\n'
' "prestatii": [\n'
' {"cod_prestatie": "OE-1"}\n'
' ]\n'
' }\n'
' ]\n'
'}'
),
},
},
},
{
"name": "Import fisier (xlsx/csv)",
"request": {
"method": "POST",
"header": [
{"key": "X-API-Key", "value": "{{api_key}}"},
],
"url": {
"raw": "{{base_url}}/v1/import",
"host": ["{{base_url}}"],
"path": ["v1", "import"],
},
"body": {
"mode": "formdata",
"formdata": [
{
"key": "file",
"type": "file",
"description": "Fisier xlsx sau csv cu prezentarile de importat",
}
],
},
},
},
{
"name": "Ping (readiness check)",
"request": {
"method": "GET",
"header": [
{"key": "X-API-Key", "value": "{{api_key}}"},
],
"url": {
"raw": "{{base_url}}/v1/ping",
"host": ["{{base_url}}"],
"path": ["v1", "ping"],
},
},
},
]
@router.get("/integrare/postman.json")
def postman_export() -> JSONResponse:
"""Export colectie Postman v2.1.0 cu cele 3 rute de integrare.
Allowlist hardcodat — NU deriva din app.routes pentru a nu expune
rute interne (ex. /v1/conturi/rar-creds, rutele web etc.).
"""
colectie = {
"info": {
"name": "RAR AUTOPASS Gateway",
"description": (
"Colectie de integrare pentru gateway-ul RAR AUTOPASS (Legea 142/2023, OM 210/2024). "
"Seteaza variabilele `base_url` si `api_key` inainte de utilizare."
),
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
},
"variable": [
{"key": "base_url", "value": "http://localhost:8010", "type": "string"},
{"key": "api_key", "value": "", "type": "string"},
],
"item": _POSTMAN_ITEMS,
}
return JSONResponse(content=colectie, media_type="application/json")