Files
rar-autopass/app/web/templates/_cont.html
Claude Agent 86408887e5 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>
2026-07-06 13:40:38 +00:00

236 lines
14 KiB
HTML

{# 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>
<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>
{# ============================ 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_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">
<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)">
</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">
</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">
</p>
<button type="submit">Salveaza datele firmei</button>
</form>
</div>
{# ========================= 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 %}
<form hx-post="/cont/rar-medii" hx-target="#card-cont" hx-swap="outerHTML">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<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>
<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>
<!-- 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>
<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>
{% 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>
<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">
{% 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 %}
<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>