feat(web): self-service cheie/creds + admin web + email signup (PRD 3.3b)
US-007: rute web proprii /cont/roteste-cheie + /cont/rar-creds scoped pe sesiune (C13), sectiune "Contul meu" cu cheie afisata o data. US-010: rol admin (users.is_admin) + require_admin->403 + CLI set-admin + bootstrap primul cont=admin (count_admins in BEGIN IMMEDIATE, anti-race). US-011: panou /admin (activare/dezactivare conturi, CSRF + PRG), link admin + logout pe dashboard. US-012: app/email.py notify_signup best-effort degradat fara SMTP + config smtp_*. Fix: migrare defensiva users.is_admin/email_verified in _migrate. VERIFY x2 context curat (PASS) + /code-review high. 393 teste pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
55
app/users.py
55
app/users.py
@@ -46,12 +46,21 @@ def _scrypt_hash(password: str, salt: bytes, n: int = _N, r: int = _R, p: int =
|
||||
)
|
||||
|
||||
|
||||
def create_user(conn: sqlite3.Connection, account_id: int, email: str, password: str) -> int:
|
||||
def create_user(
|
||||
conn: sqlite3.Connection,
|
||||
account_id: int,
|
||||
email: str,
|
||||
password: str,
|
||||
is_admin: bool = False,
|
||||
) -> int:
|
||||
"""Creeaza un user nou si intoarce id-ul.
|
||||
|
||||
Valideaza ca: contul exista, parola intre 10 si 128 caractere, emailul nu e duplicat.
|
||||
Stocheaza DOAR hash scrypt + salt (hex), niciodata parola in clar.
|
||||
Email duplicat (case-insensitive, via UNIQUE COLLATE NOCASE) -> ValueError.
|
||||
|
||||
is_admin: daca True, userul e marcat ca admin (is_admin=1). Apelantul decide
|
||||
logica de bootstrap (count_admins==0 -> primul cont devine admin).
|
||||
"""
|
||||
email = email.strip()
|
||||
|
||||
@@ -69,9 +78,9 @@ def create_user(conn: sqlite3.Connection, account_id: int, email: str, password:
|
||||
|
||||
try:
|
||||
cur = conn.execute(
|
||||
"INSERT INTO users (account_id, email, password_hash, salt, scrypt_params) "
|
||||
"VALUES (?, ?, ?, ?, ?)",
|
||||
(account_id, email, pw_hash.hex(), salt.hex(), SCRYPT_PARAMS),
|
||||
"INSERT INTO users (account_id, email, password_hash, salt, scrypt_params, is_admin) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?)",
|
||||
(account_id, email, pw_hash.hex(), salt.hex(), SCRYPT_PARAMS, 1 if is_admin else 0),
|
||||
)
|
||||
except sqlite3.IntegrityError:
|
||||
raise ValueError("email deja folosit")
|
||||
@@ -79,6 +88,44 @@ def create_user(conn: sqlite3.Connection, account_id: int, email: str, password:
|
||||
return int(cur.lastrowid or 0)
|
||||
|
||||
|
||||
def count_admins(conn: sqlite3.Connection) -> int:
|
||||
"""Numara userii cu is_admin=1 din intreaga baza."""
|
||||
row = conn.execute("SELECT COUNT(*) AS n FROM users WHERE is_admin=1").fetchone()
|
||||
return int(row["n"]) if row else 0
|
||||
|
||||
|
||||
def set_admin(conn: sqlite3.Connection, account_id: int, is_admin: bool = True) -> None:
|
||||
"""Seteaza/sterge rolul admin pe toti userii contului dat.
|
||||
|
||||
Ridica ValueError daca contul nu exista.
|
||||
Daca contul exista dar nu are useri, e no-op silentios (confom spec US-010).
|
||||
"""
|
||||
acct = conn.execute("SELECT 1 FROM accounts WHERE id=?", (account_id,)).fetchone()
|
||||
if not acct:
|
||||
raise ValueError(f"cont inexistent: {account_id}")
|
||||
conn.execute(
|
||||
"UPDATE users SET is_admin=? WHERE account_id=?",
|
||||
(1 if is_admin else 0, account_id),
|
||||
)
|
||||
|
||||
|
||||
def is_account_admin(conn: sqlite3.Connection, account_id: int) -> bool:
|
||||
"""Returneaza True daca cel putin un user al contului are is_admin=1."""
|
||||
row = conn.execute(
|
||||
"SELECT 1 FROM users WHERE account_id=? AND is_admin=1 LIMIT 1",
|
||||
(account_id,),
|
||||
).fetchone()
|
||||
return row is not None
|
||||
|
||||
|
||||
def list_admin_emails(conn: sqlite3.Connection) -> list[str]:
|
||||
"""Returneaza emailurile tuturor userilor cu is_admin=1 (folosit de US-012)."""
|
||||
rows = conn.execute(
|
||||
"SELECT email FROM users WHERE is_admin=1"
|
||||
).fetchall()
|
||||
return [row["email"] for row in rows]
|
||||
|
||||
|
||||
def verify_password(conn: sqlite3.Connection, email: str, password: str) -> int | None:
|
||||
"""Verifica parola pentru email. Intoarce account_id la potrivire, None altfel.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user