feat(setari): meniu grupat + pagina Setari cu sub-taburi + schimbare parola
Meniu hamburger reorganizat pe grupuri cu etichete (LUCRU / CONT) cu iconite SVG discrete; "Cont" redenumit "Setari" (URL ?tab=cont pastrat, zero rute moarte). Pagina Setari restructurata in sub-taburi interne: Date firma / Credentiale RAR / Cheie API / Cont (plan + schimbare parola), cu indicator de stare per tab. Sub-tabul activ e pastrat server-side (activ_subtab) dupa fiecare swap HTMX; comutarea intre taburi e client-side, delegata pe document (supravietuieste swap-urilor). Credentiale RAR: mediile Testare/Productie ca doua carduri paralele. Schimbare parola de acces din sub-tabul Cont (functie noua, lipsea): - users.change_password (scrypt): verifica parola actuala constant-time, valideaza noua (10..128, diferita), rescrie hash+salt nou; - ruta POST /cont/schimba-parola, scoped pe user_id din sesiune, CSRF enforce, parola niciodata re-pusa in raspuns, jurnal 'parola_schimbata'. Referinte copy "Meniu cont > Cont" -> "Setari". Teste: schimbare parola (succes / parola actuala gresita / confirmare nepotrivita / lungime) + izolare rate-limit login in fixture-ul clientului. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
47
app/users.py
47
app/users.py
@@ -157,6 +157,53 @@ def verify_password(conn: sqlite3.Connection, email: str, password: str) -> int
|
||||
return None
|
||||
|
||||
|
||||
def change_password(
|
||||
conn: sqlite3.Connection,
|
||||
user_id: int,
|
||||
current_password: str,
|
||||
new_password: str,
|
||||
) -> None:
|
||||
"""Schimba parola de acces a userului identificat prin user_id.
|
||||
|
||||
Verifica parola actuala (scrypt, comparatie constant-time), valideaza parola
|
||||
noua (10..128, diferita de cea actuala) si rescrie password_hash + salt nou cu
|
||||
parametrii scrypt curenti. Parola NICIODATA stocata in clar.
|
||||
|
||||
Ridica ValueError la: user inexistent, parametri parola corupti, parola actuala
|
||||
gresita, parola noua invalida. Nu invalideaza sesiunea (apelantul decide).
|
||||
"""
|
||||
row = conn.execute(
|
||||
"SELECT password_hash, salt, scrypt_params FROM users WHERE id=?",
|
||||
(user_id,),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise ValueError("utilizator inexistent")
|
||||
|
||||
salt = bytes.fromhex(row["salt"])
|
||||
expected = bytes.fromhex(row["password_hash"])
|
||||
params = _parse_scrypt_params(row["scrypt_params"] or "")
|
||||
if params is None:
|
||||
raise ValueError("parametri parola corupti")
|
||||
n, r, p = params
|
||||
actual = _scrypt_hash(current_password, salt, n=n, r=r, p=p)
|
||||
if not hmac.compare_digest(actual, expected):
|
||||
raise ValueError("Parola actuala este incorecta.")
|
||||
|
||||
if len(new_password) < _PASSWORD_MIN:
|
||||
raise ValueError(f"Parola noua e prea scurta (minim {_PASSWORD_MIN} caractere).")
|
||||
if len(new_password) > _PASSWORD_MAX:
|
||||
raise ValueError(f"Parola noua e prea lunga (maxim {_PASSWORD_MAX} caractere).")
|
||||
if new_password == current_password:
|
||||
raise ValueError("Parola noua trebuie sa difere de cea actuala.")
|
||||
|
||||
new_salt = secrets.token_bytes(16)
|
||||
new_hash = _scrypt_hash(new_password, new_salt)
|
||||
conn.execute(
|
||||
"UPDATE users SET password_hash=?, salt=?, scrypt_params=? WHERE id=?",
|
||||
(new_hash.hex(), new_salt.hex(), SCRYPT_PARAMS, user_id),
|
||||
)
|
||||
|
||||
|
||||
def get_user_by_email(conn: sqlite3.Connection, email: str) -> dict | None:
|
||||
"""Metadate user dupa email (FARA password_hash si salt)."""
|
||||
row = conn.execute(
|
||||
|
||||
Reference in New Issue
Block a user