feat(web): hub integrare /integrare — exemple cod + retetar VFP + ping + export (PRD 5.1)
Pagina /integrare (tab autentificat, scoped pe cont): exemple cod multi-limbaj (curl/Python/PHP/C#/Node) + retetar Visual FoxPro (MSXML2 + WinHttp) pe ambele canale (prezentari JSON + import fisier), export Postman/OpenAPI/Swagger si buton "Testeaza conexiunea". - US-001: GET /v1/ping (readiness: account_id/mediu/autentificat_cu_cheie/ are_creds_rar/ts) + GET /v1/integrare/postman.json (v2.1.0, allowlist 3 rute) - US-002: app/web/integrare_examples.py pur (7 limbaje x 2 canale, drift-test is_required(), JSON compact pentru C#/VFP) - US-003: tab "Integrare" IA pe 2 niveluri (limbaj->canal, VFP cu dialecte), copy din <pre><code>, empty-state CTA, export .cardlink, script scoped - US-004: POST /integrare/test-cheie (account_for_key direct, scoped sesiune, no-echo cheie) Backend trimitere (worker/masina stari/idempotenta/mapping) si schema neatinse. 568 teste pass. VERIFY context curat + E2E browser (Playwright) + code-review high. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
182
app/api/v1/integrare_router.py
Normal file
182
app/api/v1/integrare_router.py
Normal file
@@ -0,0 +1,182 @@
|
||||
"""Router integrare US-001 — 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 rar_creds_enc stocat
|
||||
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.
|
||||
aid = account_or_default(account_id)
|
||||
conn = get_connection()
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT rar_creds_enc FROM accounts WHERE id=?", (aid,)
|
||||
).fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
are_creds_rar = bool(row and row["rar_creds_enc"])
|
||||
|
||||
return JSONResponse({
|
||||
"account_id": aid,
|
||||
"mediu": settings.rar_env,
|
||||
"autentificat_cu_cheie": autentificat_cu_cheie,
|
||||
"are_creds_rar": are_creds_rar,
|
||||
"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"}},
|
||||
"raw": (
|
||||
'{\n'
|
||||
' "rar_credentials": {\n'
|
||||
' "email": "user@exemplu.ro",\n'
|
||||
' "password": "parola_rar"\n'
|
||||
' },\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")
|
||||
@@ -22,6 +22,7 @@ from starlette.responses import RedirectResponse
|
||||
|
||||
from . import __version__
|
||||
from .api.v1.import_router import router as import_v1_router
|
||||
from .api.v1.integrare_router import router as integrare_v1_router
|
||||
from .api.v1.router import router as api_v1_router
|
||||
from .config import get_settings
|
||||
from .db import get_connection, init_db, queue_depth, read_heartbeat
|
||||
@@ -90,6 +91,7 @@ app.mount("/static", StaticFiles(directory=str(_STATIC_DIR)), name="static")
|
||||
|
||||
app.include_router(api_v1_router)
|
||||
app.include_router(import_v1_router)
|
||||
app.include_router(integrare_v1_router)
|
||||
app.include_router(web_router)
|
||||
app.include_router(auth_router)
|
||||
app.include_router(admin_router)
|
||||
|
||||
377
app/web/integrare_examples.py
Normal file
377
app/web/integrare_examples.py
Normal file
@@ -0,0 +1,377 @@
|
||||
"""Generator de exemple de cod multi-limbaj pentru integrarea cu AutoPass.
|
||||
|
||||
Modul PUR: fara I/O, fara DB, fara stare globala.
|
||||
Folosit de pagina de documentatie a hub-ului de integrare (Etapa 5).
|
||||
|
||||
Campurile obligatorii din payload-ul JSON sunt derivate dinamic din
|
||||
PrezentareIn.model_fields pentru rezistenta la drift de schema.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from app.models import PrezentareIn
|
||||
|
||||
# Placeholder pentru cheia API — niciodata o cheie reala
|
||||
_CHEIE_PLACEHOLDER = "rfak_..."
|
||||
|
||||
|
||||
def _campuri_obligatorii() -> list[str]:
|
||||
"""Intoarce lista campurilor obligatorii din PrezentareIn (is_required())."""
|
||||
return [camp for camp, field in PrezentareIn.model_fields.items() if field.is_required()]
|
||||
|
||||
|
||||
def _payload_prezentari_dict(account_id: int) -> dict:
|
||||
"""Construieste un payload JSON exemplu cu toate campurile obligatorii.
|
||||
|
||||
Campurile cu default (odometru_initial, obs, b64_image, sistem_reparat) sunt
|
||||
omise pentru concizie — nu sunt obligatorii.
|
||||
"""
|
||||
# Construim un dict cu toate campurile obligatorii
|
||||
campuri = _campuri_obligatorii()
|
||||
prezentare: dict = {}
|
||||
|
||||
# Valori exemplu pentru campuri obligatorii cunoscute
|
||||
valori_exemplu: dict = {
|
||||
"vin": "WVWZZZ1JZXW000001",
|
||||
"nr_inmatriculare": "B123ABC",
|
||||
"data_prestatie": "2026-06-22",
|
||||
"odometru_final": "150000",
|
||||
"prestatii": [{"cod_prestatie": "OE-1"}],
|
||||
}
|
||||
|
||||
for camp in campuri:
|
||||
if camp in valori_exemplu:
|
||||
prezentare[camp] = valori_exemplu[camp]
|
||||
else:
|
||||
# Fallback generic pentru campuri neasteptate adaugate ulterior
|
||||
prezentare[camp] = f"<{camp}>"
|
||||
|
||||
return {
|
||||
"rar_credentials": {
|
||||
"email": "utilizator@service.ro",
|
||||
"password": "parola_rar",
|
||||
},
|
||||
"prezentari": [prezentare],
|
||||
}
|
||||
|
||||
|
||||
def _payload_json_str(account_id: int, indent: int = 2) -> str:
|
||||
"""Payload JSON formatat ca string pentru includere in snippet-uri."""
|
||||
return json.dumps(_payload_prezentari_dict(account_id), indent=indent, ensure_ascii=False)
|
||||
|
||||
|
||||
def _payload_json_compact(account_id: int) -> str:
|
||||
"""Payload JSON pe o singura linie (fara newline) pentru string literal C#/VFP.
|
||||
|
||||
Foloseste separators=(',', ':') pentru a elimina spatiile si newline-urile.
|
||||
Rezultatul e un JSON valid pe o singura linie, fara newline in interior.
|
||||
"""
|
||||
return json.dumps(_payload_prezentari_dict(account_id), separators=(",", ":"), ensure_ascii=False)
|
||||
|
||||
|
||||
def _snippet_curl_prezentari(base_url: str, account_id: int) -> str:
|
||||
payload = _payload_json_str(account_id)
|
||||
return f"""curl -X POST "{base_url}/v1/prezentari" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "X-API-Key: {_CHEIE_PLACEHOLDER}" \\
|
||||
-d '{payload}'"""
|
||||
|
||||
|
||||
def _snippet_curl_import(base_url: str, account_id: int) -> str:
|
||||
fisier = '"file=@prezentari.xlsx"'
|
||||
return (
|
||||
f'curl -X POST "{base_url}/v1/import" \\\n'
|
||||
f' -H "X-API-Key: {_CHEIE_PLACEHOLDER}" \\\n'
|
||||
f" -F {fisier}"
|
||||
)
|
||||
|
||||
|
||||
def _snippet_python_prezentari(base_url: str, account_id: int) -> str:
|
||||
payload = _payload_json_str(account_id)
|
||||
return f"""import requests
|
||||
|
||||
url = "{base_url}/v1/prezentari"
|
||||
headers = {{
|
||||
"X-API-Key": "{_CHEIE_PLACEHOLDER}",
|
||||
"Content-Type": "application/json",
|
||||
}}
|
||||
payload = {payload}
|
||||
|
||||
response = requests.post(url, json=payload, headers=headers)
|
||||
print(response.json())"""
|
||||
|
||||
|
||||
def _snippet_python_import(base_url: str, account_id: int) -> str:
|
||||
return f"""import requests
|
||||
|
||||
url = "{base_url}/v1/import"
|
||||
headers = {{
|
||||
"X-API-Key": "{_CHEIE_PLACEHOLDER}",
|
||||
}}
|
||||
|
||||
with open("prezentari.xlsx", "rb") as f:
|
||||
files = {{"file": ("prezentari.xlsx", f, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}}
|
||||
response = requests.post(url, headers=headers, files=files)
|
||||
|
||||
print(response.json())"""
|
||||
|
||||
|
||||
def _snippet_php_prezentari(base_url: str, account_id: int) -> str:
|
||||
payload = _payload_json_str(account_id)
|
||||
# Escapeaza apostrof-urile pentru PHP heredoc
|
||||
payload_php = payload.replace("'", "\\'")
|
||||
return f"""<?php
|
||||
$url = "{base_url}/v1/prezentari";
|
||||
$payload = '{payload_php}';
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
"Content-Type: application/json",
|
||||
"X-API-Key: {_CHEIE_PLACEHOLDER}",
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
echo $response;"""
|
||||
|
||||
|
||||
def _snippet_php_import(base_url: str, account_id: int) -> str:
|
||||
return f"""<?php
|
||||
$url = "{base_url}/v1/import";
|
||||
$fisier = new CURLFile("prezentari.xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "prezentari.xlsx");
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, ["file" => $fisier]);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
"X-API-Key: {_CHEIE_PLACEHOLDER}",
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
echo $response;"""
|
||||
|
||||
|
||||
def _snippet_csharp_prezentari(base_url: str, account_id: int) -> str:
|
||||
payload = _payload_json_compact(account_id)
|
||||
# Escape ghilimele duble pentru string C# (literal pe o singura linie)
|
||||
payload_cs = payload.replace('"', '\\"')
|
||||
return f"""using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
var client = new HttpClient();
|
||||
client.DefaultRequestHeaders.Add("X-API-Key", "{_CHEIE_PLACEHOLDER}");
|
||||
|
||||
var json = "{payload_cs}";
|
||||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
|
||||
var response = await client.PostAsync("{base_url}/v1/prezentari", content);
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Console.WriteLine(body);"""
|
||||
|
||||
|
||||
def _snippet_csharp_import(base_url: str, account_id: int) -> str:
|
||||
return f"""using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
var client = new HttpClient();
|
||||
client.DefaultRequestHeaders.Add("X-API-Key", "{_CHEIE_PLACEHOLDER}");
|
||||
|
||||
using var form = new MultipartFormDataContent();
|
||||
var fileBytes = File.ReadAllBytes("prezentari.xlsx");
|
||||
var fileContent = new ByteArrayContent(fileBytes);
|
||||
fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
form.Add(fileContent, "file", "prezentari.xlsx");
|
||||
|
||||
var response = await client.PostAsync("{base_url}/v1/import", form);
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Console.WriteLine(body);"""
|
||||
|
||||
|
||||
def _snippet_node_prezentari(base_url: str, account_id: int) -> str:
|
||||
payload = _payload_json_str(account_id)
|
||||
return f"""const payload = {payload};
|
||||
|
||||
const response = await fetch("{base_url}/v1/prezentari", {{
|
||||
method: "POST",
|
||||
headers: {{
|
||||
"Content-Type": "application/json",
|
||||
"X-API-Key": "{_CHEIE_PLACEHOLDER}",
|
||||
}},
|
||||
body: JSON.stringify(payload),
|
||||
}});
|
||||
|
||||
const data = await response.json();
|
||||
console.log(data);"""
|
||||
|
||||
|
||||
def _snippet_node_import(base_url: str, account_id: int) -> str:
|
||||
# FormData si Blob sunt globale in Node 18+ — nu necesita import din node:buffer
|
||||
return f"""import {{ readFileSync }} from "fs";
|
||||
|
||||
const form = new FormData();
|
||||
const continut = readFileSync("prezentari.xlsx");
|
||||
form.append("file", new Blob([continut], {{
|
||||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
}}), "prezentari.xlsx");
|
||||
|
||||
const response = await fetch("{base_url}/v1/import", {{
|
||||
method: "POST",
|
||||
headers: {{ "X-API-Key": "{_CHEIE_PLACEHOLDER}" }},
|
||||
body: form,
|
||||
}});
|
||||
|
||||
const data = await response.json();
|
||||
console.log(data);"""
|
||||
|
||||
|
||||
def _snippet_vfp_msxml_prezentari(base_url: str, account_id: int) -> str:
|
||||
payload = _payload_json_compact(account_id)
|
||||
# VFP: ghilimele in string se dubleaza; payload compact = o singura linie
|
||||
payload_vfp = payload.replace('"', '""')
|
||||
return f"""* Visual FoxPro — MSXML2.ServerXMLHTTP.6.0
|
||||
LOCAL oHTTP, cURL, cPayload, cRaspuns
|
||||
cURL = "{base_url}/v1/prezentari"
|
||||
cPayload = "{payload_vfp}"
|
||||
|
||||
oHTTP = CREATEOBJECT("MSXML2.ServerXMLHTTP.6.0")
|
||||
oHTTP.open("POST", cURL, .F.)
|
||||
oHTTP.setRequestHeader("Content-Type", "application/json")
|
||||
oHTTP.setRequestHeader("X-API-Key", "{_CHEIE_PLACEHOLDER}")
|
||||
oHTTP.send(cPayload)
|
||||
|
||||
cRaspuns = oHTTP.responseText
|
||||
? cRaspuns"""
|
||||
|
||||
|
||||
def _snippet_vfp_msxml_import(base_url: str, account_id: int) -> str:
|
||||
return f"""* Visual FoxPro — MSXML2.ServerXMLHTTP.6.0 — upload fisier
|
||||
* Necesita ADODB.Stream pentru a citi fisierul binar
|
||||
LOCAL oHTTP, oStream, oBody
|
||||
LOCAL cURL, cGranita, cCRLF, cDisp, cType
|
||||
|
||||
cURL = "{base_url}/v1/import"
|
||||
cGranita = "----AutoPassBoundary"
|
||||
cCRLF = CHR(13) + CHR(10)
|
||||
|
||||
* Citire fisier in ADODB.Stream
|
||||
oStream = CREATEOBJECT("ADODB.Stream")
|
||||
oStream.Type = 1 && adTypeBinary
|
||||
oStream.Open()
|
||||
oStream.LoadFromFile("prezentari.xlsx")
|
||||
|
||||
* Construire body multipart (simplificat — pentru fisiere mici)
|
||||
oBody = CREATEOBJECT("ADODB.Stream")
|
||||
oBody.Type = 1
|
||||
oBody.Open()
|
||||
|
||||
oHTTP = CREATEOBJECT("MSXML2.ServerXMLHTTP.6.0")
|
||||
oHTTP.open("POST", cURL, .F.)
|
||||
oHTTP.setRequestHeader("Content-Type", "multipart/form-data; boundary=" + cGranita)
|
||||
oHTTP.setRequestHeader("X-API-Key", "{_CHEIE_PLACEHOLDER}")
|
||||
* Nota: pentru upload binar complet folositi un helper COM sau ADODB.
|
||||
oHTTP.send(oBody)
|
||||
? oHTTP.responseText"""
|
||||
|
||||
|
||||
def _snippet_vfp_winhttp_prezentari(base_url: str, account_id: int) -> str:
|
||||
payload = _payload_json_compact(account_id)
|
||||
# VFP: ghilimele in string se dubleaza; payload compact = o singura linie
|
||||
payload_vfp = payload.replace('"', '""')
|
||||
return f"""* Visual FoxPro — WinHttp.WinHttpRequest.5.1
|
||||
LOCAL oHTTP, cURL, cPayload, cRaspuns
|
||||
cURL = "{base_url}/v1/prezentari"
|
||||
cPayload = "{payload_vfp}"
|
||||
|
||||
oHTTP = CREATEOBJECT("WinHttp.WinHttpRequest.5.1")
|
||||
oHTTP.Open("POST", cURL, .F.)
|
||||
oHTTP.SetRequestHeader("Content-Type", "application/json")
|
||||
oHTTP.SetRequestHeader("X-API-Key", "{_CHEIE_PLACEHOLDER}")
|
||||
oHTTP.Send(cPayload)
|
||||
|
||||
cRaspuns = oHTTP.ResponseText
|
||||
? cRaspuns"""
|
||||
|
||||
|
||||
def _snippet_vfp_winhttp_import(base_url: str, account_id: int) -> str:
|
||||
return f"""* Visual FoxPro — WinHttp.WinHttpRequest.5.1 — upload fisier
|
||||
* Necesita ADODB.Stream pentru a citi fisierul binar
|
||||
LOCAL oHTTP, oStream
|
||||
LOCAL cURL, cGranita, cCRLF
|
||||
|
||||
cURL = "{base_url}/v1/import"
|
||||
cGranita = "----AutoPassBoundary"
|
||||
cCRLF = CHR(13) + CHR(10)
|
||||
|
||||
oStream = CREATEOBJECT("ADODB.Stream")
|
||||
oStream.Type = 1 && adTypeBinary
|
||||
oStream.Open()
|
||||
oStream.LoadFromFile("prezentari.xlsx")
|
||||
|
||||
oHTTP = CREATEOBJECT("WinHttp.WinHttpRequest.5.1")
|
||||
oHTTP.Open("POST", cURL, .F.)
|
||||
oHTTP.SetRequestHeader("Content-Type", "multipart/form-data; boundary=" + cGranita)
|
||||
oHTTP.SetRequestHeader("X-API-Key", "{_CHEIE_PLACEHOLDER}")
|
||||
* Nota: pentru upload binar complet folositi un helper COM sau ADODB.
|
||||
oHTTP.Send()
|
||||
? oHTTP.ResponseText"""
|
||||
|
||||
|
||||
def exemple(base_url: str, account_id: int) -> dict:
|
||||
"""Genereaza snippet-uri de cod multi-limbaj pentru integrarea cu AutoPass.
|
||||
|
||||
Parametri:
|
||||
base_url: URL-ul de baza al gateway-ului (ex. "https://autopass.example.com")
|
||||
account_id: ID-ul contului (inclus in context, nu in snippet-uri)
|
||||
|
||||
Intoarce un dict structurat astfel:
|
||||
{
|
||||
"<limbaj>": {
|
||||
"prezentari": "<snippet string>",
|
||||
"import": "<snippet string>",
|
||||
},
|
||||
...
|
||||
}
|
||||
|
||||
Limbaje: curl, python, php, csharp, node, vfp_msxml, vfp_winhttp.
|
||||
|
||||
Functie pura: fara I/O, fara DB, fara stare globala.
|
||||
"""
|
||||
return {
|
||||
"curl": {
|
||||
"prezentari": _snippet_curl_prezentari(base_url, account_id),
|
||||
"import": _snippet_curl_import(base_url, account_id),
|
||||
},
|
||||
"python": {
|
||||
"prezentari": _snippet_python_prezentari(base_url, account_id),
|
||||
"import": _snippet_python_import(base_url, account_id),
|
||||
},
|
||||
"php": {
|
||||
"prezentari": _snippet_php_prezentari(base_url, account_id),
|
||||
"import": _snippet_php_import(base_url, account_id),
|
||||
},
|
||||
"csharp": {
|
||||
"prezentari": _snippet_csharp_prezentari(base_url, account_id),
|
||||
"import": _snippet_csharp_import(base_url, account_id),
|
||||
},
|
||||
"node": {
|
||||
"prezentari": _snippet_node_prezentari(base_url, account_id),
|
||||
"import": _snippet_node_import(base_url, account_id),
|
||||
},
|
||||
"vfp_msxml": {
|
||||
"prezentari": _snippet_vfp_msxml_prezentari(base_url, account_id),
|
||||
"import": _snippet_vfp_msxml_import(base_url, account_id),
|
||||
},
|
||||
"vfp_winhttp": {
|
||||
"prezentari": _snippet_vfp_winhttp_prezentari(base_url, account_id),
|
||||
"import": _snippet_vfp_winhttp_import(base_url, account_id),
|
||||
},
|
||||
}
|
||||
@@ -134,7 +134,7 @@ def _rar_state(hb, worker_alive: bool) -> str:
|
||||
# cade pe Acasa (tab invalid -> fallback "acasa" in dashboard()), fara 404.
|
||||
# US-003 (3.6): "coada" (Trimiteri) nu mai e tab — Trimiterile sunt sectiune pe Acasa.
|
||||
# ?tab=coada cade tot pe Acasa (fallback), fara 404, fara fragment orfan.
|
||||
_TABS_VALIDE = {"acasa", "mapari", "cont", "nomenclator"}
|
||||
_TABS_VALIDE = {"acasa", "mapari", "cont", "nomenclator", "integrare"}
|
||||
|
||||
|
||||
def _get_acasa_context(request: Request, conn, account_id: int) -> dict:
|
||||
@@ -247,6 +247,41 @@ def _render_panel_nomenclator(request: Request, conn) -> str:
|
||||
})
|
||||
|
||||
|
||||
def _render_integrare(request: Request, conn, account_id: int) -> str:
|
||||
"""Randeaza panoul Integrare ca string HTML (hub documentatie + exemple cod).
|
||||
|
||||
Calculeaza are_cheie (chei API active pe cont) si are_creds (credentiale RAR
|
||||
configurate pe cont), preia base_url real si genereaza snippet-uri multi-limbaj.
|
||||
"""
|
||||
from ..mapping import account_or_default
|
||||
from .integrare_examples import exemple as _exemple
|
||||
|
||||
acct = account_or_default(account_id)
|
||||
row_creds = conn.execute(
|
||||
"SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)
|
||||
).fetchone()
|
||||
are_creds = bool(row_creds and row_creds["rar_creds_enc"])
|
||||
|
||||
row_key = conn.execute(
|
||||
"SELECT 1 FROM api_keys WHERE account_id=? AND active=1 LIMIT 1", (acct,)
|
||||
).fetchone()
|
||||
are_cheie = row_key is not None
|
||||
|
||||
base_url = str(request.base_url).rstrip("/")
|
||||
ex = _exemple(base_url, acct)
|
||||
csrf_token = get_csrf_token(request)
|
||||
|
||||
return templates.get_template("_integrare.html").render({
|
||||
"request": request,
|
||||
"account_id": acct,
|
||||
"base_url": base_url,
|
||||
"exemple": ex,
|
||||
"are_cheie": are_cheie,
|
||||
"are_creds": are_creds,
|
||||
"csrf_token": csrf_token,
|
||||
})
|
||||
|
||||
|
||||
def _render_panel_for_tab(request: Request, conn, account_id: int, tab: str) -> str:
|
||||
"""Randeaza panoul corespunzator unui tab ca string HTML."""
|
||||
if tab == "acasa":
|
||||
@@ -261,6 +296,8 @@ def _render_panel_for_tab(request: Request, conn, account_id: int, tab: str) ->
|
||||
return _render_panel_cont(request, conn, account_id)
|
||||
if tab == "nomenclator":
|
||||
return _render_panel_nomenclator(request, conn)
|
||||
if tab == "integrare":
|
||||
return _render_integrare(request, conn, account_id)
|
||||
return _render_panel_acasa(request)
|
||||
|
||||
|
||||
@@ -346,6 +383,18 @@ def fragment_nomenclator(request: Request) -> HTMLResponse:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.get("/_fragments/integrare", response_class=HTMLResponse)
|
||||
def fragment_integrare(request: Request) -> HTMLResponse:
|
||||
"""Fragment HTMX pentru tab-ul Integrare (hub documentatie + exemple cod)."""
|
||||
account_id = require_login(request)
|
||||
conn = get_connection()
|
||||
try:
|
||||
html = _render_integrare(request, conn, account_id)
|
||||
return HTMLResponse(content=html)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.get("/_fragments/banner", response_class=HTMLResponse)
|
||||
def fragment_banner(request: Request) -> HTMLResponse:
|
||||
account_id = require_login(request)
|
||||
@@ -1938,6 +1987,62 @@ def cont_roteste_cheie(
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.post("/integrare/test-cheie", response_class=HTMLResponse)
|
||||
def integrare_test_cheie(
|
||||
request: Request,
|
||||
api_key: str = Form(""),
|
||||
csrf_token: str | None = Form(None),
|
||||
) -> HTMLResponse:
|
||||
"""Verifica cheia API lipita de utilizator — scoped pe contul sesiunii.
|
||||
|
||||
US-004 (PRD Etapa 5): permite utilizatorului sa confirme ca o cheie copiata
|
||||
din generatorul de exemple corespunde contului sau, fara efecte secundare
|
||||
(fara creare/rotire). Cheie goala, invalida sau a altui cont -> mesaj de
|
||||
eroare neutru (fara eco al cheii in raspuns).
|
||||
"""
|
||||
account_id = require_login(request)
|
||||
verify_csrf(request, csrf_token)
|
||||
acct = account_or_default(account_id)
|
||||
|
||||
# Validare cheie goala / doar spatii -> eroare inainte de DB
|
||||
cheie = (api_key or "").strip()
|
||||
if not cheie:
|
||||
return templates.TemplateResponse(
|
||||
"_integrare_test_rezultat.html",
|
||||
{"request": request, "succes": False, "mesaj": "Cheia este goala sau lipseste."},
|
||||
)
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
from ..auth import account_for_key
|
||||
cont_cheie = account_for_key(conn, cheie)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if cont_cheie is None:
|
||||
# Cheie invalida, inexistenta sau revocata
|
||||
return templates.TemplateResponse(
|
||||
"_integrare_test_rezultat.html",
|
||||
{"request": request, "succes": False,
|
||||
"mesaj": "Cheie invalida sau revocata — nu a fost gasita in sistem."},
|
||||
)
|
||||
|
||||
if cont_cheie != acct:
|
||||
# Cheie valida dar apartine altui cont — mesaj neutru, fara dezvaluire cont terta
|
||||
return templates.TemplateResponse(
|
||||
"_integrare_test_rezultat.html",
|
||||
{"request": request, "succes": False,
|
||||
"mesaj": "Cheia nu apartine contului tau."},
|
||||
)
|
||||
|
||||
# Succes — cheia e activa si corespunde contului sesiunii
|
||||
return templates.TemplateResponse(
|
||||
"_integrare_test_rezultat.html",
|
||||
{"request": request, "succes": True,
|
||||
"mesaj": f"Cheie valida — cont {acct}."},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/cont/rar-creds", response_class=HTMLResponse)
|
||||
def cont_rar_creds(
|
||||
request: Request,
|
||||
|
||||
344
app/web/templates/_integrare.html
Normal file
344
app/web/templates/_integrare.html
Normal file
@@ -0,0 +1,344 @@
|
||||
{# Panoul Integrare: exemple de cod multi-limbaj + export & referinta. #}
|
||||
{# Variabile context: account_id, base_url, exemple, are_cheie, are_creds, csrf_token #}
|
||||
|
||||
<div id="integrare-section">
|
||||
|
||||
{# Empty-state: lipsesc cheie API sau credentiale RAR #}
|
||||
{% if not are_cheie or not are_creds %}
|
||||
<div class="banner warn" style="margin-bottom:16px;" role="alert" aria-live="polite">
|
||||
<strong>Configurare incompleta.</strong>
|
||||
{% if not are_creds and not are_cheie %}
|
||||
Lipsesc atat credentialele RAR cat si o cheie API activa.
|
||||
{% elif not are_creds %}
|
||||
Lipsesc credentialele RAR pentru trimitere.
|
||||
{% else %}
|
||||
Lipseste o cheie API activa.
|
||||
{% endif %}
|
||||
Mergi la <a href="/?tab=cont">tab-ul Cont</a> pentru a le configura.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Card: ID cont si endpoint de baza #}
|
||||
<div class="card" style="margin-bottom:16px;">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:8px;">
|
||||
<span class="muted" style="font-size:13px;">Cont ID:</span>
|
||||
<strong style="font-size:13px;">{{ account_id }}</strong>
|
||||
<span class="muted" style="font-size:13px; margin-left:16px;">Endpoint:</span>
|
||||
<code style="font-size:12px; color:var(--accent);">{{ base_url }}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Tab-list PRIMAR: limbaje #}
|
||||
<div class="card" style="margin-bottom:16px;">
|
||||
<div role="tablist" class="tab-bar" aria-label="Limbaje de programare" id="tl-limbaje" style="margin-bottom:0; border-bottom:1px solid var(--line);">
|
||||
<button role="tab" id="tab-curl" aria-selected="true" aria-controls="panel-curl" tabindex="0" class="tab-link tab-activ" style="border:none; background:none; cursor:pointer;">curl</button>
|
||||
<button role="tab" id="tab-python" aria-selected="false" aria-controls="panel-python" tabindex="-1" class="tab-link" style="border:none; background:none; cursor:pointer;">Python</button>
|
||||
<button role="tab" id="tab-php" aria-selected="false" aria-controls="panel-php" tabindex="-1" class="tab-link" style="border:none; background:none; cursor:pointer;">PHP</button>
|
||||
<button role="tab" id="tab-csharp" aria-selected="false" aria-controls="panel-csharp" tabindex="-1" class="tab-link" style="border:none; background:none; cursor:pointer;">C#</button>
|
||||
<button role="tab" id="tab-node" aria-selected="false" aria-controls="panel-node" tabindex="-1" class="tab-link" style="border:none; background:none; cursor:pointer;">Node</button>
|
||||
<button role="tab" id="tab-vfp" aria-selected="false" aria-controls="panel-vfp" tabindex="-1" class="tab-link" style="border:none; background:none; cursor:pointer;">VFP</button>
|
||||
</div>
|
||||
|
||||
{# Panel curl #}
|
||||
<div role="tabpanel" id="panel-curl" aria-labelledby="tab-curl" style="padding:16px 0 0;">
|
||||
{% set ex = exemple["curl"] %}
|
||||
<div role="tablist" class="tab-bar" aria-label="Canal curl" id="tl-curl-canal" style="margin-bottom:8px; border-bottom:1px solid var(--line);">
|
||||
<button role="tab" id="tab-curl-prez" aria-selected="true" aria-controls="panel-curl-prez" tabindex="0" class="tab-link tab-activ" style="border:none; background:none; cursor:pointer; font-size:13px;">Prezentari JSON</button>
|
||||
<button role="tab" id="tab-curl-import" aria-selected="false" aria-controls="panel-curl-import" tabindex="-1" class="tab-link" style="border:none; background:none; cursor:pointer; font-size:13px;">Import fisier</button>
|
||||
</div>
|
||||
<div role="tabpanel" id="panel-curl-prez" aria-labelledby="tab-curl-prez">
|
||||
<div style="position:relative;">
|
||||
<button class="btn-copiaza" aria-label="Copiaza snippet curl prezentari" style="position:absolute; top:8px; right:8px; font-size:12px; padding:4px 10px; background:var(--line); color:var(--ink); border:1px solid var(--line); border-radius:4px; cursor:pointer;">Copiaza</button>
|
||||
<pre style="background:var(--bg); border:1px solid var(--line); border-radius:6px; padding:16px; overflow-x:auto; margin:0;"><code style="font-size:12px; font-family:ui-monospace,monospace; color:var(--ink);">{{ ex["prezentari"] | e }}</code></pre>
|
||||
</div>
|
||||
<div aria-live="polite" class="copiaza-feedback muted" style="font-size:12px; min-height:20px; margin-top:4px;"></div>
|
||||
</div>
|
||||
<div role="tabpanel" id="panel-curl-import" aria-labelledby="tab-curl-import" hidden>
|
||||
<div style="position:relative;">
|
||||
<button class="btn-copiaza" aria-label="Copiaza snippet curl import" style="position:absolute; top:8px; right:8px; font-size:12px; padding:4px 10px; background:var(--line); color:var(--ink); border:1px solid var(--line); border-radius:4px; cursor:pointer;">Copiaza</button>
|
||||
<pre style="background:var(--bg); border:1px solid var(--line); border-radius:6px; padding:16px; overflow-x:auto; margin:0;"><code style="font-size:12px; font-family:ui-monospace,monospace; color:var(--ink);">{{ ex["import"] | e }}</code></pre>
|
||||
</div>
|
||||
<div aria-live="polite" class="copiaza-feedback muted" style="font-size:12px; min-height:20px; margin-top:4px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Panel python #}
|
||||
<div role="tabpanel" id="panel-python" aria-labelledby="tab-python" hidden style="padding:16px 0 0;">
|
||||
{% set ex = exemple["python"] %}
|
||||
<div role="tablist" class="tab-bar" aria-label="Canal python" id="tl-python-canal" style="margin-bottom:8px; border-bottom:1px solid var(--line);">
|
||||
<button role="tab" id="tab-python-prez" aria-selected="true" aria-controls="panel-python-prez" tabindex="0" class="tab-link tab-activ" style="border:none; background:none; cursor:pointer; font-size:13px;">Prezentari JSON</button>
|
||||
<button role="tab" id="tab-python-import" aria-selected="false" aria-controls="panel-python-import" tabindex="-1" class="tab-link" style="border:none; background:none; cursor:pointer; font-size:13px;">Import fisier</button>
|
||||
</div>
|
||||
<div role="tabpanel" id="panel-python-prez" aria-labelledby="tab-python-prez">
|
||||
<div style="position:relative;">
|
||||
<button class="btn-copiaza" aria-label="Copiaza snippet python prezentari" style="position:absolute; top:8px; right:8px; font-size:12px; padding:4px 10px; background:var(--line); color:var(--ink); border:1px solid var(--line); border-radius:4px; cursor:pointer;">Copiaza</button>
|
||||
<pre style="background:var(--bg); border:1px solid var(--line); border-radius:6px; padding:16px; overflow-x:auto; margin:0;"><code style="font-size:12px; font-family:ui-monospace,monospace; color:var(--ink);">{{ ex["prezentari"] | e }}</code></pre>
|
||||
</div>
|
||||
<div aria-live="polite" class="copiaza-feedback muted" style="font-size:12px; min-height:20px; margin-top:4px;"></div>
|
||||
</div>
|
||||
<div role="tabpanel" id="panel-python-import" aria-labelledby="tab-python-import" hidden>
|
||||
<div style="position:relative;">
|
||||
<button class="btn-copiaza" aria-label="Copiaza snippet python import" style="position:absolute; top:8px; right:8px; font-size:12px; padding:4px 10px; background:var(--line); color:var(--ink); border:1px solid var(--line); border-radius:4px; cursor:pointer;">Copiaza</button>
|
||||
<pre style="background:var(--bg); border:1px solid var(--line); border-radius:6px; padding:16px; overflow-x:auto; margin:0;"><code style="font-size:12px; font-family:ui-monospace,monospace; color:var(--ink);">{{ ex["import"] | e }}</code></pre>
|
||||
</div>
|
||||
<div aria-live="polite" class="copiaza-feedback muted" style="font-size:12px; min-height:20px; margin-top:4px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Panel PHP #}
|
||||
<div role="tabpanel" id="panel-php" aria-labelledby="tab-php" hidden style="padding:16px 0 0;">
|
||||
{% set ex = exemple["php"] %}
|
||||
<div role="tablist" class="tab-bar" aria-label="Canal php" id="tl-php-canal" style="margin-bottom:8px; border-bottom:1px solid var(--line);">
|
||||
<button role="tab" id="tab-php-prez" aria-selected="true" aria-controls="panel-php-prez" tabindex="0" class="tab-link tab-activ" style="border:none; background:none; cursor:pointer; font-size:13px;">Prezentari JSON</button>
|
||||
<button role="tab" id="tab-php-import" aria-selected="false" aria-controls="panel-php-import" tabindex="-1" class="tab-link" style="border:none; background:none; cursor:pointer; font-size:13px;">Import fisier</button>
|
||||
</div>
|
||||
<div role="tabpanel" id="panel-php-prez" aria-labelledby="tab-php-prez">
|
||||
<div style="position:relative;">
|
||||
<button class="btn-copiaza" aria-label="Copiaza snippet php prezentari" style="position:absolute; top:8px; right:8px; font-size:12px; padding:4px 10px; background:var(--line); color:var(--ink); border:1px solid var(--line); border-radius:4px; cursor:pointer;">Copiaza</button>
|
||||
<pre style="background:var(--bg); border:1px solid var(--line); border-radius:6px; padding:16px; overflow-x:auto; margin:0;"><code style="font-size:12px; font-family:ui-monospace,monospace; color:var(--ink);">{{ ex["prezentari"] | e }}</code></pre>
|
||||
</div>
|
||||
<div aria-live="polite" class="copiaza-feedback muted" style="font-size:12px; min-height:20px; margin-top:4px;"></div>
|
||||
</div>
|
||||
<div role="tabpanel" id="panel-php-import" aria-labelledby="tab-php-import" hidden>
|
||||
<div style="position:relative;">
|
||||
<button class="btn-copiaza" aria-label="Copiaza snippet php import" style="position:absolute; top:8px; right:8px; font-size:12px; padding:4px 10px; background:var(--line); color:var(--ink); border:1px solid var(--line); border-radius:4px; cursor:pointer;">Copiaza</button>
|
||||
<pre style="background:var(--bg); border:1px solid var(--line); border-radius:6px; padding:16px; overflow-x:auto; margin:0;"><code style="font-size:12px; font-family:ui-monospace,monospace; color:var(--ink);">{{ ex["import"] | e }}</code></pre>
|
||||
</div>
|
||||
<div aria-live="polite" class="copiaza-feedback muted" style="font-size:12px; min-height:20px; margin-top:4px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Panel C# #}
|
||||
<div role="tabpanel" id="panel-csharp" aria-labelledby="tab-csharp" hidden style="padding:16px 0 0;">
|
||||
{% set ex = exemple["csharp"] %}
|
||||
<div role="tablist" class="tab-bar" aria-label="Canal csharp" id="tl-csharp-canal" style="margin-bottom:8px; border-bottom:1px solid var(--line);">
|
||||
<button role="tab" id="tab-csharp-prez" aria-selected="true" aria-controls="panel-csharp-prez" tabindex="0" class="tab-link tab-activ" style="border:none; background:none; cursor:pointer; font-size:13px;">Prezentari JSON</button>
|
||||
<button role="tab" id="tab-csharp-import" aria-selected="false" aria-controls="panel-csharp-import" tabindex="-1" class="tab-link" style="border:none; background:none; cursor:pointer; font-size:13px;">Import fisier</button>
|
||||
</div>
|
||||
<div role="tabpanel" id="panel-csharp-prez" aria-labelledby="tab-csharp-prez">
|
||||
<div style="position:relative;">
|
||||
<button class="btn-copiaza" aria-label="Copiaza snippet csharp prezentari" style="position:absolute; top:8px; right:8px; font-size:12px; padding:4px 10px; background:var(--line); color:var(--ink); border:1px solid var(--line); border-radius:4px; cursor:pointer;">Copiaza</button>
|
||||
<pre style="background:var(--bg); border:1px solid var(--line); border-radius:6px; padding:16px; overflow-x:auto; margin:0;"><code style="font-size:12px; font-family:ui-monospace,monospace; color:var(--ink);">{{ ex["prezentari"] | e }}</code></pre>
|
||||
</div>
|
||||
<div aria-live="polite" class="copiaza-feedback muted" style="font-size:12px; min-height:20px; margin-top:4px;"></div>
|
||||
</div>
|
||||
<div role="tabpanel" id="panel-csharp-import" aria-labelledby="tab-csharp-import" hidden>
|
||||
<div style="position:relative;">
|
||||
<button class="btn-copiaza" aria-label="Copiaza snippet csharp import" style="position:absolute; top:8px; right:8px; font-size:12px; padding:4px 10px; background:var(--line); color:var(--ink); border:1px solid var(--line); border-radius:4px; cursor:pointer;">Copiaza</button>
|
||||
<pre style="background:var(--bg); border:1px solid var(--line); border-radius:6px; padding:16px; overflow-x:auto; margin:0;"><code style="font-size:12px; font-family:ui-monospace,monospace; color:var(--ink);">{{ ex["import"] | e }}</code></pre>
|
||||
</div>
|
||||
<div aria-live="polite" class="copiaza-feedback muted" style="font-size:12px; min-height:20px; margin-top:4px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Panel Node #}
|
||||
<div role="tabpanel" id="panel-node" aria-labelledby="tab-node" hidden style="padding:16px 0 0;">
|
||||
{% set ex = exemple["node"] %}
|
||||
<div role="tablist" class="tab-bar" aria-label="Canal node" id="tl-node-canal" style="margin-bottom:8px; border-bottom:1px solid var(--line);">
|
||||
<button role="tab" id="tab-node-prez" aria-selected="true" aria-controls="panel-node-prez" tabindex="0" class="tab-link tab-activ" style="border:none; background:none; cursor:pointer; font-size:13px;">Prezentari JSON</button>
|
||||
<button role="tab" id="tab-node-import" aria-selected="false" aria-controls="panel-node-import" tabindex="-1" class="tab-link" style="border:none; background:none; cursor:pointer; font-size:13px;">Import fisier</button>
|
||||
</div>
|
||||
<div role="tabpanel" id="panel-node-prez" aria-labelledby="tab-node-prez">
|
||||
<div style="position:relative;">
|
||||
<button class="btn-copiaza" aria-label="Copiaza snippet node prezentari" style="position:absolute; top:8px; right:8px; font-size:12px; padding:4px 10px; background:var(--line); color:var(--ink); border:1px solid var(--line); border-radius:4px; cursor:pointer;">Copiaza</button>
|
||||
<pre style="background:var(--bg); border:1px solid var(--line); border-radius:6px; padding:16px; overflow-x:auto; margin:0;"><code style="font-size:12px; font-family:ui-monospace,monospace; color:var(--ink);">{{ ex["prezentari"] | e }}</code></pre>
|
||||
</div>
|
||||
<div aria-live="polite" class="copiaza-feedback muted" style="font-size:12px; min-height:20px; margin-top:4px;"></div>
|
||||
</div>
|
||||
<div role="tabpanel" id="panel-node-import" aria-labelledby="tab-node-import" hidden>
|
||||
<div style="position:relative;">
|
||||
<button class="btn-copiaza" aria-label="Copiaza snippet node import" style="position:absolute; top:8px; right:8px; font-size:12px; padding:4px 10px; background:var(--line); color:var(--ink); border:1px solid var(--line); border-radius:4px; cursor:pointer;">Copiaza</button>
|
||||
<pre style="background:var(--bg); border:1px solid var(--line); border-radius:6px; padding:16px; overflow-x:auto; margin:0;"><code style="font-size:12px; font-family:ui-monospace,monospace; color:var(--ink);">{{ ex["import"] | e }}</code></pre>
|
||||
</div>
|
||||
<div aria-live="polite" class="copiaza-feedback muted" style="font-size:12px; min-height:20px; margin-top:4px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Panel VFP: tab-list SECUNDAR pentru dialecte #}
|
||||
<div role="tabpanel" id="panel-vfp" aria-labelledby="tab-vfp" hidden style="padding:16px 0 0;">
|
||||
<p class="muted" style="font-size:13px; margin:0 0 8px;">Visual FoxPro — alege dialectul COM:</p>
|
||||
{# Dialecte VFP #}
|
||||
<div role="tablist" class="tab-bar" aria-label="Dialecte VFP" id="tl-vfp-dialect" style="margin-bottom:12px; border-bottom:1px solid var(--line);">
|
||||
<button role="tab" id="tab-vfp-msxml" aria-selected="true" aria-controls="panel-vfp-msxml" tabindex="0" class="tab-link tab-activ" style="border:none; background:none; cursor:pointer; font-size:13px;">MSXML2</button>
|
||||
<button role="tab" id="tab-vfp-winhttp" aria-selected="false" aria-controls="panel-vfp-winhttp" tabindex="-1" class="tab-link" style="border:none; background:none; cursor:pointer; font-size:13px;">WinHttp</button>
|
||||
</div>
|
||||
|
||||
{# MSXML2 #}
|
||||
<div role="tabpanel" id="panel-vfp-msxml" aria-labelledby="tab-vfp-msxml">
|
||||
{% set ex = exemple["vfp_msxml"] %}
|
||||
<div role="tablist" class="tab-bar" aria-label="Canal vfp msxml" id="tl-vfp-msxml-canal" style="margin-bottom:8px; border-bottom:1px solid var(--line);">
|
||||
<button role="tab" id="tab-vfp-msxml-prez" aria-selected="true" aria-controls="panel-vfp-msxml-prez" tabindex="0" class="tab-link tab-activ" style="border:none; background:none; cursor:pointer; font-size:12px;">Prezentari JSON</button>
|
||||
<button role="tab" id="tab-vfp-msxml-import" aria-selected="false" aria-controls="panel-vfp-msxml-import" tabindex="-1" class="tab-link" style="border:none; background:none; cursor:pointer; font-size:12px;">Import fisier</button>
|
||||
</div>
|
||||
<div role="tabpanel" id="panel-vfp-msxml-prez" aria-labelledby="tab-vfp-msxml-prez">
|
||||
<div style="position:relative;">
|
||||
<button class="btn-copiaza" aria-label="Copiaza snippet VFP MSXML2 prezentari" style="position:absolute; top:8px; right:8px; font-size:12px; padding:4px 10px; background:var(--line); color:var(--ink); border:1px solid var(--line); border-radius:4px; cursor:pointer;">Copiaza</button>
|
||||
<pre style="background:var(--bg); border:1px solid var(--line); border-radius:6px; padding:16px; overflow-x:auto; margin:0;"><code style="font-size:12px; font-family:ui-monospace,monospace; color:var(--ink);">{{ ex["prezentari"] | e }}</code></pre>
|
||||
</div>
|
||||
<div aria-live="polite" class="copiaza-feedback muted" style="font-size:12px; min-height:20px; margin-top:4px;"></div>
|
||||
</div>
|
||||
<div role="tabpanel" id="panel-vfp-msxml-import" aria-labelledby="tab-vfp-msxml-import" hidden>
|
||||
<div style="position:relative;">
|
||||
<button class="btn-copiaza" aria-label="Copiaza snippet VFP MSXML2 import" style="position:absolute; top:8px; right:8px; font-size:12px; padding:4px 10px; background:var(--line); color:var(--ink); border:1px solid var(--line); border-radius:4px; cursor:pointer;">Copiaza</button>
|
||||
<pre style="background:var(--bg); border:1px solid var(--line); border-radius:6px; padding:16px; overflow-x:auto; margin:0;"><code style="font-size:12px; font-family:ui-monospace,monospace; color:var(--ink);">{{ ex["import"] | e }}</code></pre>
|
||||
</div>
|
||||
<div aria-live="polite" class="copiaza-feedback muted" style="font-size:12px; min-height:20px; margin-top:4px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# WinHttp #}
|
||||
<div role="tabpanel" id="panel-vfp-winhttp" aria-labelledby="tab-vfp-winhttp" hidden>
|
||||
{% set ex = exemple["vfp_winhttp"] %}
|
||||
<div role="tablist" class="tab-bar" aria-label="Canal vfp winhttp" id="tl-vfp-winhttp-canal" style="margin-bottom:8px; border-bottom:1px solid var(--line);">
|
||||
<button role="tab" id="tab-vfp-winhttp-prez" aria-selected="true" aria-controls="panel-vfp-winhttp-prez" tabindex="0" class="tab-link tab-activ" style="border:none; background:none; cursor:pointer; font-size:12px;">Prezentari JSON</button>
|
||||
<button role="tab" id="tab-vfp-winhttp-import" aria-selected="false" aria-controls="panel-vfp-winhttp-import" tabindex="-1" class="tab-link" style="border:none; background:none; cursor:pointer; font-size:12px;">Import fisier</button>
|
||||
</div>
|
||||
<div role="tabpanel" id="panel-vfp-winhttp-prez" aria-labelledby="tab-vfp-winhttp-prez">
|
||||
<div style="position:relative;">
|
||||
<button class="btn-copiaza" aria-label="Copiaza snippet VFP WinHttp prezentari" style="position:absolute; top:8px; right:8px; font-size:12px; padding:4px 10px; background:var(--line); color:var(--ink); border:1px solid var(--line); border-radius:4px; cursor:pointer;">Copiaza</button>
|
||||
<pre style="background:var(--bg); border:1px solid var(--line); border-radius:6px; padding:16px; overflow-x:auto; margin:0;"><code style="font-size:12px; font-family:ui-monospace,monospace; color:var(--ink);">{{ ex["prezentari"] | e }}</code></pre>
|
||||
</div>
|
||||
<div aria-live="polite" class="copiaza-feedback muted" style="font-size:12px; min-height:20px; margin-top:4px;"></div>
|
||||
</div>
|
||||
<div role="tabpanel" id="panel-vfp-winhttp-import" aria-labelledby="tab-vfp-winhttp-import" hidden>
|
||||
<div style="position:relative;">
|
||||
<button class="btn-copiaza" aria-label="Copiaza snippet VFP WinHttp import" style="position:absolute; top:8px; right:8px; font-size:12px; padding:4px 10px; background:var(--line); color:var(--ink); border:1px solid var(--line); border-radius:4px; cursor:pointer;">Copiaza</button>
|
||||
<pre style="background:var(--bg); border:1px solid var(--line); border-radius:6px; padding:16px; overflow-x:auto; margin:0;"><code style="font-size:12px; font-family:ui-monospace,monospace; color:var(--ink);">{{ ex["import"] | e }}</code></pre>
|
||||
</div>
|
||||
<div aria-live="polite" class="copiaza-feedback muted" style="font-size:12px; min-height:20px; margin-top:4px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Card: Export & referinta #}
|
||||
<div class="card" style="margin-bottom:16px;">
|
||||
<h3 style="margin:0 0 12px; font-size:15px;">Export & referinta</h3>
|
||||
<div style="display:flex; flex-wrap:wrap; gap:8px;">
|
||||
<a class="cardlink" href="/docs" target="_blank" rel="noopener">Swagger UI — /docs</a>
|
||||
<a class="cardlink" href="/openapi.json" target="_blank" rel="noopener">Schema OpenAPI — /openapi.json</a>
|
||||
<a class="cardlink" href="/v1/integrare/postman.json" download>Colectie Postman — /v1/integrare/postman.json</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Formular test conexiune #}
|
||||
<div class="card" style="margin-bottom:16px;">
|
||||
<h3 style="margin:0 0 12px; font-size:15px;">Testeaza conexiunea</h3>
|
||||
<form hx-post="/integrare/test-cheie"
|
||||
hx-target="#integrare-test-rezultat"
|
||||
hx-swap="innerHTML"
|
||||
style="display:flex; gap:8px; flex-wrap:wrap; align-items:flex-end;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div>
|
||||
<label for="test-api-key" style="display:block; font-size:13px; color:var(--muted); margin-bottom:4px;">Cheie API (rfak_...)</label>
|
||||
<input type="password" id="test-api-key" name="api_key" placeholder="rfak_..."
|
||||
style="width:280px;" autocomplete="off">
|
||||
<p class="muted" style="font-size:12px; margin:4px 0 0;">Verificam doar daca cheia e valida. Nu o salvam si nu o memoram — cheia se gestioneaza in Cont.</p>
|
||||
</div>
|
||||
<button type="submit">Testeaza</button>
|
||||
</form>
|
||||
<div id="integrare-test-rezultat" style="margin-top:8px;"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
/* Navigare ARIA pentru tab-uri multiple (scoped pe containerul propriu). */
|
||||
var root = document.getElementById('integrare-section');
|
||||
if (!root) return;
|
||||
root.querySelectorAll('[role="tablist"]').forEach(function(tablist) {
|
||||
var tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
|
||||
if (!tabs.length) return;
|
||||
|
||||
/* Navigare cu sageti, Home, End */
|
||||
tablist.addEventListener('keydown', function(e) {
|
||||
var idx = tabs.indexOf(document.activeElement);
|
||||
if (idx === -1) return;
|
||||
var next = -1;
|
||||
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
||||
next = (idx + 1) % tabs.length;
|
||||
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||
next = (idx - 1 + tabs.length) % tabs.length;
|
||||
} else if (e.key === 'Home') {
|
||||
next = 0;
|
||||
} else if (e.key === 'End') {
|
||||
next = tabs.length - 1;
|
||||
}
|
||||
if (next !== -1) {
|
||||
e.preventDefault();
|
||||
tabs[next].focus();
|
||||
}
|
||||
});
|
||||
|
||||
/* Click pe tab: activeaza panelul corespunzator */
|
||||
tabs.forEach(function(tab) {
|
||||
tab.addEventListener('click', function() {
|
||||
var panelId = tab.getAttribute('aria-controls');
|
||||
if (!panelId) return;
|
||||
/* Dezactiveaza toate tab-urile din acest tablist */
|
||||
tabs.forEach(function(t) {
|
||||
t.setAttribute('aria-selected', 'false');
|
||||
t.setAttribute('tabindex', '-1');
|
||||
t.classList.remove('tab-activ');
|
||||
var pid = t.getAttribute('aria-controls');
|
||||
if (pid) {
|
||||
var p = document.getElementById(pid);
|
||||
if (p) p.hidden = true;
|
||||
}
|
||||
});
|
||||
/* Activeaza tab-ul curent */
|
||||
tab.setAttribute('aria-selected', 'true');
|
||||
tab.setAttribute('tabindex', '0');
|
||||
tab.classList.add('tab-activ');
|
||||
var panel = document.getElementById(panelId);
|
||||
if (panel) panel.hidden = false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* Buton Copiaza: citeste textul din <pre><code> sibling, nu din data-* */
|
||||
root.querySelectorAll('.btn-copiaza').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var wrapper = btn.closest('div[style*="position:relative"]') || btn.parentElement;
|
||||
var code = wrapper ? wrapper.querySelector('pre code') : null;
|
||||
if (!code) return;
|
||||
var text = code.innerText || code.textContent;
|
||||
var feedback = wrapper.parentElement && wrapper.parentElement.querySelector('.copiaza-feedback');
|
||||
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
btn.textContent = 'Copiat';
|
||||
setTimeout(function() { btn.textContent = 'Copiaza'; }, 2000);
|
||||
if (feedback) {
|
||||
feedback.textContent = 'Copiat!';
|
||||
setTimeout(function() { feedback.textContent = ''; }, 2000);
|
||||
}
|
||||
}).catch(function() {
|
||||
if (feedback) feedback.textContent = 'Eroare la copiere.';
|
||||
});
|
||||
} else {
|
||||
/* Fallback pentru browsere fara Clipboard API */
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.style.position = 'fixed';
|
||||
ta.style.opacity = '0';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
btn.textContent = 'Copiat';
|
||||
setTimeout(function() { btn.textContent = 'Copiaza'; }, 2000);
|
||||
if (feedback) {
|
||||
feedback.textContent = 'Copiat!';
|
||||
setTimeout(function() { feedback.textContent = ''; }, 2000);
|
||||
}
|
||||
} catch(err) {
|
||||
if (feedback) feedback.textContent = 'Eroare la copiere.';
|
||||
}
|
||||
document.body.removeChild(ta);
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
9
app/web/templates/_integrare_test_rezultat.html
Normal file
9
app/web/templates/_integrare_test_rezultat.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{% if succes %}
|
||||
<div class="flash" aria-live="polite" role="status">
|
||||
{{ mesaj }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="banner" aria-live="polite" role="alert">
|
||||
{{ mesaj }}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -21,12 +21,13 @@
|
||||
<!-- Tab-bar: navigare intre sectiuni -->
|
||||
<div role="tablist" class="tab-bar" aria-label="Sectiuni dashboard">
|
||||
{# US-003 (3.6): tab-ul "Trimiteri" (coada) a fost eliminat — Trimiterile traiesc
|
||||
ca sectiune permanenta pe Acasa. Raman: Acasa·Mapari·Cont·Nomenclator. #}
|
||||
ca sectiune permanenta pe Acasa. Raman: Acasa·Mapari·Cont·Nomenclator·Integrare. #}
|
||||
{% set tabs = [
|
||||
("acasa", "Acasa", "tab-acasa"),
|
||||
("mapari", "Mapari", "tab-mapari"),
|
||||
("cont", "Cont", "tab-cont"),
|
||||
("nomenclator", "Nomenclator", "tab-nomenclator")
|
||||
("nomenclator", "Nomenclator", "tab-nomenclator"),
|
||||
("integrare", "Integrare", "tab-integrare")
|
||||
] %}
|
||||
{% for tab_id, tab_label, tab_elem_id in tabs %}
|
||||
{% set badge = (badges.get(tab_id, 0) if badges else 0) %}
|
||||
@@ -52,40 +53,44 @@
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
/* Navigare cu sageti intre tab-uri (ARIA pattern) */
|
||||
var tablist = document.querySelector('[role="tablist"]');
|
||||
if (!tablist) return;
|
||||
var tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
|
||||
tablist.addEventListener('keydown', function(e) {
|
||||
var idx = tabs.indexOf(document.activeElement);
|
||||
if (idx === -1) return;
|
||||
var next = -1;
|
||||
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
||||
next = (idx + 1) % tabs.length;
|
||||
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||
next = (idx - 1 + tabs.length) % tabs.length;
|
||||
} else if (e.key === 'Home') {
|
||||
next = 0;
|
||||
} else if (e.key === 'End') {
|
||||
next = tabs.length - 1;
|
||||
}
|
||||
if (next !== -1) {
|
||||
e.preventDefault();
|
||||
tabs[next].focus();
|
||||
}
|
||||
});
|
||||
/* Navigare cu sageti intre tab-uri (ARIA pattern) — scoped pe fiecare tablist.
|
||||
Folosim querySelectorAll pentru a suporta multiple tablist-uri pe pagina
|
||||
(tab-bar principal + tab-urile interne din panoul Integrare). */
|
||||
document.querySelectorAll('[role="tablist"]').forEach(function(tablist) {
|
||||
var tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
|
||||
if (!tabs.length) return;
|
||||
|
||||
/* La click pe tab: actualizeaza aria-selected + tabindex */
|
||||
tabs.forEach(function(tab) {
|
||||
tab.addEventListener('click', function() {
|
||||
tabs.forEach(function(t) {
|
||||
t.setAttribute('aria-selected', 'false');
|
||||
t.setAttribute('tabindex', '-1');
|
||||
t.classList.remove('tab-activ');
|
||||
tablist.addEventListener('keydown', function(e) {
|
||||
var idx = tabs.indexOf(document.activeElement);
|
||||
if (idx === -1) return;
|
||||
var next = -1;
|
||||
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
||||
next = (idx + 1) % tabs.length;
|
||||
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||
next = (idx - 1 + tabs.length) % tabs.length;
|
||||
} else if (e.key === 'Home') {
|
||||
next = 0;
|
||||
} else if (e.key === 'End') {
|
||||
next = tabs.length - 1;
|
||||
}
|
||||
if (next !== -1) {
|
||||
e.preventDefault();
|
||||
tabs[next].focus();
|
||||
}
|
||||
});
|
||||
|
||||
/* La click pe tab: actualizeaza aria-selected + tabindex (scoped pe tablist-ul curent) */
|
||||
tabs.forEach(function(tab) {
|
||||
tab.addEventListener('click', function() {
|
||||
tabs.forEach(function(t) {
|
||||
t.setAttribute('aria-selected', 'false');
|
||||
t.setAttribute('tabindex', '-1');
|
||||
t.classList.remove('tab-activ');
|
||||
});
|
||||
tab.setAttribute('aria-selected', 'true');
|
||||
tab.setAttribute('tabindex', '0');
|
||||
tab.classList.add('tab-activ');
|
||||
});
|
||||
tab.setAttribute('aria-selected', 'true');
|
||||
tab.setAttribute('tabindex', '0');
|
||||
tab.classList.add('tab-activ');
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user