feat(account): CLI lifecycle conturi + accounts.active (PRD 3.1)
Inlocuieste crearea conturilor prin INSERT SQL manual cu un tool admin dedicat, simetric cu tools/apikey.py. Fundatia Etapei 3 (3.2/3.3). - app/accounts.py: create_account/set_active/list_accounts (helper pur, partajat CLI + viitor flux web 3.3). Normalizeaza CUI (trim+upper), prinde IntegrityError -> ValueError cu cauza+fix. - accounts.active (lifecycle cont) + index unic partial ux_accounts_cui (unicitate la nivel de index, fara fereastra de coliziune). Migrare idempotenta in _migrate. - tools/account.py: create (--name/--cui/--inactive/--with-key atomic), list [--pending], activate/deactivate --account N. Erori -> exit 2. - 20 teste noi (12 helper + 8 CLI); suita 299 passed. active e inert pana la gate-ul worker din 3.3 (documentat). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
70
app/accounts.py
Normal file
70
app/accounts.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""Lifecycle conturi ROAAUTO (admin, fara suprafata HTTP).
|
||||
|
||||
Functii pure de creare/listare/(de)activare cont, partajate intre CLI
|
||||
(`tools/account.py`, Etapa 3.1) si fluxul web de self-onboarding (Etapa 3.3,
|
||||
care reuseaza `create_account` + `active`). Identitatea de login (email/parola)
|
||||
NU traieste aici — apartine 3.3.
|
||||
|
||||
NOTA lifecycle `active`: coloana `accounts.active` este un flag de lifecycle
|
||||
consumat de 3.3 (gate „cont in asteptare", `active=0`). Pana la gate-ul worker din
|
||||
3.3, `active=0` NU opreste trimiterile (worker-ul nu citeste contul, doar
|
||||
`api_keys.active`). `deactivate` marcheaza intentia administrativa; nu blocheaza
|
||||
inca fluxul de trimitere. (Addendum A2.)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
|
||||
|
||||
def _norm_cui(cui: str | None) -> str | None:
|
||||
"""trim + upper; sir gol -> None (tratat ca „fara CUI")."""
|
||||
if cui is None:
|
||||
return None
|
||||
cui = cui.strip().upper()
|
||||
return cui or None
|
||||
|
||||
|
||||
def create_account(
|
||||
conn: sqlite3.Connection, name: str, cui: str | None = None, active: bool = True
|
||||
) -> int:
|
||||
"""Insereaza un cont si intoarce id-ul nou (AUTOINCREMENT, deci >=2 — nu atinge default id=1).
|
||||
|
||||
`name` gol/whitespace -> ValueError. `cui` se normalizeaza (trim+upper); un CUI
|
||||
deja folosit -> ValueError cu cauza+fix. Unicitatea e impusa de indexul partial
|
||||
`ux_accounts_cui` (nu de un check separat), deci e sigura la concurenta.
|
||||
"""
|
||||
name = (name or "").strip()
|
||||
if not name:
|
||||
raise ValueError("name gol (un cont are nevoie de nume)")
|
||||
cui = _norm_cui(cui)
|
||||
try:
|
||||
cur = conn.execute(
|
||||
"INSERT INTO accounts (name, cui, active) VALUES (?, ?, ?)",
|
||||
(name, cui, 1 if active else 0),
|
||||
)
|
||||
except sqlite3.IntegrityError:
|
||||
existing = conn.execute("SELECT id FROM accounts WHERE cui=?", (cui,)).fetchone()
|
||||
owner = existing["id"] if existing else "?"
|
||||
raise ValueError(
|
||||
f"CUI {cui} e deja folosit de contul {owner} "
|
||||
f"(foloseste 'activate --account {owner}' sau alt CUI)"
|
||||
)
|
||||
return int(cur.lastrowid or 0)
|
||||
|
||||
|
||||
def set_active(conn: sqlite3.Connection, account_id: int, active: bool) -> None:
|
||||
"""Comuta `accounts.active`. Idempotent (set activ pe activ nu arunca).
|
||||
Cont inexistent -> ValueError."""
|
||||
row = conn.execute("SELECT 1 FROM accounts WHERE id=?", (account_id,)).fetchone()
|
||||
if not row:
|
||||
raise ValueError(f"cont inexistent: {account_id}")
|
||||
conn.execute("UPDATE accounts SET active=? WHERE id=?", (1 if active else 0, account_id))
|
||||
|
||||
|
||||
def list_accounts(conn: sqlite3.Connection) -> list[dict]:
|
||||
"""Metadate conturi (FARA `rar_creds_enc`), ordonate dupa id."""
|
||||
rows = conn.execute(
|
||||
"SELECT id, name, cui, active, created_at FROM accounts ORDER BY id"
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
@@ -60,6 +60,13 @@ def _migrate(conn: sqlite3.Connection) -> None:
|
||||
acc_cols = {r["name"] for r in conn.execute("PRAGMA table_info(accounts)").fetchall()}
|
||||
if "rar_creds_enc" not in acc_cols:
|
||||
conn.execute("ALTER TABLE accounts ADD COLUMN rar_creds_enc TEXT")
|
||||
if "active" not in acc_cols:
|
||||
# Conturi existente raman active (default 1). Lifecycle consumat de 3.3.
|
||||
conn.execute("ALTER TABLE accounts ADD COLUMN active INTEGER NOT NULL DEFAULT 1")
|
||||
# Unicitate CUI (un CUI = un cont); NULL distinct nativ -> conturi fara CUI multiplu.
|
||||
conn.execute(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS ux_accounts_cui ON accounts(cui) WHERE cui IS NOT NULL"
|
||||
)
|
||||
|
||||
# Index batch_id pe submissions (poate lipsi pe DB veche)
|
||||
existing_idx = {r["name"] for r in conn.execute(
|
||||
|
||||
@@ -10,9 +10,14 @@ CREATE TABLE IF NOT EXISTS accounts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
cui TEXT,
|
||||
active INTEGER NOT NULL DEFAULT 1, -- lifecycle cont (3.1); gate „in asteptare" consumat de 3.3
|
||||
rar_creds_enc TEXT, -- creds RAR criptate (Fernet) durabile per-cont (D4/Eng#1)
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
-- Un CUI = un cont (cand e prezent). NULL ramane distinct nativ in SQLite -> conturi
|
||||
-- fara CUI (ex. default) se pot crea multiplu. Unicitate la nivel de index (nu check
|
||||
-- in helper) ca sa nu existe fereastra de coliziune intre doi create_account concurenti.
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_accounts_cui ON accounts(cui) WHERE cui IS NOT NULL;
|
||||
-- Cont implicit (id=1): auth API-key (CORE) inca neimplementat, deci ingestiile vin
|
||||
-- cu account_id NULL. Le atribuim contului default ca FK + UNIQUE(account_id,...) din
|
||||
-- operations_mapping sa fie valide; cand auth livreaza, account_id real va curge natural.
|
||||
|
||||
@@ -48,7 +48,7 @@ Reguli de contract (detalii in `docs/api-rar-contract.md`): `FINALIZATA` e termi
|
||||
> PRD-uri (`docs/prd/prd-X.Y-*.md`), linkate in coloana Detalii. La fiecare livrabila terminata:
|
||||
> schimba statusul + data + linkul PRD si actualizeaza "Ultima actualizare".
|
||||
|
||||
**Ultima actualizare**: 2026-06-17 — Etapa 3 PLANIFICATA: 3 PRD-uri scrise + autoplan (`[subagent-only]`, Codex usage-limit) + aprobate. Urmeaza EXECUTE, incepand cu 3.1. 3.3 a crescut (7→12 stories: +CSRF, +interfata admin web, +email).
|
||||
**Ultima actualizare**: 2026-06-17 — 3.1 LIVRAT (CLI `tools/account.py` + `accounts.active` + index unic CUI + helper-e `app/accounts.py`; 299 teste pass). Urmeaza 3.2. Deferat din 3.1 (P3, fara SQL manual): `rename`/`set-cui` (corectie typo), `--if-not-exists` (provisioning idempotent); `set-password --account N` se implementeaza in 3.3 cu `app/users.py`.
|
||||
|
||||
### Etapa 1 — Canal API ROAAUTO (Treapta 1)
|
||||
|
||||
@@ -72,7 +72,7 @@ Reguli de contract (detalii in `docs/api-rar-contract.md`): `FINALIZATA` e termi
|
||||
|
||||
| # | Livrabila | Status | Data | Detalii |
|
||||
|---|-----------|--------|------|---------|
|
||||
| 3.1 | Creare cont nou (CLI dedicat) | TODO (PRD aprobat) | | CLI `tools/account.py` + `accounts.active`. PRD: [prd-3.1](prd/prd-3.1-creare-cont.md) |
|
||||
| 3.1 | Creare cont nou (CLI dedicat) | DONE | 2026-06-17 | CLI `tools/account.py` (create/list[--pending]/activate/deactivate, `--with-key` atomic) + `accounts.active` + index unic CUI + `app/accounts.py`. 20 teste noi. PRD: [prd-3.1](prd/prd-3.1-creare-cont.md) |
|
||||
| 3.2 | Filtrare pe cont a GET-urilor de listare | TODO (PRD aprobat) | | scope cheie pe `/v1/prezentari`, `/v1/mapari`, `/v1/audit/export`; nomenclator global. PRD: [prd-3.2](prd/prd-3.2-filtrare-cont-get.md) |
|
||||
| 3.3 | Self-onboarding web + interfata admin | TODO (PRD aprobat) | | signup/login/sesiuni + cont "in asteptare" + gate worker + CSRF + panou admin web + email. 12 stories. PRD: [prd-3.3](prd/prd-3.3-self-onboarding-web.md) |
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# PRD 3.1 — Creare cont nou
|
||||
|
||||
**Stare**: aprobat
|
||||
**Stare**: verify-pass
|
||||
|
||||
> 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).
|
||||
@@ -148,5 +148,30 @@ nu blocheaza livrabila.
|
||||
|
||||
## Raport VERIFY
|
||||
|
||||
> Completat de subagentul verificator (context curat) in faza VERIFY — vezi ROADMAP §5.6.
|
||||
> PASS/FAIL per criteriu, cu dovezi. Lipseste pana la VERIFY.
|
||||
> Executat 2026-06-17. Suita completa: **299 passed** (20 teste noi: 12 helper + 8 CLI).
|
||||
|
||||
**US-001 — PASS**
|
||||
- [x] `accounts.active INTEGER NOT NULL DEFAULT 1` in `schema.sql` + migrat idempotent in `_migrate`
|
||||
(conturi existente raman active; default id=1 activ). Dovada: `test_foundation` + migrare ALTER.
|
||||
- [x] `create_account(conn, name, cui=None, active=True) -> int` insereaza + intoarce id.
|
||||
`test_create_account_returneaza_id`.
|
||||
- [x] `name` gol/whitespace → `ValueError`, nu insereaza. `test_create_account_name_gol_ridica_eroare`.
|
||||
- [x] `cui` duplicat → `ValueError` (cauza+fix, numeste contul); `cui=None` multiplu OK.
|
||||
`test_create_account_cui_duplicat_respins`, `test_create_cui_null_multiplu_permis`.
|
||||
- [x] `set_active` comuta; inexistent → `ValueError`; idempotent. `test_set_active_*`.
|
||||
- [x] `list_accounts` → `id,name,cui,active,created_at`, ordonat, FARA `rar_creds_enc`.
|
||||
`test_list_accounts_ordonat_fara_creds`.
|
||||
|
||||
**US-002 — PASS** (E2E PRD reprodus: `create --inactive --with-key` → id=2 + cheie; `activate` → list `activ=da`)
|
||||
- [x] `create --name [--cui] [--inactive]` creeaza (implicit activ), tipareste id. `test_create_afiseaza_id`.
|
||||
- [x] `--with-key` emite cheie afisata o data, atomic (rollback pe esec). `test_create_with_key_emite_cheie`,
|
||||
`test_with_key_atomic_pe_cui_duplicat`.
|
||||
- [x] `activate`/`deactivate --account N` comuta. `test_activate_comuta_starea`.
|
||||
- [x] erori → stderr + exit 2. `test_create_cui_duplicat_exit_2`, `test_activate_inexistent_exit_2`.
|
||||
- [x] `list` tipareste tabelul; `list --pending` filtreaza. `test_list_afiseaza_activ`, `test_list_pending_filtreaza`.
|
||||
- [x] `init_db()` la start.
|
||||
|
||||
**Addendum** — A1 (index unic partial `ux_accounts_cui` + normalizare trim/upper, `test_create_cui_normalizat`),
|
||||
A3 (`--account` pe activate/deactivate), A4 (mesaj cauza+fix), A5 (`--with-key` atomic), A6 (`list --pending`;
|
||||
`set-password` deferat la 3.3), A7 (teste RED suplimentare) — toate aplicate. A2 (`active` inert pana la 3.3)
|
||||
documentat in `app/accounts.py` + `tools/account.py`.
|
||||
|
||||
115
tests/test_accounts.py
Normal file
115
tests/test_accounts.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""Teste US-001 (PRD 3.1): coloana accounts.active + helper-e cont in app/accounts.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def conn(monkeypatch):
|
||||
tmp = tempfile.mkdtemp()
|
||||
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "test_accounts.db"))
|
||||
from app.config import get_settings
|
||||
get_settings.cache_clear()
|
||||
from app.db import get_connection, init_db
|
||||
init_db()
|
||||
c = get_connection()
|
||||
yield c
|
||||
c.close()
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
def test_create_account_returneaza_id(conn):
|
||||
from app.accounts import create_account
|
||||
acct_id = create_account(conn, "Service X")
|
||||
assert isinstance(acct_id, int)
|
||||
# AUTOINCREMENT peste default id=1 -> primul cont creat are id>=2 (nu atinge default).
|
||||
assert acct_id >= 2
|
||||
|
||||
|
||||
def test_create_account_activ_implicit(conn):
|
||||
from app.accounts import create_account
|
||||
acct_id = create_account(conn, "Service X")
|
||||
row = conn.execute("SELECT active FROM accounts WHERE id=?", (acct_id,)).fetchone()
|
||||
assert row["active"] == 1
|
||||
|
||||
|
||||
def test_create_account_inactiv(conn):
|
||||
from app.accounts import create_account
|
||||
acct_id = create_account(conn, "Service X", active=False)
|
||||
row = conn.execute("SELECT active FROM accounts WHERE id=?", (acct_id,)).fetchone()
|
||||
assert row["active"] == 0
|
||||
|
||||
|
||||
def test_create_account_name_gol_ridica_eroare(conn):
|
||||
from app.accounts import create_account
|
||||
with pytest.raises(ValueError):
|
||||
create_account(conn, " ")
|
||||
# nu a inserat nimic peste default
|
||||
n = conn.execute("SELECT COUNT(*) AS n FROM accounts").fetchone()["n"]
|
||||
assert n == 1
|
||||
|
||||
|
||||
def test_create_account_cui_duplicat_respins(conn):
|
||||
from app.accounts import create_account
|
||||
first = create_account(conn, "Service A", cui="RO123")
|
||||
with pytest.raises(ValueError) as exc:
|
||||
create_account(conn, "Service B", cui="RO123")
|
||||
# mesaj cu cauza + fix care numeste contul existent (A4)
|
||||
msg = str(exc.value)
|
||||
assert "RO123" in msg
|
||||
assert str(first) in msg
|
||||
|
||||
|
||||
def test_create_cui_null_multiplu_permis(conn):
|
||||
from app.accounts import create_account
|
||||
a = create_account(conn, "Fara CUI 1")
|
||||
b = create_account(conn, "Fara CUI 2")
|
||||
assert a != b
|
||||
|
||||
|
||||
def test_create_cui_normalizat(conn):
|
||||
from app.accounts import create_account
|
||||
create_account(conn, "Service A", cui=" ro123 ")
|
||||
# normalizat la RO123 -> duplicat respins indiferent de spatii/caz
|
||||
with pytest.raises(ValueError):
|
||||
create_account(conn, "Service B", cui="RO123")
|
||||
row = conn.execute("SELECT cui FROM accounts WHERE name='Service A'").fetchone()
|
||||
assert row["cui"] == "RO123"
|
||||
|
||||
|
||||
def test_set_active_comuta(conn):
|
||||
from app.accounts import create_account, set_active
|
||||
acct_id = create_account(conn, "Service X")
|
||||
set_active(conn, acct_id, False)
|
||||
assert conn.execute("SELECT active FROM accounts WHERE id=?", (acct_id,)).fetchone()["active"] == 0
|
||||
set_active(conn, acct_id, True)
|
||||
assert conn.execute("SELECT active FROM accounts WHERE id=?", (acct_id,)).fetchone()["active"] == 1
|
||||
|
||||
|
||||
def test_set_active_idempotent(conn):
|
||||
from app.accounts import create_account, set_active
|
||||
acct_id = create_account(conn, "Service X") # deja activ
|
||||
set_active(conn, acct_id, True) # nu trebuie sa arunce
|
||||
assert conn.execute("SELECT active FROM accounts WHERE id=?", (acct_id,)).fetchone()["active"] == 1
|
||||
|
||||
|
||||
def test_set_active_inexistent_ridica(conn):
|
||||
from app.accounts import set_active
|
||||
with pytest.raises(ValueError):
|
||||
set_active(conn, 9999, True)
|
||||
|
||||
|
||||
def test_list_accounts_ordonat_fara_creds(conn):
|
||||
from app.accounts import create_account, list_accounts
|
||||
create_account(conn, "Service B")
|
||||
create_account(conn, "Service A")
|
||||
rows = list_accounts(conn)
|
||||
ids = [r["id"] for r in rows]
|
||||
assert ids == sorted(ids)
|
||||
for r in rows:
|
||||
assert "rar_creds_enc" not in r
|
||||
assert set(r.keys()) == {"id", "name", "cui", "active", "created_at"}
|
||||
124
tests/test_tools_account.py
Normal file
124
tests/test_tools_account.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""Teste US-002 (PRD 3.1): CLI tools/account.py (create/list/activate/deactivate)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def env(monkeypatch):
|
||||
tmp = tempfile.mkdtemp()
|
||||
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "test_tools_account.db"))
|
||||
from app.config import get_settings
|
||||
get_settings.cache_clear()
|
||||
yield
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
def _run(argv):
|
||||
from tools.account import main
|
||||
return main(argv)
|
||||
|
||||
|
||||
def test_create_afiseaza_id(env, capsys):
|
||||
rc = _run(["create", "--name", "Service X"])
|
||||
out = capsys.readouterr().out
|
||||
assert rc == 0
|
||||
assert "id=2" in out
|
||||
assert "activ=da" in out
|
||||
|
||||
|
||||
def test_create_inactive_in_asteptare(env, capsys):
|
||||
rc = _run(["create", "--name", "Service X", "--inactive"])
|
||||
out = capsys.readouterr().out
|
||||
assert rc == 0
|
||||
assert "activ=nu" in out
|
||||
|
||||
|
||||
def test_create_with_key_emite_cheie(env, capsys):
|
||||
rc = _run(["create", "--name", "Service X", "--with-key"])
|
||||
out = capsys.readouterr().out
|
||||
assert rc == 0
|
||||
assert "rfak_" in out
|
||||
# cheia exista in DB pentru contul nou
|
||||
from app.db import get_connection
|
||||
conn = get_connection()
|
||||
try:
|
||||
n = conn.execute(
|
||||
"SELECT COUNT(*) AS n FROM api_keys k JOIN accounts a ON a.id=k.account_id "
|
||||
"WHERE a.name='Service X'"
|
||||
).fetchone()["n"]
|
||||
finally:
|
||||
conn.close()
|
||||
assert n == 1
|
||||
|
||||
|
||||
def test_create_cui_duplicat_exit_2(env, capsys):
|
||||
assert _run(["create", "--name", "Service A", "--cui", "RO123"]) == 0
|
||||
rc = _run(["create", "--name", "Service B", "--cui", "RO123"])
|
||||
err = capsys.readouterr().err
|
||||
assert rc == 2
|
||||
assert "RO123" in err
|
||||
|
||||
|
||||
def test_with_key_atomic_pe_cui_duplicat(env, capsys):
|
||||
# cont initial care ocupa CUI
|
||||
assert _run(["create", "--name", "Service A", "--cui", "RO123"]) == 0
|
||||
capsys.readouterr()
|
||||
# --with-key pe CUI duplicat: rollback -> niciun cont B, nicio cheie orfana
|
||||
rc = _run(["create", "--name", "Service B", "--cui", "RO123", "--with-key"])
|
||||
assert rc == 2
|
||||
from app.db import get_connection
|
||||
conn = get_connection()
|
||||
try:
|
||||
assert conn.execute("SELECT COUNT(*) AS n FROM accounts WHERE name='Service B'").fetchone()["n"] == 0
|
||||
# o singura cheie n-a fost emisa (doar contul A nu are cheie)
|
||||
assert conn.execute("SELECT COUNT(*) AS n FROM api_keys").fetchone()["n"] == 0
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_activate_comuta_starea(env, capsys):
|
||||
_run(["create", "--name", "Service X", "--inactive"])
|
||||
capsys.readouterr()
|
||||
assert _run(["deactivate", "--account", "2"]) == 0
|
||||
assert _run(["activate", "--account", "2"]) == 0
|
||||
capsys.readouterr()
|
||||
rc = _run(["list"])
|
||||
out = capsys.readouterr().out
|
||||
assert rc == 0
|
||||
assert "Service X" in out
|
||||
# contul 2 apare activ
|
||||
line = [ln for ln in out.splitlines() if "Service X" in ln][0]
|
||||
assert "da" in line
|
||||
|
||||
|
||||
def test_activate_inexistent_exit_2(env, capsys):
|
||||
rc = _run(["activate", "--account", "9999"])
|
||||
err = capsys.readouterr().err
|
||||
assert rc == 2
|
||||
assert "inexistent" in err
|
||||
|
||||
|
||||
def test_list_afiseaza_activ(env, capsys):
|
||||
_run(["create", "--name", "Service X"])
|
||||
capsys.readouterr()
|
||||
rc = _run(["list"])
|
||||
out = capsys.readouterr().out
|
||||
assert rc == 0
|
||||
assert "Service X" in out
|
||||
assert "activ" in out # antet tabel
|
||||
|
||||
|
||||
def test_list_pending_filtreaza(env, capsys):
|
||||
_run(["create", "--name", "Activ SRL"])
|
||||
_run(["create", "--name", "Asteptare SRL", "--inactive"])
|
||||
capsys.readouterr()
|
||||
rc = _run(["list", "--pending"])
|
||||
out = capsys.readouterr().out
|
||||
assert rc == 0
|
||||
assert "Asteptare SRL" in out
|
||||
assert "Activ SRL" not in out
|
||||
124
tools/account.py
Normal file
124
tools/account.py
Normal file
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
"""CLI lifecycle conturi ROAAUTO (admin gateway).
|
||||
|
||||
Onboardeaza/activeaza un client fara INSERT SQL manual, simetric cu
|
||||
`tools/apikey.py`. Adminul ruleaza pe masina gateway — nicio suprafata HTTP de
|
||||
admin (admin web vine in 3.3). Optional emite si prima cheie API intr-un pas
|
||||
(`--with-key`), atomic cu crearea contului.
|
||||
|
||||
NOTA: `deactivate` comuta `accounts.active` (lifecycle), dar NU opreste inca
|
||||
trimiterile — gate-ul worker pe `active` apartine 3.3. Vezi `app/accounts.py`.
|
||||
|
||||
Utilizare:
|
||||
python -m tools.account create --name "Service X" [--cui RO123] [--inactive] [--with-key]
|
||||
python -m tools.account list [--pending]
|
||||
python -m tools.account activate --account 2
|
||||
python -m tools.account deactivate --account 2
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
from app.accounts import create_account, list_accounts, set_active
|
||||
from app.auth import create_api_key
|
||||
from app.db import get_connection, init_db
|
||||
|
||||
|
||||
def _create(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
||||
active = not args.inactive
|
||||
if not args.with_key:
|
||||
try:
|
||||
acct_id = create_account(conn, args.name, args.cui, active=active)
|
||||
except ValueError as exc:
|
||||
print(f"eroare: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
print(f"Cont creat: id={acct_id} (activ={'da' if active else 'nu'})")
|
||||
return 0
|
||||
|
||||
# --with-key: cont + cheie in aceeasi tranzactie (DB ruleaza autocommit).
|
||||
conn.execute("BEGIN IMMEDIATE")
|
||||
try:
|
||||
acct_id = create_account(conn, args.name, args.cui, active=active)
|
||||
key = create_api_key(conn, acct_id)
|
||||
conn.execute("COMMIT")
|
||||
except ValueError as exc:
|
||||
conn.execute("ROLLBACK")
|
||||
print(f"eroare: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
except Exception:
|
||||
conn.execute("ROLLBACK")
|
||||
raise
|
||||
print(f"Cont creat: id={acct_id} (activ={'da' if active else 'nu'})")
|
||||
print("Cheie API (pastreaz-o, nu se mai afiseaza):")
|
||||
print(key)
|
||||
return 0
|
||||
|
||||
|
||||
def _set_active(conn: sqlite3.Connection, account_id: int, active: bool) -> int:
|
||||
try:
|
||||
set_active(conn, account_id, active)
|
||||
except ValueError as exc:
|
||||
print(f"eroare: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
print(f"Cont {account_id}: activ={'da' if active else 'nu'}")
|
||||
return 0
|
||||
|
||||
|
||||
def _list(conn: sqlite3.Connection, pending_only: bool) -> int:
|
||||
rows = list_accounts(conn)
|
||||
if pending_only:
|
||||
rows = [r for r in rows if not r["active"]]
|
||||
if not rows:
|
||||
print("(niciun cont in asteptare)" if pending_only else "(niciun cont)")
|
||||
return 0
|
||||
print(f"{'id':>4} {'activ':>5} {'cui':<14} {'creat':<20} nume")
|
||||
for r in rows:
|
||||
print(
|
||||
f"{r['id']:>4} {('da' if r['active'] else 'nu'):>5} "
|
||||
f"{(r['cui'] or ''):<14} {(r['created_at'] or ''):<20} {r['name']}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description="Lifecycle conturi gateway RAR AUTOPASS")
|
||||
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
p_create = sub.add_parser("create", help="creeaza un cont nou")
|
||||
p_create.add_argument("--name", required=True, help="nume cont (service)")
|
||||
p_create.add_argument("--cui", default=None, help="CUI (unic cand e prezent)")
|
||||
p_create.add_argument("--inactive", action="store_true", help="creeaza cont in asteptare (active=0)")
|
||||
p_create.add_argument("--with-key", action="store_true", help="emite si prima cheie API (atomic)")
|
||||
|
||||
p_list = sub.add_parser("list", help="listeaza conturi")
|
||||
p_list.add_argument("--pending", action="store_true", help="doar conturi in asteptare (active=0)")
|
||||
|
||||
p_act = sub.add_parser("activate", help="activeaza un cont")
|
||||
p_act.add_argument("--account", type=int, required=True, help="account_id")
|
||||
|
||||
p_deact = sub.add_parser("deactivate", help="dezactiveaza un cont")
|
||||
p_deact.add_argument("--account", type=int, required=True, help="account_id")
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
init_db() # asigura schema (accounts.active + index CUI) + cont default
|
||||
conn = get_connection()
|
||||
try:
|
||||
if args.cmd == "create":
|
||||
return _create(conn, args)
|
||||
if args.cmd == "list":
|
||||
return _list(conn, args.pending)
|
||||
if args.cmd == "activate":
|
||||
return _set_active(conn, args.account, True)
|
||||
if args.cmd == "deactivate":
|
||||
return _set_active(conn, args.account, False)
|
||||
finally:
|
||||
conn.close()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user