215 lines
8.6 KiB
Python
215 lines
8.6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Client generic pentru API-ul roa2web — autentificare + query-uri rapoarte.
|
|
|
|
Autentificare: user/parolă Oracle (ca la login-ul normal ROA) + 2FA email la
|
|
prima logare. După primul login cu trust_device=True, sesiunile următoare
|
|
folosesc refresh_token din keyring — fără OTP.
|
|
|
|
Credențiale (keyring, service "echo-core", vezi src/credential_store.py):
|
|
roa2web_user, roa2web_pass, roa2web_server,
|
|
roa2web_access_token, roa2web_refresh_token, roa2web_trusted_device_token
|
|
|
|
Firma implicită și base_url sunt în config.json -> "roa2web".
|
|
|
|
Usage:
|
|
python3 tools/roa2web_client.py summary [--company COD_SAU_NUME] [--luna N] [--an N]
|
|
python3 tools/roa2web_client.py companies
|
|
python3 tools/roa2web_client.py get <path> [--company COD_SAU_NUME] [--param k=v ...]
|
|
python3 tools/roa2web_client.py verify-2fa <cod> <email>
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from urllib.parse import urlsplit, urlunsplit
|
|
|
|
import requests
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
from src.credential_store import get_secret, set_secret
|
|
from src.config import Config
|
|
|
|
|
|
class ROA2WebError(Exception):
|
|
pass
|
|
|
|
|
|
class ROA2WebClient:
|
|
def __init__(self):
|
|
self.config = Config()
|
|
self.base_url = self.config.get("roa2web.base_url", "https://roa2web.romfast.ro/roa2web/api")
|
|
self.session = requests.Session()
|
|
self._token = get_secret("roa2web_access_token")
|
|
|
|
parsed = urlsplit(self.base_url)
|
|
self._origin = f"{parsed.scheme}://{parsed.netloc}"
|
|
# IIS/ARR reverse-proxy-ul din fata backend-ului serveste totul sub /roa2web,
|
|
# dar FastAPI nu stie de acest prefix -> redirect-urile lui (307, ex. trailing
|
|
# slash pe rutele definite la "/") vin fara el si pica in 404. Il reinjectam.
|
|
self._prefix = parsed.path[: -len("/api")] if parsed.path.endswith("/api") else parsed.path
|
|
|
|
@property
|
|
def default_company(self) -> str:
|
|
return self.config.get("roa2web.default_company", "114")
|
|
|
|
def _store_tokens(self, data: dict) -> None:
|
|
set_secret("roa2web_access_token", data["access_token"])
|
|
if data.get("refresh_token"):
|
|
set_secret("roa2web_refresh_token", data["refresh_token"])
|
|
if data.get("trusted_device_token"):
|
|
set_secret("roa2web_trusted_device_token", data["trusted_device_token"])
|
|
self._token = data["access_token"]
|
|
|
|
def _login_password(self) -> None:
|
|
user = get_secret("roa2web_user")
|
|
pw = get_secret("roa2web_pass")
|
|
server = get_secret("roa2web_server") or "romfast"
|
|
if not user or not pw:
|
|
raise ROA2WebError("Lipsesc roa2web_user/roa2web_pass din keyring (eco secrets set ...)")
|
|
|
|
payload = {"username": user, "password": pw, "server_id": server}
|
|
trusted = get_secret("roa2web_trusted_device_token")
|
|
if trusted:
|
|
payload["trusted_device_token"] = trusted
|
|
|
|
r = self.session.post(f"{self.base_url}/auth/login", json=payload, timeout=15)
|
|
r.raise_for_status()
|
|
data = r.json()
|
|
if data.get("requires_2fa"):
|
|
raise ROA2WebError(
|
|
f"Necesar cod OTP nou, trimis pe {data.get('masked_email')}. "
|
|
"Cere-i lui Marius codul, apoi ruleaza verify_2fa(code, email)."
|
|
)
|
|
self._store_tokens(data)
|
|
|
|
def _refresh(self) -> bool:
|
|
refresh_token = get_secret("roa2web_refresh_token")
|
|
if not refresh_token:
|
|
return False
|
|
r = self.session.post(
|
|
f"{self.base_url}/auth/refresh",
|
|
json={"refresh_token": refresh_token},
|
|
timeout=15,
|
|
)
|
|
if r.status_code != 200:
|
|
return False
|
|
self._store_tokens(r.json())
|
|
return True
|
|
|
|
def verify_2fa(self, code: str, email: str, server_id: str | None = None) -> dict:
|
|
server = server_id or get_secret("roa2web_server") or "romfast"
|
|
r = self.session.post(
|
|
f"{self.base_url}/auth/verify-2fa-code",
|
|
json={"code": code, "email": email, "server_id": server, "trust_device": True},
|
|
timeout=15,
|
|
)
|
|
r.raise_for_status()
|
|
data = r.json()
|
|
self._store_tokens(data)
|
|
return data
|
|
|
|
def request(self, method: str, path: str, params: dict | None = None, _retried: bool = False) -> dict:
|
|
if not self._token and not self._refresh():
|
|
self._login_password()
|
|
|
|
url = f"{self.base_url}{path}" if path.startswith("/") else f"{self.base_url}/{path}"
|
|
headers = {"Authorization": f"Bearer {self._token}"}
|
|
r = self.session.request(method, url, params=params, headers=headers, timeout=30, allow_redirects=False)
|
|
|
|
if r.status_code in (307, 308) and "Location" in r.headers:
|
|
loc = urlsplit(r.headers["Location"])
|
|
loc_path = loc.path
|
|
if self._prefix and not loc_path.startswith(self._prefix):
|
|
loc_path = self._prefix + loc_path
|
|
fixed_url = urlunsplit((urlsplit(self.base_url).scheme, urlsplit(self.base_url).netloc, loc_path, loc.query, ""))
|
|
r = self.session.request(method, fixed_url, headers=headers, timeout=30)
|
|
|
|
if r.status_code == 401 and not _retried:
|
|
if self._refresh() or (self._login_password() or True):
|
|
return self.request(method, path, params=params, _retried=True)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
def get(self, path: str, params: dict | None = None) -> dict:
|
|
return self.request("GET", path, params=params)
|
|
|
|
def companies(self) -> list:
|
|
return self.get("/companies").get("companies", [])
|
|
|
|
def dashboard_summary(self, company: str | None = None, luna: int | None = None, an: int | None = None) -> dict:
|
|
params = {"company": company or self.default_company}
|
|
if luna:
|
|
params["luna"] = luna
|
|
if an:
|
|
params["an"] = an
|
|
return self.get("/reports/dashboard/summary", params)
|
|
|
|
|
|
def resolve_company(client: ROA2WebClient, value: str | None) -> str:
|
|
"""Acceptă cod numeric sau (parte din) numele firmei; implicit firma din config."""
|
|
if not value:
|
|
return client.default_company
|
|
if value.isdigit():
|
|
return value
|
|
needle = value.lower()
|
|
matches = [c for c in client.companies() if needle in c["name"].lower()]
|
|
if not matches:
|
|
raise ROA2WebError(f"Firma '{value}' nu a fost gasita in lista de firme.")
|
|
if len(matches) > 1:
|
|
names = ", ".join(f"{c['id_firma']}={c['name']}" for c in matches)
|
|
raise ROA2WebError(f"'{value}' e ambiguu, se potriveste cu mai multe firme: {names}")
|
|
return str(matches[0]["id_firma"])
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Client CLI pentru roa2web API")
|
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
|
|
p = sub.add_parser("summary", help="Dashboard financiar (trezorerie, clienti, furnizori)")
|
|
p.add_argument("--company", help="Cod sau nume firma (implicit: cea din config.json)")
|
|
p.add_argument("--luna", type=int)
|
|
p.add_argument("--an", type=int)
|
|
|
|
sub.add_parser("companies", help="Lista firmelor disponibile")
|
|
|
|
p = sub.add_parser("get", help="Query generic pe orice endpoint /api/...")
|
|
p.add_argument("path", help='ex: "/reports/invoices" sau "/reports/treasury/bank-cash-register"')
|
|
p.add_argument("--company", help="Daca e dat, se adauga ca param 'company'")
|
|
p.add_argument("--param", action="append", default=[], metavar="k=v", help="Parametru query, repetabil")
|
|
|
|
v = sub.add_parser("verify-2fa", help="Finalizeaza login-ul cu un cod OTP primit pe email")
|
|
v.add_argument("code")
|
|
v.add_argument("email")
|
|
|
|
args = parser.parse_args()
|
|
client = ROA2WebClient()
|
|
|
|
try:
|
|
if args.cmd == "summary":
|
|
company = resolve_company(client, args.company)
|
|
data = client.dashboard_summary(company=company, luna=args.luna, an=args.an)
|
|
elif args.cmd == "companies":
|
|
for c in client.companies():
|
|
print(f"{c['id_firma']}\t{c['name']}")
|
|
return
|
|
elif args.cmd == "get":
|
|
params = dict(kv.split("=", 1) for kv in args.param)
|
|
if args.company:
|
|
params["company"] = resolve_company(client, args.company)
|
|
data = client.get(args.path, params or None)
|
|
elif args.cmd == "verify-2fa":
|
|
raw = client.verify_2fa(args.code, args.email)
|
|
data = {k: v for k, v in raw.items() if k not in ("access_token", "refresh_token", "trusted_device_token")}
|
|
except ROA2WebError as e:
|
|
print(f"Eroare: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|