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(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,228 +1,235 @@
|
||||
<div class="card" id="card-cont">
|
||||
<h2 style="font-size:15px; margin:0 0 16px;">Contul meu</h2>
|
||||
|
||||
<!-- Sectiunea: Plan curent (US-006 PRD 5.17) -->
|
||||
{% if plan_linie is defined %}
|
||||
<div id="sectiune-plan" style="margin-bottom:20px; padding-bottom:20px; border-bottom:1px solid var(--line);">
|
||||
<h3 style="font-size:var(--fs-sm); color:var(--muted); font-weight:500; margin:0 0 10px;
|
||||
text-transform:uppercase; letter-spacing:.04em;">Plan curent</h3>
|
||||
|
||||
<div style="font-size:var(--fs-md); font-weight:600; margin-bottom:6px;
|
||||
color:{% if plan_limita_atinsa|default(false) %}var(--err){% elif plan_warn|default(false) %}var(--warn){% else %}var(--ink){% endif %};">
|
||||
{{ plan_linie }}
|
||||
</div>
|
||||
|
||||
{% if monthly_limit_val is defined and monthly_limit_val is not none and effective_tier_name|default('') == 'free' %}
|
||||
<div style="font-size:var(--fs-sm); color:var(--muted); margin-bottom:8px;">
|
||||
Planul Gratuit include {{ monthly_limit_val }} prestatii/luna prin dashboard-ul web.
|
||||
{% if plan_limita_atinsa|default(false) %}
|
||||
Limita lunara a fost atinsa — trimiterile noi sunt blocate pana la inceputul lunii urmatoare.
|
||||
{% elif plan_warn|default(false) %}
|
||||
Te apropii de limita lunara.
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div style="font-size:var(--fs-sm); color:var(--muted); padding:8px 10px;
|
||||
border:1px solid var(--line); border-radius:6px; margin-top:4px;">
|
||||
Vrei sa treci pe Standard, Pro sau Premium?
|
||||
Contacteaza-ne pentru alocare manuala — nu exista inca plata self-service.
|
||||
<strong>Pro</strong> adauga import prin API; <strong>Standard</strong> si
|
||||
<strong>Premium</strong> ridica limita de volum.
|
||||
</div>
|
||||
{# Pagina Setari: sub-taburi interne — Date firma / Credentiale RAR / Cheie API /
|
||||
Cont (plan + parola). `activ_subtab` (server) alege panoul activ dupa fiecare
|
||||
swap HTMX, ca re-randarea sa nu resete la primul tab. Comutarea intre taburi
|
||||
fara server = JS delegat in base.html (#card-cont). #}
|
||||
{% set _firma_ok = account_meta.name and account_meta.email and account_meta.cui %}
|
||||
{% set _rar_ok = medii_disponibile and medii_disponibile|length > 0 %}
|
||||
<div class="card" id="card-cont" data-activ-subtab="{{ activ_subtab|default('firma') }}">
|
||||
<div class="setari-head">
|
||||
<h2 class="setari-title">
|
||||
<svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.6 4.6l2.1 2.1M17.3 17.3l2.1 2.1M4.6 19.4l2.1-2.1M17.3 6.7l2.1-2.1"/></svg>
|
||||
Setari
|
||||
</h2>
|
||||
<p class="setari-sub">Datele firmei, credentialele RAR, cheia API si contul tau.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Sectiunea: Date firma (US-002) -->
|
||||
<div style="margin-bottom:20px; padding-bottom:20px; border-bottom:1px solid var(--line);">
|
||||
<h3 style="font-size:13px; color:var(--muted); font-weight:500; margin:0 0 8px; text-transform:uppercase; letter-spacing:.04em;">Date firma</h3>
|
||||
<div class="setari-subtabs" role="tablist" aria-label="Sectiuni setari">
|
||||
<button type="button" class="setari-subtab{{ ' is-active' if activ_subtab|default('firma') == 'firma' else '' }}" role="tab" data-subtab="firma"
|
||||
aria-selected="{{ 'true' if activ_subtab|default('firma') == 'firma' else 'false' }}">
|
||||
<span class="st-dot {{ 'ok' if _firma_ok else 'warn' }}" aria-hidden="true"></span>Date firma</button>
|
||||
<button type="button" class="setari-subtab{{ ' is-active' if activ_subtab|default('firma') == 'rar' else '' }}" role="tab" data-subtab="rar"
|
||||
aria-selected="{{ 'true' if activ_subtab|default('firma') == 'rar' else 'false' }}">
|
||||
<span class="st-dot {{ 'ok' if _rar_ok else 'warn' }}" aria-hidden="true"></span>Credentiale RAR</button>
|
||||
<button type="button" class="setari-subtab{{ ' is-active' if activ_subtab|default('firma') == 'api' else '' }}" role="tab" data-subtab="api"
|
||||
aria-selected="{{ 'true' if activ_subtab|default('firma') == 'api' else 'false' }}">
|
||||
<span class="st-dot {{ 'ok' if are_cheie else 'warn' }}" aria-hidden="true"></span>Cheie API</button>
|
||||
<button type="button" class="setari-subtab{{ ' is-active' if activ_subtab|default('firma') == 'cont' else '' }}" role="tab" data-subtab="cont"
|
||||
aria-selected="{{ 'true' if activ_subtab|default('firma') == 'cont' else 'false' }}">
|
||||
<span class="st-dot info" aria-hidden="true"></span>Cont</button>
|
||||
</div>
|
||||
|
||||
{% if date_firma_mesaj %}
|
||||
<div class="flash" style="margin-bottom:12px;">{{ date_firma_mesaj }}</div>
|
||||
{% endif %}
|
||||
{# ============================ Sub-tab: Date firma ============================ #}
|
||||
<div class="setari-pane" data-subpane="firma" role="tabpanel"
|
||||
{% if activ_subtab|default('firma') != 'firma' %}hidden{% endif %}>
|
||||
<div class="setari-zone-title">
|
||||
<svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 21V4a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v17"/><path d="M15 9h4a1 1 0 0 1 1 1v11"/><path d="M2 21h20"/><path d="M8 7h3M8 11h3M8 15h3"/></svg>
|
||||
Date firma
|
||||
<span class="setari-badge {{ 'sb-ok' if _firma_ok else 'sb-warn' }}">{{ 'complet' if _firma_ok else 'incomplet' }}</span>
|
||||
</div>
|
||||
<p class="setari-zone-desc">Apar pe declaratiile catre RAR si pe facturile de plan.</p>
|
||||
|
||||
{% if date_firma_eroare %}
|
||||
<div class="banner" style="margin-bottom:12px; padding:8px 12px;">{{ date_firma_eroare }}</div>
|
||||
{% endif %}
|
||||
{% if date_firma_mesaj %}<div class="flash" style="margin-bottom:12px;">{{ date_firma_mesaj }}</div>{% endif %}
|
||||
{% if date_firma_eroare %}<div class="banner" style="margin-bottom:12px; padding:8px 12px;">{{ date_firma_eroare }}</div>{% endif %}
|
||||
|
||||
<form hx-post="/cont/date-firma"
|
||||
hx-target="#card-cont"
|
||||
hx-swap="outerHTML">
|
||||
<form hx-post="/cont/date-firma" hx-target="#card-cont" hx-swap="outerHTML">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<p style="margin:0 0 8px;">
|
||||
<label style="font-size:13px; color:var(--muted);">Companie</label><br>
|
||||
<input type="text" name="companie" required
|
||||
value="{{ account_meta.name or '' }}"
|
||||
style="width:100%; max-width:340px;"
|
||||
placeholder="Numele firmei (ex. Service Auto SRL)">
|
||||
<input type="text" name="companie" required value="{{ account_meta.name or '' }}"
|
||||
style="width:100%; max-width:340px;" placeholder="Numele firmei (ex. Service Auto SRL)">
|
||||
</p>
|
||||
<p style="margin:0 0 8px;">
|
||||
<label style="font-size:13px; color:var(--muted);">Email contact</label><br>
|
||||
<input type="email" name="email" required
|
||||
value="{{ account_meta.email or '' }}"
|
||||
style="width:100%; max-width:340px;"
|
||||
placeholder="contact@firma.ro">
|
||||
<input type="email" name="email" required value="{{ account_meta.email or '' }}"
|
||||
style="width:100%; max-width:340px;" placeholder="contact@firma.ro">
|
||||
</p>
|
||||
<p style="margin:0 0 12px;">
|
||||
<label style="font-size:13px; color:var(--muted);">CUI (cod unic de identificare)</label><br>
|
||||
<input type="text" name="cui" required
|
||||
value="{{ account_meta.cui or '' }}"
|
||||
style="width:100%; max-width:340px;"
|
||||
placeholder="RO12345678">
|
||||
<input type="text" name="cui" required value="{{ account_meta.cui or '' }}"
|
||||
style="width:100%; max-width:340px;" placeholder="RO12345678">
|
||||
</p>
|
||||
<button type="submit">Salveaza datele firmei</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Sectiunea: Cheia mea API -->
|
||||
<div style="margin-bottom:20px; padding-bottom:20px; border-bottom:1px solid var(--line);">
|
||||
<h3 style="font-size:13px; color:var(--muted); font-weight:500; margin:0 0 8px; text-transform:uppercase; letter-spacing:.04em;">Cheia mea API</h3>
|
||||
|
||||
{% if api_key %}
|
||||
<div class="flash" style="margin-bottom:12px;">Cheia a fost rotita. Salveaz-o acum — nu o vei mai putea vedea.</div>
|
||||
|
||||
<div class="card" style="font-family:monospace; word-break:break-all; font-size:14px; background:#0f1115; margin:0 0 8px;">
|
||||
{{ api_key }}
|
||||
</div>
|
||||
|
||||
<button type="button"
|
||||
data-key="{{ api_key }}"
|
||||
onclick="navigator.clipboard.writeText(this.dataset.key).then(()=>this.textContent='Copiat!')">
|
||||
Copiaza cheia
|
||||
</button>
|
||||
|
||||
<p style="font-size:13px; color:var(--warn); margin:10px 0 0;">
|
||||
Atentie: la urmatoarea vizita aceasta cheie dispare. Daca o pierzi, roteste din nou.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if rot_eroare %}
|
||||
<div class="banner" style="margin-bottom:12px; padding:8px 12px;">{{ rot_eroare }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form hx-post="/cont/roteste-cheie"
|
||||
hx-target="#card-cont"
|
||||
hx-swap="outerHTML"
|
||||
style="margin-top:{% if api_key %}12px{% else %}0{% endif %};">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" style="background:var(--card); color:var(--warn); border-color:var(--warn);">
|
||||
Roteste cheia API
|
||||
</button>
|
||||
<span style="font-size:12px; color:var(--muted); margin-left:8px;">Cheia veche se revoca imediat.</span>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Sectiunea: Credentiale RAR -->
|
||||
<div>
|
||||
<h3 style="font-size:13px; color:var(--muted); font-weight:500; margin:0 0 12px; text-transform:uppercase; letter-spacing:.04em;">Credentiale RAR (portal AUTOPASS)</h3>
|
||||
|
||||
<p class="muted" style="font-size:12px; margin:0 0 12px; line-height:1.5;">
|
||||
Configureaza mai jos mediile RAR — Testare si/sau Productie — fiecare cu credentialele
|
||||
lui, validate separat la salvare pe sistemul RAR corespunzator. Un mediu devine activ
|
||||
cand e bifat si are credentiale; abia atunci importul web poate trimite pe el. Fara
|
||||
niciun mediu activ, importul web nu are credentiale si trimiterile esueaza. (Canalul API
|
||||
ramane pe mediul global al instantei, {{ mediu_instanta() }}, pentru ca isi cara singur
|
||||
credentialele.)
|
||||
{# ========================= Sub-tab: Credentiale RAR ========================= #}
|
||||
<div class="setari-pane" data-subpane="rar" role="tabpanel"
|
||||
{% if activ_subtab|default('firma') != 'rar' %}hidden{% endif %}>
|
||||
<div class="setari-zone-title">
|
||||
<svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 11l1.5-4A2 2 0 0 1 8.4 5.6h7.2A2 2 0 0 1 17.5 7L19 11"/><path d="M4 11h16v6H4z"/><path d="M6 17v2M18 17v2"/><circle cx="7.5" cy="14" r="1"/><circle cx="16.5" cy="14" r="1"/></svg>
|
||||
Credentiale RAR (portal AUTOPASS)
|
||||
<span class="setari-badge {{ 'sb-ok' if _rar_ok else 'sb-warn' }}">{{ 'configurat' if _rar_ok else 'neconfigurat' }}</span>
|
||||
</div>
|
||||
<p class="setari-zone-desc">
|
||||
Configureaza mediile RAR — Testare si/sau Productie — fiecare cu credentialele lui,
|
||||
validate separat la salvare pe sistemul RAR corespunzator. Un mediu devine activ cand e
|
||||
bifat si are credentiale; abia atunci importul web poate trimite pe el. (Canalul API ramane
|
||||
pe mediul global al instantei, {{ mediu_instanta() }}, pentru ca isi cara singur credentialele.)
|
||||
</p>
|
||||
|
||||
{% if creds_mesaj %}
|
||||
<div class="flash" style="margin-bottom:12px;">{{ creds_mesaj }}</div>
|
||||
{% endif %}
|
||||
{% if creds_eroare %}
|
||||
<div class="banner" style="margin-bottom:12px; padding:8px 12px;">{{ creds_eroare }}</div>
|
||||
{% endif %}
|
||||
{% if creds_mesaj %}<div class="flash" style="margin-bottom:12px;">{{ creds_mesaj }}</div>{% endif %}
|
||||
{% if creds_eroare %}<div class="banner" style="margin-bottom:12px; padding:8px 12px;">{{ creds_eroare }}</div>{% endif %}
|
||||
|
||||
<form hx-post="/cont/rar-medii"
|
||||
hx-target="#card-cont"
|
||||
hx-swap="outerHTML">
|
||||
<form hx-post="/cont/rar-medii" hx-target="#card-cont" hx-swap="outerHTML">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
|
||||
<!-- Subsectiunea: Testare -->
|
||||
<div style="margin-bottom:16px; padding-bottom:16px; border-bottom:1px solid var(--line);">
|
||||
<p style="margin:0 0 6px;">
|
||||
<label style="font-size:13px; color:var(--muted); display:flex; align-items:center; gap:6px;">
|
||||
<div class="env-cards">
|
||||
<!-- Testare -->
|
||||
<div class="env-card{% if test_disponibil %} active-test{% endif %}">
|
||||
<h4>Testare
|
||||
{% if test_disponibil %}<span class="setari-badge sb-ok">configurat</span>{% else %}<span class="setari-badge sb-info">inactiv</span>{% endif %}
|
||||
</h4>
|
||||
<label class="env-sw">
|
||||
<input type="checkbox" name="test_enabled" value="1" {% if test_enabled %}checked{% endif %}>
|
||||
Activare Testare
|
||||
</label>
|
||||
{% if test_disponibil %}
|
||||
<span style="font-size:12px; color:var(--ok);">configurat</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p style="margin:0 0 8px;">
|
||||
<label style="font-size:13px; color:var(--muted);">Email RAR Testare</label><br>
|
||||
<input type="email" name="test_email" style="width:100%; max-width:340px;"
|
||||
placeholder="email@service.ro">
|
||||
</p>
|
||||
<p style="margin:0 0 8px;">
|
||||
<label style="font-size:13px; color:var(--muted);">Parola RAR Testare</label><br>
|
||||
<input type="password" name="test_parola" style="width:100%; max-width:340px;"
|
||||
autocomplete="new-password">
|
||||
</p>
|
||||
{% if creds_test_mesaj %}
|
||||
<div class="flash" style="margin-top:6px;">{{ creds_test_mesaj }}</div>
|
||||
{% endif %}
|
||||
{% if creds_test_eroare %}
|
||||
<div class="banner" style="margin-top:6px; padding:8px 12px;">{{ creds_test_eroare }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<label class="env-lbl">Email RAR Testare</label>
|
||||
<input type="email" name="test_email" placeholder="email@service.ro">
|
||||
<label class="env-lbl">Parola RAR Testare</label>
|
||||
<input type="password" name="test_parola" autocomplete="new-password">
|
||||
{% if creds_test_mesaj %}<div class="flash" style="margin-top:6px;">{{ creds_test_mesaj }}</div>{% endif %}
|
||||
{% if creds_test_eroare %}<div class="banner" style="margin-top:6px; padding:8px 12px;">{{ creds_test_eroare }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Subsectiunea: Productie -->
|
||||
<div style="margin-bottom:16px; padding-bottom:16px; border-bottom:1px solid var(--line);">
|
||||
<p style="margin:0 0 6px;">
|
||||
<label style="font-size:13px; color:var(--muted); display:flex; align-items:center; gap:6px;">
|
||||
<!-- Productie -->
|
||||
<div class="env-card{% if not prod_disponibil %} inactive{% endif %}">
|
||||
<h4>Productie
|
||||
{% if prod_disponibil %}<span class="setari-badge sb-ok">configurat</span>{% else %}<span class="setari-badge sb-warn">inactiv</span>{% endif %}
|
||||
</h4>
|
||||
<label class="env-sw">
|
||||
<input type="checkbox" name="prod_enabled" value="1" {% if prod_enabled %}checked{% endif %}>
|
||||
Activare Productie
|
||||
</label>
|
||||
{% if prod_disponibil %}
|
||||
<span style="font-size:12px; color:var(--ok);">configurat</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p style="margin:0 0 8px;">
|
||||
<label style="font-size:13px; color:var(--muted);">Email RAR Productie</label><br>
|
||||
<input type="email" name="prod_email" style="width:100%; max-width:340px;"
|
||||
placeholder="email@service.ro">
|
||||
</p>
|
||||
<p style="margin:0 0 8px;">
|
||||
<label style="font-size:13px; color:var(--muted);">Parola RAR Productie</label><br>
|
||||
<input type="password" name="prod_parola" style="width:100%; max-width:340px;"
|
||||
autocomplete="new-password">
|
||||
</p>
|
||||
<p style="margin:0 0 8px;">
|
||||
<label style="font-size:13px; color:var(--muted); display:flex; align-items:flex-start; gap:6px;">
|
||||
<input type="checkbox" name="prod_confirmare" value="1" style="margin-top:2px; flex-shrink:0;">
|
||||
Inteleg ca trimiterile pe Productie sunt declaratii reale (L.142), finale si fara anulare.
|
||||
<label class="env-lbl">Email RAR Productie</label>
|
||||
<input type="email" name="prod_email" placeholder="email@service.ro">
|
||||
<label class="env-lbl">Parola RAR Productie</label>
|
||||
<input type="password" name="prod_parola" autocomplete="new-password">
|
||||
<label class="env-confirm">
|
||||
<input type="checkbox" name="prod_confirmare" value="1">
|
||||
<span>Inteleg ca trimiterile pe Productie sunt declaratii reale (L.142), finale si fara anulare.</span>
|
||||
</label>
|
||||
</p>
|
||||
{% if creds_prod_mesaj %}
|
||||
<div class="flash" style="margin-top:6px;">{{ creds_prod_mesaj }}</div>
|
||||
{% endif %}
|
||||
{% if creds_prod_eroare %}
|
||||
<div class="banner" style="margin-top:6px; padding:8px 12px;">{{ creds_prod_eroare }}</div>
|
||||
{% endif %}
|
||||
{% if creds_prod_mesaj %}<div class="flash" style="margin-top:6px;">{{ creds_prod_mesaj }}</div>{% endif %}
|
||||
{% if creds_prod_eroare %}<div class="banner" style="margin-top:6px; padding:8px 12px;">{{ creds_prod_eroare }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selector mediu implicit -->
|
||||
<div style="margin-bottom:16px;">
|
||||
<label style="font-size:13px; color:var(--muted);">Mediu implicit pentru trimiteri</label><br>
|
||||
<div class="default-env">
|
||||
<label style="font-size:13px; color:var(--muted);">Mediu implicit pentru trimiteri</label>
|
||||
{% if medii_disponibile %}
|
||||
<select name="rar_env_default" style="width:100%; max-width:340px; margin-top:4px;">
|
||||
<select name="rar_env_default">
|
||||
{% for env in medii_disponibile %}
|
||||
<option value="{{ env }}"{% if env == rar_env_default %} selected{% endif %}>{{ "Testare" if env == "test" else "Productie" }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% else %}
|
||||
<p style="font-size:13px; color:var(--muted); margin:4px 0 0;">Activeaza si valideaza un mediu intai.</p>
|
||||
{% endif %}
|
||||
{% if creds_default_mesaj %}
|
||||
<div class="flash" style="margin-top:6px;">{{ creds_default_mesaj }}</div>
|
||||
{% endif %}
|
||||
{% if creds_default_eroare %}
|
||||
<div class="banner" style="margin-top:6px; padding:8px 12px;">{{ creds_default_eroare }}</div>
|
||||
<span style="font-size:13px; color:var(--muted);">Activeaza si valideaza un mediu intai.</span>
|
||||
{% endif %}
|
||||
{% if creds_default_mesaj %}<div class="flash" style="margin-top:6px; flex-basis:100%;">{{ creds_default_mesaj }}</div>{% endif %}
|
||||
{% if creds_default_eroare %}<div class="banner" style="margin-top:6px; padding:8px 12px; flex-basis:100%;">{{ creds_default_eroare }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<button type="submit">Salveaza mediile RAR</button>
|
||||
<span style="font-size:12px; color:var(--muted); margin-left:8px;">Parolele stocate criptat, niciodata in clar.</span>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{# ============================ Sub-tab: Cheie API ============================ #}
|
||||
<div class="setari-pane" data-subpane="api" role="tabpanel"
|
||||
{% if activ_subtab|default('firma') != 'api' %}hidden{% endif %}>
|
||||
<div class="setari-zone-title">
|
||||
<svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="8" cy="15" r="4"/><path d="M11 12l9-9"/><path d="M17 6l2.5 2.5M14 9l2.5 2.5"/></svg>
|
||||
Cheie API
|
||||
<span class="setari-badge {{ 'sb-ok' if are_cheie else 'sb-warn' }}">{{ 'activa' if are_cheie else 'lipsa' }}</span>
|
||||
</div>
|
||||
<p class="setari-zone-desc">
|
||||
O foloseste softul propriu / ROAAUTO ca sa trimita prezentari prin API. Se vede integral o
|
||||
singura data, la creare sau rotire.
|
||||
</p>
|
||||
|
||||
{% if api_key %}
|
||||
<div class="flash" style="margin-bottom:12px;">Cheia a fost rotita. Salveaz-o acum — nu o vei mai putea vedea.</div>
|
||||
<div class="keybox">{{ api_key }}</div>
|
||||
<button type="button" class="btn-secondary" style="margin-top:8px;"
|
||||
data-key="{{ api_key }}"
|
||||
onclick="navigator.clipboard.writeText(this.dataset.key).then(()=>this.textContent='Copiat!')">
|
||||
Copiaza cheia
|
||||
</button>
|
||||
<p style="font-size:13px; color:var(--warn); margin:10px 0 0;">
|
||||
Atentie: la urmatoarea vizita aceasta cheie dispare. Daca o pierzi, roteste din nou.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if rot_eroare %}<div class="banner" style="margin:12px 0; padding:8px 12px;">{{ rot_eroare }}</div>{% endif %}
|
||||
|
||||
<form hx-post="/cont/roteste-cheie" hx-target="#card-cont" hx-swap="outerHTML"
|
||||
style="margin-top:{% if api_key %}14px{% else %}0{% endif %};">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" class="btn-warn">{{ 'Roteste cheia API' if are_cheie else 'Genereaza cheia API' }}</button>
|
||||
<span style="font-size:12px; color:var(--muted); margin-left:8px;">Cheia veche se revoca imediat.</span>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{# ========================= Sub-tab: Cont (plan + parola) ==================== #}
|
||||
<div class="setari-pane" data-subpane="cont" role="tabpanel"
|
||||
{% if activ_subtab|default('firma') != 'cont' %}hidden{% endif %}>
|
||||
|
||||
{# --- Plan curent --- #}
|
||||
{% if plan_linie is defined %}
|
||||
<div class="setari-zone-title">
|
||||
<svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3 3 8l9 5 9-5z"/><path d="M3 13l9 5 9-5"/></svg>
|
||||
Plan curent
|
||||
<span class="setari-badge sb-info">{{ tier_label|default('Gratuit') }}</span>
|
||||
</div>
|
||||
<div style="font-size:var(--fs-md); font-weight:600; margin-bottom:6px;
|
||||
color:{% if plan_limita_atinsa|default(false) %}var(--err){% elif plan_warn|default(false) %}var(--warn){% else %}var(--ink){% endif %};">
|
||||
{{ plan_linie }}
|
||||
</div>
|
||||
{% if plan_limita_atinsa|default(false) %}
|
||||
<p class="setari-zone-desc" style="color:var(--err);">Limita lunara a fost atinsa — trimiterile noi sunt blocate pana la inceputul lunii urmatoare.</p>
|
||||
{% elif plan_warn|default(false) %}
|
||||
<p class="setari-zone-desc" style="color:var(--warn);">Te apropii de limita lunara.</p>
|
||||
{% endif %}
|
||||
<p class="setari-zone-desc">
|
||||
Pentru mai mult volum sau import prin API, <a href="mailto:contact@romfast.ro">contacteaza-ne</a> — alocarea planurilor se face manual.
|
||||
</p>
|
||||
<hr class="setari-subzone-sep">
|
||||
{% endif %}
|
||||
|
||||
{# --- Parola de acces --- #}
|
||||
<div class="setari-zone-title">
|
||||
<svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="10" width="16" height="11" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/><circle cx="12" cy="15.5" r="1.2"/></svg>
|
||||
Parola de acces
|
||||
</div>
|
||||
<p class="setari-zone-desc">Parola cu care te autentifici in dashboard (separata de credentialele RAR). Minim 10 caractere.</p>
|
||||
|
||||
{% if parola_mesaj %}<div class="flash" style="margin-bottom:12px;">{{ parola_mesaj }}</div>{% endif %}
|
||||
{% if parola_eroare %}<div class="banner" style="margin-bottom:12px; padding:8px 12px;">{{ parola_eroare }}</div>{% endif %}
|
||||
|
||||
<form hx-post="/cont/schimba-parola" hx-target="#card-cont" hx-swap="outerHTML">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<p style="margin:0 0 8px;">
|
||||
<label style="font-size:13px; color:var(--muted);">Parola actuala</label><br>
|
||||
<input type="password" name="parola_actuala" autocomplete="current-password" style="width:100%; max-width:340px;">
|
||||
</p>
|
||||
<p style="margin:0 0 8px;">
|
||||
<label style="font-size:13px; color:var(--muted);">Parola noua</label><br>
|
||||
<input type="password" name="parola_noua" autocomplete="new-password" style="width:100%; max-width:340px;">
|
||||
</p>
|
||||
<p style="margin:0 0 12px;">
|
||||
<label style="font-size:13px; color:var(--muted);">Confirma parola noua</label><br>
|
||||
<input type="password" name="parola_confirma" autocomplete="new-password" style="width:100%; max-width:340px;">
|
||||
</p>
|
||||
<button type="submit">Schimba parola</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
{% else %}
|
||||
Lipseste o cheie API activa.
|
||||
{% endif %}
|
||||
Mergi la <a href="/?tab=cont">Meniu cont › Cont</a> pentru a le configura.
|
||||
Mergi la <a href="/?tab=cont">Meniu cont › Setari</a> pentru a le configura.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
</div>
|
||||
<p class="muted" style="font-size:12px; margin:10px 0 0;">
|
||||
Cererile trimit doar cheia API + datele prezentarii. Credentialele RAR se configureaza
|
||||
o data in <a href="/?tab=cont">Cont</a> si sunt folosite automat la trimitere. Optional,
|
||||
o data in <a href="/?tab=cont">Setari</a> si sunt folosite automat la trimitere. Optional,
|
||||
poti include <code>rar_credentials</code> in payload ca sa le suprascrii pe acea cerere.
|
||||
</p>
|
||||
</div>
|
||||
@@ -216,7 +216,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Card: De ce nu ajunge la RAR? (PRD 5.19 US-010 — onestitate canal API) #}
|
||||
{# Card: De ce nu ajunge la RAR? (onestitate canal API) #}
|
||||
{# Un POST /v1/prezentari poate intoarce 200 dar randul sa NU plece la RAR. #}
|
||||
<div class="card" style="margin-bottom:16px;">
|
||||
<h3 style="margin:0 0 6px; font-size:15px;">De ce nu ajunge la RAR?</h3>
|
||||
@@ -292,7 +292,7 @@
|
||||
<div id="integrare-test-rezultat" style="margin-top:8px;"></div>
|
||||
</div>
|
||||
|
||||
{# Formular test credentiale RAR (US-007, PRD 5.20) #}
|
||||
{# Formular test credentiale RAR #}
|
||||
{# Login de proba pe mediul ales — fara efecte secundare, nu salveaza nimic. #}
|
||||
{# Banner-ul de rezultat include eticheta mediului ("pe TESTARE" / "pe PRODUCTIE"). #}
|
||||
<div class="card" style="margin-bottom:16px;">
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
hx-indicator="#upload-spinner">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token or '' }}">
|
||||
|
||||
{# Indicator mediu RAR (US-009, PRD 5.20): vizibil inainte de drop-zone #}
|
||||
{# Indicator mediu RAR: vizibil inainte de drop-zone #}
|
||||
{% set medii_rar = medii | default([]) %}
|
||||
{% if medii_rar | length == 0 %}
|
||||
{# Banner avertisment — non-blocant (upload continua; commit foloseste ancora globala) #}
|
||||
@@ -41,7 +41,7 @@
|
||||
border:1px solid var(--warn, #e6b34a); font-size:13px;" role="note">
|
||||
<strong>Niciun mediu RAR activ.</strong>
|
||||
Activeaza mediul de Productie sau Testare in
|
||||
<a href="?tab=cont" style="color:var(--accent);">Meniu cont › Cont</a>.
|
||||
<a href="?tab=cont" style="color:var(--accent);">Meniu cont › Setari</a>.
|
||||
</div>
|
||||
{% elif medii_rar | length == 1 %}
|
||||
{# Eticheta statica (un singur mediu disponibil) #}
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
})();
|
||||
</script>
|
||||
<style>
|
||||
/* US-001 PRD 5.16: stive de font standard web. Toate regulile font-face IBM Plex sterse.
|
||||
Motiv: decizie user (risc AI-Slop #11 acceptat constient), uniformitate cross-page.
|
||||
/* Stive de font standard web. Toate regulile font-face IBM Plex sterse.
|
||||
Motiv: decizie user, uniformitate cross-page.
|
||||
Fisierele woff2 raman pe disc (curatare = follow-up optional, non-blocant).
|
||||
Referinte catre directorul de fonturi statice eliminate — font-ui si font-mono sunt stive sistem. */
|
||||
/* Paleta dark (default) — accent azur ROMFAST.
|
||||
@@ -45,10 +45,10 @@
|
||||
--line2: separator subtire (intre --bg si --line). */
|
||||
:root { --bg:#0f1218; --card:#181c24; --card2:#0f1218; --ink:#e6e9ef; --muted:#8b93a7; --line:#262b36; --line2:#1f2530;
|
||||
--ok:#2FBF8F; --warn:#E0A93B; --err:#E05D5D; --accent:#2E74D6;
|
||||
/* US-001 (PRD 5.16): stive font standard web — sursa unica de adevar */
|
||||
/* Stive font standard web — sursa unica de adevar */
|
||||
--font-ui: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", "Segoe UI Mono", "Roboto Mono", Menlo, Consolas, monospace;
|
||||
/* US-002 (PRD 5.16): scala tipografica uniforma — sursa unica de adevar */
|
||||
/* Scala tipografica uniforma — sursa unica de adevar */
|
||||
--fs-xs:12px; --fs-sm:13.5px; --fs-base:15px; --fs-md:16px;
|
||||
--fs-lg:18px; --fs-xl:20px; --fs-2xl:28px; --fs-3xl:34px;
|
||||
--lh-tight:1.25; --lh-body:1.55; }
|
||||
@@ -60,7 +60,7 @@
|
||||
[data-theme="petrol"] { --bg:#0e1416; --card:#161e20; --card2:#0e1416; --ink:#e6e9ef; --muted:#8b93a7; --line:#232c2e; --line2:#1c2426;
|
||||
--ok:#2FBF8F; --warn:#E0A93B; --err:#E05D5D; --accent:#0E7C7B; }
|
||||
/* Paleta Grafit — similara cu dark, accent azur deschis (#6ea2ec = landing --infot).
|
||||
Distinta de dark la cererea userului (D2 PRD 5.15). */
|
||||
Distinta de dark la cererea userului. */
|
||||
[data-theme="grafit"] { --bg:#0f1218; --card:#181c24; --card2:#0f1218; --ink:#e6e9ef; --muted:#8b93a7; --line:#262b36; --line2:#1f2530;
|
||||
--ok:#2FBF8F; --warn:#E0A93B; --err:#E05D5D; --accent:#6ea2ec; }
|
||||
/* Paleta Cobalt — fundal bleumarin adanc, accent albastru viu (#8aa0ff = landing --infot). */
|
||||
@@ -70,7 +70,7 @@
|
||||
[data-theme="cupru"] { --bg:#15110b; --card:#211a12; --card2:#15110b; --ink:#efe6d6; --muted:#a89a85; --line:#36291c; --line2:#281e14;
|
||||
--ok:#67b98c; --warn:#c97d2e; --err:#e2685a; --accent:#dfa45c; }
|
||||
/* Paleta Hartie — fundal crem cald, accent albastru clasic (#1F5FBF = landing --infot = --accent).
|
||||
Similara cu light, distinta la cererea userului (D2 PRD 5.15). */
|
||||
Similara cu light, distinta la cererea userului. */
|
||||
[data-theme="hartie"] { --bg:#f3efe6; --card:#fffdf7; --card2:#f3efe6; --ink:#1e1a13; --muted:#6a6052; --line:#e2dccc; --line2:#ece6d9;
|
||||
--ok:#1c7d5d; --warn:#b45309; --err:#bd463c; --accent:#1F5FBF; }
|
||||
* { box-sizing:border-box; }
|
||||
@@ -104,7 +104,7 @@
|
||||
th { color:var(--muted); font-weight:500; font-size:12px; text-transform:uppercase; letter-spacing:.04em; }
|
||||
.empty { color:var(--muted); padding:24px; text-align:center; }
|
||||
.pill { font-size:12px; padding:2px 8px; border-radius:99px; border:1px solid var(--line); }
|
||||
/* Badge sursa sugestie (5.18 US-007): de unde vine sugestia de cod in editorul de mapare.
|
||||
/* Badge sursa sugestie: de unde vine sugestia de cod in editorul de mapare.
|
||||
confirmat = GOLD validat de om (verde); similar = SILVER/embedding k-NN (azur);
|
||||
non-operatie = pre-filtru NUL / vecin NUL (gri-cald). Suggestion-only, doar indiciu vizual. */
|
||||
.sugg-sursa { display:inline-block; font-size:10px; font-weight:700; line-height:1; padding:2px 6px;
|
||||
@@ -142,7 +142,7 @@
|
||||
.s-ok{color:var(--ok);}
|
||||
.s-needs_review{color:var(--warn);}
|
||||
.s-already_sent,.s-duplicate_in_file{color:var(--muted);}
|
||||
/* Badge mediu RAR (US-010 PRD 5.20) — semantica risc L.142.
|
||||
/* Badge mediu RAR — semantica risc L.142.
|
||||
Productie: fill atentie (ton --err), text alb — declaratie reala, ireversibila.
|
||||
Testare: outline discret, ton --muted — mediu de proba, low-stakes.
|
||||
NU se foloseste verde pentru Productie (ar semnala "sigur"). */
|
||||
@@ -167,7 +167,7 @@
|
||||
flex-wrap:wrap; z-index:10; }
|
||||
/* Indicator HTMX — ascuns pana la request */
|
||||
.htmx-indicator { display:none; }
|
||||
/* US-011 (PRD 5.16): selector tema stil pill — icon + eticheta temei curente.
|
||||
/* Selector tema stil pill — icon + eticheta temei curente.
|
||||
Eticheta se ascunde pe <=560px (spatiu ingust), ramane iconita. */
|
||||
.tema-btn { display:inline-flex; align-items:center; gap:6px; height:36px; padding:0 12px;
|
||||
border-radius:8px; background:transparent; border:1px solid var(--line);
|
||||
@@ -176,7 +176,7 @@
|
||||
.tema-btn:hover { border-color:var(--accent); color:var(--ink); }
|
||||
.tema-btn:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
|
||||
@media (max-width:560px) { #tema-label { display:none; } }
|
||||
/* US-003 (PRD 5.16): dot RAR compact in antet.
|
||||
/* Dot RAR compact in antet.
|
||||
Stare OK: dot verde pulsant + "RAR online". Stare BLOCAT: dot rosu.
|
||||
Stilat ca pill; sensul NU depinde de culoare (aria-label + title). */
|
||||
.rar-chip { display:inline-flex; align-items:center; gap:7px; height:36px; padding:0 12px;
|
||||
@@ -190,7 +190,7 @@
|
||||
.rar-dot.live { animation:rar-pulse 2s ease-in-out infinite; }
|
||||
@keyframes rar-pulse { 0%,100%{opacity:1;} 50%{opacity:.5;} }
|
||||
@media (max-width:560px) { .rar-chip .rar-tx { display:none; } }
|
||||
/* US-010 (PRD 5.16): sub-titlu cu numele service-ului (cand autentificat). */
|
||||
/* Sub-titlu cu numele service-ului (cand autentificat). */
|
||||
.h-sub { font-size:var(--fs-xs); color:var(--muted); margin-top:2px; line-height:1.2; }
|
||||
.h-sub .svc { color:var(--ink); font-weight:600; }
|
||||
/* Badge env (test/prod) si badge tier (plan cont) langa titlu. */
|
||||
@@ -367,7 +367,7 @@
|
||||
border-radius:6px; cursor:pointer; min-height:36px; white-space:nowrap; }
|
||||
.kebab-menu button:hover, .kebab-menu a:hover { background:var(--line); }
|
||||
.kebab-menu button.danger { color:var(--err); }
|
||||
/* === Accordion import compact (US-006 — regiune CSS disjuncta) ===
|
||||
/* === Accordion import compact (regiune CSS disjuncta) ===
|
||||
<details id="import-details"> invelete stepper + upload pe Acasa.
|
||||
Atribut `open` setat de server din `are_trimiteri`:
|
||||
False (first-run) → open; True (returning) → colapsat.
|
||||
@@ -394,7 +394,7 @@
|
||||
border-bottom:none; border-radius:0;
|
||||
margin-bottom:0; }
|
||||
/* === Sfarsit regiune accordion import === */
|
||||
/* === Inceput regiune nav links status-bar (US-005) ===
|
||||
/* === Inceput regiune nav links status-bar ===
|
||||
Linkuri Trimiteri + Mapari sub contoare; marcaj activ cu aria-current/status-nav-activ. */
|
||||
.status-nav-link { color:var(--accent); text-decoration:none; padding:2px 6px; border-radius:4px;
|
||||
transition:background .1s; }
|
||||
@@ -504,7 +504,7 @@
|
||||
/* === Preview import: coloane extra fata de tabelul Trimiteri.
|
||||
SCOPAT prin .tabel-trimiteri (clasa partajata). Regiune separata —
|
||||
nu atinge coloanele existente (col-chk/id/stare/data/rar/actualizat).
|
||||
US-007: 8 coloane (coloana de verificare manuala eliminata).
|
||||
8 coloane (coloana de verificare manuala eliminata).
|
||||
Suma latimi fixe: col-id(48) + col-stare(104) + col-data(104) +
|
||||
col-km(76) + col-note(176) + col-actiuni(92) = 600px.
|
||||
Restul (~680px la 1280px) revine lui col-vehicul + col-operatie (fluid). === */
|
||||
@@ -561,7 +561,7 @@
|
||||
font-size:12px; margin-bottom:2px; }
|
||||
.tabel-trimiteri td.col-chk, .tabel-trimiteri td.col-id { display:none; }
|
||||
|
||||
/* === Card COMPACT (PRD 5.13, corectie dogfood) ===
|
||||
/* === Card COMPACT ===
|
||||
Inlocuieste stiva generica eticheta+valoare (prea inalta) cu un card
|
||||
scanabil la prima vedere: vehicul = titlu, stare = pill dreapta-sus,
|
||||
operatie+cod pe rand, meta (data/km/rar) muted mic, nota mica. Fara
|
||||
@@ -701,10 +701,10 @@
|
||||
.sticky-bar { padding:10px 12px; gap:10px; }
|
||||
.sticky-bar button { width:100%; min-height:44px; }
|
||||
}
|
||||
/* === SENTINEL-COMPONENTE-SLIM: inceput componente slim US-002 (PRD 5.15).
|
||||
/* === SENTINEL-COMPONENTE-SLIM: inceput componente slim.
|
||||
Testele ancoreaza pe acest marker. Nu muta/sterge. === */
|
||||
/* .contor-card — card cifra contor: fundal --card2, bordura --line, radius 8px.
|
||||
US-002 PRD 5.16: padding marit (18px), cifra pe --fs-2xl, label pe --fs-sm, sub pe --fs-xs.
|
||||
Padding marit (18px), cifra pe --fs-2xl, label pe --fs-sm, sub pe --fs-xs.
|
||||
Variante de culoare a cifrei prin clasele .s-* existente (verde/accent/rosu). */
|
||||
.contor-card { background:var(--card2); border:1px solid var(--line); border-radius:8px; padding:18px 18px; }
|
||||
.contor-cifra { font-size:var(--fs-2xl); font-weight:700; line-height:1; }
|
||||
@@ -796,8 +796,8 @@
|
||||
@media (max-width:767px) {
|
||||
.trimitere-slim { padding:12px 14px; }
|
||||
}
|
||||
/* === SENTINEL-COMPONENTE-SLIM: sfarsit componente slim US-002 === */
|
||||
/* === Fix mobil Mapari (bug live 2026-06-29) ===
|
||||
/* === SENTINEL-COMPONENTE-SLIM: sfarsit componente slim === */
|
||||
/* === Fix mobil Mapari ===
|
||||
Doua probleme raportate la 390px pe pagina Mapari:
|
||||
(1) butoanele Salveaza/Sterge taiate: regula `.tabel-card td button {width:100%}`
|
||||
(specificitate 0,1,2) batea `.act {width:44px}` (0,1,0) -> cele doua butoane act
|
||||
@@ -823,10 +823,88 @@
|
||||
.tabel-card td .act .act-tx { display:inline; }
|
||||
.tabel-card td .act .act-ic { display:inline-block; }
|
||||
}
|
||||
/* === Meniu hamburger grupat === */
|
||||
/* Iconite SVG stroke unic, currentColor — mostenesc culoarea intrarii. */
|
||||
.ic { width:18px; height:18px; stroke:currentColor; fill:none; stroke-width:1.7;
|
||||
stroke-linecap:round; stroke-linejoin:round; flex-shrink:0; }
|
||||
.cont-menu a, .cont-menu form button { display:flex; align-items:center; gap:10px; }
|
||||
.cont-menu a .mi, .cont-menu form button .mi { display:inline-flex; opacity:.6; }
|
||||
.cont-menu a:hover .mi, .cont-menu form button:hover .mi { opacity:.9; }
|
||||
.cont-menu-group { font-size:10px; text-transform:uppercase; letter-spacing:.08em;
|
||||
color:var(--muted); font-weight:700; padding:8px 12px 4px; }
|
||||
/* Badge-ul de mapari se ancoreaza la dreapta chiar cand intrarea e flex. */
|
||||
.cont-menu a .tab-badge { margin-left:auto; }
|
||||
/* === Pagina Setari cu sub-taburi === */
|
||||
.setari-head { margin-bottom:4px; }
|
||||
.setari-title { font-size:var(--fs-lg); margin:0 0 2px; letter-spacing:-.01em;
|
||||
display:flex; align-items:center; gap:9px; }
|
||||
.setari-title .ic { width:20px; height:20px; opacity:.7; }
|
||||
.setari-sub { color:var(--muted); font-size:var(--fs-sm); margin:0 0 14px; }
|
||||
.setari-subtabs { display:flex; gap:2px; border-bottom:1px solid var(--line); margin-bottom:20px;
|
||||
overflow-x:auto; -webkit-overflow-scrolling:touch; scrollbar-width:none; }
|
||||
.setari-subtabs::-webkit-scrollbar { display:none; }
|
||||
.setari-subtab { appearance:none; background:transparent; border:none; border-bottom:2px solid transparent;
|
||||
color:var(--muted); font:inherit; font-size:var(--fs-sm); font-weight:500; cursor:pointer;
|
||||
padding:10px 2px; margin-right:22px; white-space:nowrap; display:inline-flex;
|
||||
align-items:center; gap:8px; margin-bottom:-1px; transition:color .12s, border-color .12s; }
|
||||
.setari-subtab:hover { color:var(--ink); }
|
||||
/* Stare activa prin clasa (nu selector pe atribut): aria-selected ramane pe buton
|
||||
pentru accesibilitate, dar stilul nu depinde de el. */
|
||||
.setari-subtab.is-active { color:var(--ink); border-bottom-color:var(--accent); font-weight:600; }
|
||||
.setari-subtab:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
|
||||
.st-dot { width:7px; height:7px; border-radius:99px; flex-shrink:0; }
|
||||
.st-dot.ok { background:var(--ok); }
|
||||
.st-dot.warn { background:var(--warn); }
|
||||
.st-dot.info { background:var(--muted); }
|
||||
.setari-pane[hidden] { display:none; }
|
||||
.setari-zone-title { font-size:var(--fs-md); font-weight:600; margin:0 0 4px; display:flex;
|
||||
align-items:center; gap:10px; flex-wrap:wrap; }
|
||||
.setari-zone-title .ic { opacity:.7; }
|
||||
.setari-zone-desc { color:var(--muted); font-size:var(--fs-sm); margin:0 0 16px; max-width:64ch; line-height:1.5; }
|
||||
.setari-badge { font-size:11px; font-weight:700; padding:2px 9px; border-radius:99px;
|
||||
text-transform:uppercase; letter-spacing:.03em; border:1px solid transparent; }
|
||||
.setari-badge.sb-ok { color:var(--ok); background:color-mix(in srgb,var(--ok) 12%,transparent);
|
||||
border-color:color-mix(in srgb,var(--ok) 40%,transparent); }
|
||||
.setari-badge.sb-warn { color:var(--warn); background:color-mix(in srgb,var(--warn) 12%,transparent);
|
||||
border-color:color-mix(in srgb,var(--warn) 40%,transparent); }
|
||||
.setari-badge.sb-info { color:var(--muted); background:color-mix(in srgb,var(--muted) 12%,transparent);
|
||||
border-color:color-mix(in srgb,var(--muted) 35%,transparent); }
|
||||
.setari-subzone-sep { border:none; border-top:1px solid var(--line); margin:26px 0 22px; }
|
||||
/* Cheie API afisata o singura data. */
|
||||
.keybox { font-family:var(--font-mono); font-size:14px; background:var(--card2);
|
||||
border:1px solid var(--line); border-radius:8px; padding:12px 14px; word-break:break-all;
|
||||
max-width:480px; }
|
||||
.btn-warn { background:transparent; color:var(--warn); border:1px solid var(--warn); border-radius:6px;
|
||||
padding:8px 14px; font:inherit; font-weight:500; cursor:pointer; min-height:36px;
|
||||
display:inline-flex; align-items:center; justify-content:center; gap:6px; }
|
||||
.btn-warn:hover, .btn-warn:focus-visible { background:var(--warn); color:#fff; filter:none; }
|
||||
/* Credentiale RAR: doua carduri de mediu paralele. */
|
||||
.env-cards { display:grid; grid-template-columns:1fr 1fr; gap:14px; margin-bottom:18px; }
|
||||
.env-card { border:1px solid var(--line); border-radius:10px; padding:16px; background:var(--card2); }
|
||||
.env-card.active-test { border-color:color-mix(in srgb,var(--ok) 45%,var(--line)); }
|
||||
.env-card h4 { margin:0 0 12px; font-size:var(--fs-sm); display:flex; align-items:center;
|
||||
justify-content:space-between; gap:8px; }
|
||||
.env-card .env-sw { display:flex; align-items:center; gap:8px; font-size:var(--fs-sm);
|
||||
color:var(--muted); margin-bottom:12px; }
|
||||
.env-card .env-lbl { display:block; font-size:var(--fs-xs); color:var(--muted); margin-bottom:5px; }
|
||||
.env-card input[type=email], .env-card input[type=password] {
|
||||
width:100%; font:inherit; background:var(--bg); color:var(--ink); border:1px solid var(--line);
|
||||
border-radius:6px; padding:8px 10px; margin-bottom:10px; font-size:var(--fs-sm); }
|
||||
.env-card .env-confirm { display:flex; align-items:flex-start; gap:8px; font-size:var(--fs-xs);
|
||||
color:var(--muted); line-height:1.4;
|
||||
background:color-mix(in srgb,var(--err) 7%,transparent);
|
||||
border:1px solid color-mix(in srgb,var(--err) 22%,transparent);
|
||||
border-radius:7px; padding:9px 11px; margin-top:2px; }
|
||||
.env-card .env-confirm input { margin-top:2px; flex-shrink:0; }
|
||||
.default-env { display:flex; align-items:center; gap:12px; flex-wrap:wrap; margin:0 0 18px;
|
||||
padding:14px; border:1px solid var(--line); border-radius:9px; background:var(--card2); }
|
||||
.default-env select { font:inherit; background:var(--bg); color:var(--ink); border:1px solid var(--line);
|
||||
border-radius:6px; padding:7px 10px; max-width:340px; }
|
||||
@media (max-width:640px) { .env-cards { grid-template-columns:1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{# US-010 (PRD 5.16): antet branduit ROA AUTOPASS.
|
||||
{# Antet branduit ROA AUTOPASS.
|
||||
Grila 3 coloane — stanga (logo) | centru (titlu+env+tier+account_name) | dreapta (RAR dot + tema + burger).
|
||||
Antet MINIMAL pe /login: nu afiseaza RAR dot, meniu burger sau account_name (nelogat). #}
|
||||
<header>
|
||||
@@ -850,7 +928,7 @@
|
||||
</div>
|
||||
{# Celula dreapta: dot RAR (numai cand logat) + selector tema + versiune + meniu burger #}
|
||||
<div class="header-right">
|
||||
{# US-003 (PRD 5.16): dot RAR in antet — OK = chip verde pulsant, BLOCAT = chip rosu.
|
||||
{# Dot RAR in antet — OK = chip verde pulsant, BLOCAT = chip rosu.
|
||||
Banda plina apare DOAR in _status.html cand BLOCAT (nu mai e mereu vizibila). #}
|
||||
{% if is_authenticated|default(false) %}
|
||||
{% if sanatate_ok|default(true) %}
|
||||
@@ -868,11 +946,11 @@
|
||||
<span class="rar-tx">RAR blocat</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{# US-004 (PRD 5.19 / D1): comutatorul "Trimite automat la RAR" langa semnalul RAR.
|
||||
In afara swap-ului periodic al #status-bar (D3). #}
|
||||
{# Comutatorul "Trimite automat la RAR" langa semnalul RAR.
|
||||
In afara swap-ului periodic al #status-bar. #}
|
||||
{% include '_auto_send_toggle.html' %}
|
||||
{% endif %}
|
||||
{# US-011 (PRD 5.16): selector tema = pill cu icon FIX (acelasi SVG ca landing) +
|
||||
{# Selector tema = pill cu icon FIX (acelasi SVG ca landing) +
|
||||
eticheta temei curente. Eticheta ascunsa pe <=560px via CSS. JS actualizeaza
|
||||
doar #tema-label (iconita nu se mai schimba per tema — consecventa cu landing). #}
|
||||
<button id="tema-toggle" class="tema-btn"
|
||||
@@ -885,14 +963,14 @@
|
||||
<span class="muted" style="font-size:var(--fs-xs);">v{{ version }}</span>
|
||||
{% if is_authenticated|default(false) %}
|
||||
{# Meniu cont: Cont/Integrare/Nomenclator + (admin) + logout.
|
||||
US-010: structura cu <hr> separatori + RAR status (prima intrare) + Plan tier.
|
||||
Structura cu <hr> separatori + RAR status (prima intrare) + Plan tier.
|
||||
Pe paginile neautentificate (login/signup) nu se randeaza deloc. #}
|
||||
<div class="cont-menu-wrap">
|
||||
<button id="cont-menu-toggle" class="icon-btn"
|
||||
aria-haspopup="true" aria-expanded="false" aria-controls="cont-menu"
|
||||
aria-label="Meniu cont" title="Meniu cont">☰</button>
|
||||
<div id="cont-menu" class="cont-menu" role="menu" aria-labelledby="cont-menu-toggle" hidden>
|
||||
{# Prima intrare: starea RAR (US-003) #}
|
||||
{# Prima intrare: starea RAR #}
|
||||
{% if sanatate_ok|default(true) %}
|
||||
<div class="menu-rar-line ok" role="menuitem" aria-disabled="true">
|
||||
<span style="width:8px;height:8px;border-radius:99px;background:currentColor;display:inline-block;"></span>
|
||||
@@ -904,31 +982,31 @@
|
||||
RAR indisponibil
|
||||
</div>
|
||||
{% endif %}
|
||||
{# Plan cont curent (US-006 PRD 5.17): linie detaliata cu trial/consum/warn.
|
||||
Warn = culoare + text (accesibilitate, decizie #14). #}
|
||||
{# Plan cont curent: linie detaliata cu trial/consum/warn.
|
||||
Warn = culoare + text (accesibilitate). #}
|
||||
<div class="menu-rar-line" role="menuitem" aria-disabled="true"
|
||||
style="color:{% if plan_limita_atinsa|default(false) %}var(--err){% elif plan_warn|default(false) %}var(--warn){% else %}var(--muted){% endif %};
|
||||
{% if plan_warn|default(false) %}font-weight:600;{% endif %}">
|
||||
{{ plan_linie|default('Plan: ' + (tier_label|default('Gratuit'))) }}
|
||||
</div>
|
||||
<hr>
|
||||
{# Navigare principala: Trimiteri + Mapari #}
|
||||
<a role="menuitem" href="/">Trimiteri</a>
|
||||
{# Grup LUCRU: fluxul zilnic — trimiteri, mapari, nomenclator. #}
|
||||
<div class="cont-menu-group">Lucru</div>
|
||||
<a role="menuitem" href="/"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M22 2 11 13"/><path d="M22 2 15 22l-4-9-9-4z"/></svg></span>Trimiteri</a>
|
||||
{% set _mapari_badge = (badges.mapari if (badges is defined and badges and badges.mapari) else 0) %}
|
||||
<a role="menuitem" href="/?tab=mapari">Mapari{% if _mapari_badge %}<span class="tab-badge" aria-hidden="true" style="display:inline-flex; align-items:center; justify-content:center; min-width:18px; height:18px; margin-left:6px; padding:0 5px; border-radius:99px; background:var(--err); color:#fff; font-size:11px; font-weight:700;">{{ _mapari_badge }}</span>{% endif %}</a>
|
||||
<a role="menuitem" href="/?tab=mapari"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14 14 10"/><path d="M11.5 6.5 13 5a4 4 0 0 1 6 6l-1.5 1.5"/><path d="M12.5 17.5 11 19a4 4 0 0 1-6-6l1.5-1.5"/></svg></span>Mapari{% if _mapari_badge %}<span class="tab-badge" aria-hidden="true" style="display:inline-flex; align-items:center; justify-content:center; min-width:18px; height:18px; padding:0 5px; border-radius:99px; background:var(--err); color:#fff; font-size:11px; font-weight:700;">{{ _mapari_badge }}</span>{% endif %}</a>
|
||||
<a role="menuitem" href="/?tab=nomenclator"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4a1 1 0 0 1 1-1h13v18H6a1 1 0 0 0-1 1z"/><path d="M5 18h14"/></svg></span>Nomenclator</a>
|
||||
<hr>
|
||||
{# Nomenclator: coduri RAR — public, dar in meniu arata mai logic la logat #}
|
||||
<a role="menuitem" href="/?tab=nomenclator">Nomenclator</a>
|
||||
<hr>
|
||||
{# Setari cont #}
|
||||
<a role="menuitem" href="/?tab=cont">Cont</a>
|
||||
<a role="menuitem" href="/?tab=integrare">Integrare</a>
|
||||
<a role="menuitem" href="/?tab=jurnal">Jurnal</a>
|
||||
{% if is_admin|default(false) %}<a role="menuitem" href="/admin">Conturi clienti</a>{% endif %}
|
||||
{# Grup CONT: configurare + audit + admin. "Cont" redenumit "Setari" (URL ramane ?tab=cont). #}
|
||||
<div class="cont-menu-group">Cont</div>
|
||||
<a role="menuitem" href="/?tab=cont"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.6 4.6l2.1 2.1M17.3 17.3l2.1 2.1M4.6 19.4l2.1-2.1M17.3 6.7l2.1-2.1"/></svg></span>Setari</a>
|
||||
<a role="menuitem" href="/?tab=integrare"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 3v6M15 3v6"/><path d="M7 9h10v3a5 5 0 0 1-10 0z"/><path d="M12 17v4"/></svg></span>Integrare</a>
|
||||
<a role="menuitem" href="/?tab=jurnal"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M7 3h8l4 4v14H7z"/><path d="M15 3v4h4"/><path d="M10 13h6M10 17h6"/></svg></span>Jurnal</a>
|
||||
{% if is_admin|default(false) %}<a role="menuitem" href="/admin"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="9" cy="8" r="3"/><path d="M3.5 20a5.5 5.5 0 0 1 11 0"/><path d="M16 5.5a3 3 0 0 1 0 5"/><path d="M17.5 14.2A5.5 5.5 0 0 1 21 20"/></svg></span>Conturi clienti</a>{% endif %}
|
||||
<hr>
|
||||
<form method="post" action="/logout">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token|default('') }}">
|
||||
<button role="menuitem" type="submit">Iesi din cont</button>
|
||||
<button role="menuitem" type="submit"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M14 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2v-3"/><path d="M9 12h12"/><path d="m18 9 3 3-3 3"/></svg></span>Iesi din cont</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -954,12 +1032,12 @@
|
||||
<div id="detaliu-modal-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
{# Slot pentru modalul de confirmare tipata (PRD 5.19 D4): auto-release OFF->ON injecteaza
|
||||
{# Slot pentru modalul de confirmare tipata: auto-release OFF->ON injecteaza
|
||||
aici modalul via OOB (#modal-confirma-slot). Bulk "Trimite toate" isi randeaza propriul
|
||||
modal inline in _coada.html. #}
|
||||
<div id="modal-confirma-slot"></div>
|
||||
<script>
|
||||
// Comutator tema ciclic (DRY E2 — PRD 5.15): config traieste intr-o singura structura
|
||||
// Comutator tema ciclic: config traieste intr-o singura structura
|
||||
// sursa-de-adevar THEMES din care se DERIVA CYCLE/VALID/LABELS/NEXT.
|
||||
// Adaugarea unei teme noi = O singura intrare in THEMES.
|
||||
// Ciclu: Light->Dark->Petrol->Grafit->Cobalt->Cupru->Hartie->Auto->(inapoi la Light).
|
||||
@@ -997,7 +1075,7 @@
|
||||
}
|
||||
function _syncButton(stored) {
|
||||
var s = VALID[stored] ? stored : 'auto';
|
||||
// US-011: iconita e fixa (nu se schimba per tema); actualizam doar eticheta.
|
||||
// Iconita e fixa (nu se schimba per tema); actualizam doar eticheta.
|
||||
var label = document.getElementById('tema-label');
|
||||
if (label) label.textContent = LABELS[s];
|
||||
btn.setAttribute('aria-label', 'Tema: ' + LABELS[s] + ', apasa pentru ' + NEXT[s]);
|
||||
@@ -1109,6 +1187,28 @@
|
||||
if (saveBtn) saveBtn.classList.add('dirty');
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
// Sub-taburi pagina Setari: comutare pur client-side, fara server.
|
||||
// Delegare pe document -> supravietuieste swap-urilor HTMX ale #card-cont (dupa
|
||||
// fiecare salvare serverul re-randeaza cu sub-tabul corect via `aria-selected`/`hidden`,
|
||||
// iar acest handler preia doar navigarea manuala intre taburi).
|
||||
document.addEventListener('click', function(e) {
|
||||
var tab = e.target.closest('.setari-subtab');
|
||||
if (!tab) return;
|
||||
var card = tab.closest('#card-cont');
|
||||
if (!card) return;
|
||||
var name = tab.getAttribute('data-subtab');
|
||||
card.querySelectorAll('.setari-subtab').forEach(function(t) {
|
||||
var on = t === tab;
|
||||
t.setAttribute('aria-selected', on ? 'true' : 'false');
|
||||
t.classList.toggle('is-active', on);
|
||||
});
|
||||
card.querySelectorAll('.setari-pane').forEach(function(p) {
|
||||
if (p.getAttribute('data-subpane') === name) { p.removeAttribute('hidden'); }
|
||||
else { p.setAttribute('hidden', ''); }
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
// Toast global: feedback tranzitoriu vizibil + accesibil (aria-live).
|
||||
// window.arataToast(text, stareCss) — stareCss (ex. "s-error"/"s-needs_review")
|
||||
@@ -1274,7 +1374,7 @@
|
||||
// Deschidere la click pe rand (htmx:beforeRequest): arata modalul cu placeholder
|
||||
// inainte ca raspunsul fragmentului sa fie swap-uit in corp.
|
||||
// Trateaza atat .trimitere-row (Trimiteri) cat si .btn-editeaza (preview import)
|
||||
// → open() instaleaza inert pe <main>, focus-trap si readuce focusul la inchidere (US-006).
|
||||
// → open() instaleaza inert pe <main>, focus-trap si readuce focusul la inchidere.
|
||||
document.body.addEventListener('htmx:beforeRequest', function(evt) {
|
||||
var elt = evt.detail && evt.detail.elt;
|
||||
if (!elt || !elt.classList) return;
|
||||
@@ -1387,7 +1487,7 @@
|
||||
// Daca difera (schimbari externe — ex. worker a procesat trimiteri), reincarca automat
|
||||
// pastrand filtrul curent. Fara nudge "Date noi" — auto-refresh e mai consistent.
|
||||
// Decizie: nudge eliminat; distinctia propriu vs extern e imposibila pe client
|
||||
// fara sesiune dedicata — auto-refresh acoper ambele cazuri (US-008).
|
||||
// fara sesiune dedicata — auto-refresh acoper ambele cazuri.
|
||||
var INTERVAL = 20000;
|
||||
function versiuneCurenta() {
|
||||
var e = document.getElementById('trimiteri-versiune');
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
"""Teste US-007 (PRD 3.3b): sectiunea 'Contul meu' — rotire cheie API + creds RAR din UI.
|
||||
|
||||
TDD: testele se scriu INAINTE de implementare; la inceput pica (RED),
|
||||
dupa implementare trec (GREEN).
|
||||
"""Sectiunea 'Contul meu' — rotire cheie API + creds RAR din UI.
|
||||
|
||||
Rute testate:
|
||||
- GET /_fragments/cont -> card "Contul meu"
|
||||
@@ -25,6 +22,10 @@ def client(monkeypatch):
|
||||
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "t.db"))
|
||||
from app.config import get_settings
|
||||
get_settings.cache_clear()
|
||||
# Izolare rate-limit login intre teste (limiter global per-IP pe viata procesului):
|
||||
# fara reset, seria de _login din fisier depaseste login_rate_max si da 429 (fals negativ).
|
||||
from app.web import ratelimit
|
||||
ratelimit._hits.clear()
|
||||
from app.main import app
|
||||
with TestClient(app, follow_redirects=False) as c:
|
||||
yield c
|
||||
@@ -162,7 +163,7 @@ def test_fragment_cont_nelogat_redirect(monkeypatch):
|
||||
|
||||
|
||||
# ============================================================
|
||||
# US-002: sectiunea 'Date firma' + banner cont incomplet
|
||||
# Sectiunea 'Date firma' + banner cont incomplet
|
||||
# ============================================================
|
||||
|
||||
def _create_complete_account(
|
||||
@@ -282,3 +283,117 @@ def test_banner_cont_incomplet_pe_legacy(client):
|
||||
# Banner trebuie sa apara cand contul e incomplet
|
||||
assert "completeaza" in text or "date firm" in text or "incomplet" in text, \
|
||||
f"Banner 'Completeaza datele firmei' lipsa pe Acasa: {resp.text[:2000]}"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Sub-tab Cont: schimbare parola de acces (PRD 5.21)
|
||||
# ============================================================
|
||||
|
||||
def _get_csrf_from_cont(client) -> str:
|
||||
"""CSRF token din panoul Setari (tab=cont)."""
|
||||
resp = client.get("/?tab=cont")
|
||||
assert resp.status_code == 200
|
||||
m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text)
|
||||
if not m:
|
||||
m = re.search(r'value="([^"]+)"\s+name="csrf_token"', resp.text)
|
||||
assert m, f"csrf_token negasit in panoul Setari: {resp.text[:500]}"
|
||||
return m.group(1)
|
||||
|
||||
|
||||
def test_setari_subtaburi_prezente(client):
|
||||
"""Panoul Setari expune cele 4 sub-taburi + forma de schimbare parola."""
|
||||
_create_account_user("subtab@test.com")
|
||||
_login(client, "subtab@test.com", "parolasecreta10")
|
||||
|
||||
resp = client.get("/?tab=cont")
|
||||
assert resp.status_code == 200
|
||||
html = resp.text
|
||||
for eticheta in ("Date firma", "Credentiale RAR", "Cheie API", "Parola de acces"):
|
||||
assert eticheta in html, f"Lipseste sub-tabul/zona '{eticheta}': {html[:800]}"
|
||||
assert 'hx-post="/cont/schimba-parola"' in html, "Lipseste forma de schimbare parola"
|
||||
|
||||
|
||||
def test_schimba_parola_succes(client):
|
||||
"""Schimbare parola cu parola actuala corecta -> succes + login cu parola noua."""
|
||||
_create_account_user("pwok@test.com", "parolaveche10")
|
||||
_login(client, "pwok@test.com", "parolaveche10")
|
||||
csrf = _get_csrf_from_cont(client)
|
||||
|
||||
resp = client.post("/cont/schimba-parola", data={
|
||||
"csrf_token": csrf,
|
||||
"parola_actuala": "parolaveche10",
|
||||
"parola_noua": "parolanoua123",
|
||||
"parola_confirma": "parolanoua123",
|
||||
})
|
||||
assert resp.status_code == 200, f"POST schimba-parola a returnat {resp.status_code}"
|
||||
assert "schimbat" in resp.text.lower(), f"Lipseste confirmarea: {resp.text[:600]}"
|
||||
|
||||
# Parola noua verifica in DB
|
||||
from app.db import get_connection
|
||||
from app.users import verify_password
|
||||
conn = get_connection()
|
||||
try:
|
||||
assert verify_password(conn, "pwok@test.com", "parolanoua123") is not None, \
|
||||
"Parola noua nu autentifica"
|
||||
assert verify_password(conn, "pwok@test.com", "parolaveche10") is None, \
|
||||
"Parola veche inca autentifica dupa schimbare"
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_schimba_parola_actuala_gresita(client):
|
||||
"""Parola actuala gresita -> eroare, parola neschimbata."""
|
||||
_create_account_user("pwbad@test.com", "parolaveche10")
|
||||
_login(client, "pwbad@test.com", "parolaveche10")
|
||||
csrf = _get_csrf_from_cont(client)
|
||||
|
||||
resp = client.post("/cont/schimba-parola", data={
|
||||
"csrf_token": csrf,
|
||||
"parola_actuala": "gresita000",
|
||||
"parola_noua": "parolanoua123",
|
||||
"parola_confirma": "parolanoua123",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
assert "incorect" in resp.text.lower(), f"Lipseste eroarea de parola gresita: {resp.text[:600]}"
|
||||
|
||||
from app.db import get_connection
|
||||
from app.users import verify_password
|
||||
conn = get_connection()
|
||||
try:
|
||||
assert verify_password(conn, "pwbad@test.com", "parolaveche10") is not None, \
|
||||
"Parola veche trebuia sa ramana valida dupa esec"
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_schimba_parola_confirmare_nepotrivita(client):
|
||||
"""Confirmarea nu se potriveste -> eroare, fara scriere."""
|
||||
_create_account_user("pwmis@test.com", "parolaveche10")
|
||||
_login(client, "pwmis@test.com", "parolaveche10")
|
||||
csrf = _get_csrf_from_cont(client)
|
||||
|
||||
resp = client.post("/cont/schimba-parola", data={
|
||||
"csrf_token": csrf,
|
||||
"parola_actuala": "parolaveche10",
|
||||
"parola_noua": "parolanoua123",
|
||||
"parola_confirma": "altceva99999",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
assert "confirmarea" in resp.text.lower() or "potriv" in resp.text.lower(), \
|
||||
f"Lipseste eroarea de confirmare: {resp.text[:600]}"
|
||||
|
||||
|
||||
def test_schimba_parola_prea_scurta(client):
|
||||
"""Parola noua sub 10 caractere -> respinsa cu mesaj."""
|
||||
_create_account_user("pwshort@test.com", "parolaveche10")
|
||||
_login(client, "pwshort@test.com", "parolaveche10")
|
||||
csrf = _get_csrf_from_cont(client)
|
||||
|
||||
resp = client.post("/cont/schimba-parola", data={
|
||||
"csrf_token": csrf,
|
||||
"parola_actuala": "parolaveche10",
|
||||
"parola_noua": "scurt",
|
||||
"parola_confirma": "scurt",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
assert "scurt" in resp.text.lower(), f"Lipseste eroarea de lungime: {resp.text[:600]}"
|
||||
|
||||
Reference in New Issue
Block a user