Files
rar-autopass/app/db.py
Claude Agent fbb2695336 feat(creds): livrare creds per-cerere la worker (criptat efemer + sesiuni per-cont)
Plan sect.5: parola RAR vine per-cerere, stocata CRIPTATA in submission pana la
primul login reusit pe cont, apoi stearsa; JWT 30h acopera restul.

- app/crypto.py: Fernet, cheie din AUTOPASS_creds_key (nesetata -> efemera la
  runtime, creds nu supravietuiesc restartului). encrypt/decrypt_creds.
- schema + migrare: submissions.rar_creds_enc (creds criptate).
- ingestie: cripteaza rar_credentials, le lipeste de fiecare submission nou.
  Niciodata in clar in DB.
- worker: AccountSessions (login per-cont cu creds decriptate, cache JWT in
  memorie, sterge creds-urile contului dupa primul login + refresh nomenclator).
  401 creds gresite -> error fara retry; token expirat -> invalidare + requeue;
  fara creds (restart) -> requeue "indisponibile" (ROAAUTO re-trimite).
  claim_one intoarce account_id + creds_enc; recover_orphans filtrabil pe cont.
- requirements: cryptography==46.0.5.

Nota: refresh nomenclator e acum lazy la primul login per-cont (nu la pornire);
seed-ul fallback acopera editorul offline.

10 teste noi (tests/test_creds_delivery.py). 95 pass total.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 20:16:16 +00:00

78 lines
2.8 KiB
Python

"""Acces SQLite (WAL). Conexiune per-thread, schema idempotenta, heartbeat worker."""
from __future__ import annotations
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
from .config import get_settings
_SCHEMA = Path(__file__).resolve().parent / "schema.sql"
def _connect(db_path: Path) -> sqlite3.Connection:
db_path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(db_path, timeout=15.0, isolation_level=None) # autocommit; tranzactii explicite
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode = WAL")
conn.execute("PRAGMA foreign_keys = ON")
conn.execute("PRAGMA busy_timeout = 15000")
return conn
def get_connection() -> sqlite3.Connection:
"""Conexiune noua catre baza configurata. Apelantul o inchide."""
return _connect(get_settings().db_path)
def init_db() -> None:
"""Creeaza schema daca lipseste + migrari aditive. Idempotent — sigur la fiecare boot."""
conn = get_connection()
try:
conn.executescript(_SCHEMA.read_text(encoding="utf-8"))
_migrate(conn)
# Seed fallback nomenclator (doar daca e gol) ca editorul de mapari + fuzzy
# sa mearga inainte ca worker-ul sa fi luat lista live din RAR.
from .mapping import seed_nomenclator_if_empty
seed_nomenclator_if_empty(conn)
finally:
conn.close()
def _migrate(conn: sqlite3.Connection) -> None:
"""Migrari aditive pentru DB create inainte de o coloana noua (CREATE IF NOT EXISTS nu altereaza)."""
cols = {r["name"] for r in conn.execute("PRAGMA table_info(submissions)").fetchall()}
if "next_attempt_at" not in cols:
conn.execute("ALTER TABLE submissions ADD COLUMN next_attempt_at TEXT")
if "rar_creds_enc" not in cols:
conn.execute("ALTER TABLE submissions ADD COLUMN rar_creds_enc TEXT")
def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat(timespec="seconds")
def write_heartbeat(conn: sqlite3.Connection, *, rar_login_ok: bool = False, detail: str = "") -> None:
"""Worker bate la fiecare iteratie. last_rar_login_ok se actualizeaza doar la login reusit."""
if rar_login_ok:
conn.execute(
"UPDATE worker_heartbeat SET last_beat=?, last_rar_login_ok=?, detail=? WHERE id=1",
(_now_iso(), _now_iso(), detail),
)
else:
conn.execute(
"UPDATE worker_heartbeat SET last_beat=?, detail=? WHERE id=1",
(_now_iso(), detail),
)
def read_heartbeat(conn: sqlite3.Connection) -> sqlite3.Row | None:
return conn.execute("SELECT * FROM worker_heartbeat WHERE id=1").fetchone()
def queue_depth(conn: sqlite3.Connection) -> int:
row = conn.execute("SELECT COUNT(*) AS n FROM submissions WHERE status='queued'").fetchone()
return int(row["n"]) if row else 0