feat(import): T1 accounts.rar_creds_enc durabil + worker fallback + gate purjare

- worker: _creds_from_account(conn, account_id) — fallback la accounts.rar_creds_enc
  cand submission n-are creds (canal web fara re-pusher, restart worker)
- run(): creds = _creds_for(claimed, settings) OR _creds_from_account(conn, account_id)
- gate purjare (Voce#5): comentariu explicit — sterge DOAR submissions.rar_creds_enc,
  NU accounts.rar_creds_enc (inofensiv pt canal web, neatins pt canal API)
- POST /v1/conturi/rar-creds: seteaza creds durabile criptate Fernet per cont
- DELETE /v1/conturi/rar-creds: revenire la modelul efemer Treapta 1
- 7 teste: fallback, restart, coada mixta, endpoint set/delete, gate purjare

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-16 20:18:41 +00:00
parent 4ea21a034e
commit 12f0ca3a81
3 changed files with 322 additions and 2 deletions

View File

@@ -323,3 +323,48 @@ def create_mapare(
return {"saved": {"cod_op_service": req.cod_op_service.strip(), "cod_prestatie": cod}, "reresolve": stats}
finally:
conn.close()
class RarCredsIn(BaseModel):
"""Creds RAR durabile per-cont (D4). Stocate criptate (Fernet) in accounts.rar_creds_enc."""
email: str = Field(..., min_length=1)
password: str = Field(..., min_length=1, repr=False)
@router.post("/conturi/rar-creds")
def set_rar_creds(
req: RarCredsIn,
account_id: int = Depends(resolve_account_id),
) -> dict:
"""Seteaza creds RAR durabile per-cont (D4/T1).
Criptate Fernet in accounts.rar_creds_enc. Worker-ul le foloseste ca fallback
cand submission-ul nu mai are creds (canal web fara re-pusher, restart worker).
Contul vine din cheia API.
"""
acct = account_or_default(account_id)
enc = encrypt_creds({"email": req.email, "password": req.password})
conn = get_connection()
try:
conn.execute(
"UPDATE accounts SET rar_creds_enc=? WHERE id=?",
(enc, acct),
)
return {"ok": True, "account_id": acct}
finally:
conn.close()
@router.delete("/conturi/rar-creds")
def delete_rar_creds(
account_id: int = Depends(resolve_account_id),
) -> dict:
"""Sterge creds RAR durabile per-cont (revenire la modelul efemer Treapta 1)."""
acct = account_or_default(account_id)
conn = get_connection()
try:
conn.execute("UPDATE accounts SET rar_creds_enc=NULL WHERE id=?", (acct,))
return {"ok": True, "account_id": acct}
finally:
conn.close()

View File

@@ -267,7 +267,10 @@ class AccountSessions:
raise
self._sessions[account_id] = (rar, token)
write_heartbeat(conn, rar_login_ok=True, detail=f"login RAR ok (cont {account_id})")
# Creds nu mai sunt necesare: JWT acopera retry-urile -> sterge la rest.
# Creds efemere pe submissions nu mai sunt necesare: JWT acopera retry-urile -> sterge.
# GATE PURJARE (T1/Voce#5): sterge DOAR submissions.rar_creds_enc, NU accounts.rar_creds_enc.
# Canal web: fallback exista in accounts -> purjarea e inofensiva (re-login dupa restart).
# Canal API pur: purjarea e identica cu Treapta 1 (neatinsa).
conn.execute(
"UPDATE submissions SET rar_creds_enc=NULL WHERE account_id=? AND rar_creds_enc IS NOT NULL",
(account_id,),
@@ -303,6 +306,20 @@ def _creds_for(claimed: dict, settings: Settings) -> dict | None:
return None
def _creds_from_account(conn, account_id: int) -> dict | None:
"""Fallback T1/D4: crede RAR durabile per-cont din accounts.rar_creds_enc.
Canal web nu are re-pusher. Cand submission n-are creds (sterse dupa primul login
sau upload web fara creds), worker-ul re-citeste din cont si poate re-login oricand.
"""
row = conn.execute(
"SELECT rar_creds_enc FROM accounts WHERE id=?", (account_id,)
).fetchone()
if row and row["rar_creds_enc"]:
return decrypt_creds(row["rar_creds_enc"])
return None
def run() -> int:
signal.signal(signal.SIGTERM, _stop)
signal.signal(signal.SIGINT, _stop)
@@ -332,7 +349,9 @@ def run() -> int:
sid = claimed["id"]
account_id = claimed["account_id"]
creds = _creds_for(claimed, settings)
# T1/D4: incearca creds din submission (canal API efemer), cu fallback la
# accounts.rar_creds_enc (canal web durabil). Canal web n-are re-pusher.
creds = _creds_for(claimed, settings) or _creds_from_account(conn, account_id)
try:
token = sessions.get_token(conn, account_id, creds)