feat(signup+admin): aliniere formular signup la landing + plan cerut, GDPR, control tier/trial in panou

Signup:
- /signup aliniat ca format la formularul din landing (campuri, etichete,
  placeholder-uri, select plan, checkbox GDPR, buton). Eticheta `name` = "Companie"
  (corecta: backendul salveaza nume de firma), uniform si in landing.
- Consimtamant GDPR validat server-side (functional, nu doar client-side) + salvat
  cu marca temporala (accounts.consent_at).
- Plan ales la signup salvat in accounts.requested_plan (intentie, NU drept): tier
  ramane sursa de adevar pentru gate-ul API; coloana pregateste integrarea platilor.
- landing: valorile `plan` = coduri tier (free/standard/pro/premium), data-plan
  sincronizat pe butoanele de pret; checkbox consimtamant primeste name.

Schema/DB:
- accounts: coloane noi requested_plan + consent_at (cu migrare aditiva in db.py).

Panou admin:
- Coloane noi: Plan curent (plan EFECTIV acum + zile trial ramase) si Plan cerut.
- Buton "Aplica" (POST /admin/set-tier): aloca plan real si INCHEIE trial-ul
  (efect imediat; altfel trial-ul Pro universal de 30z masca alegerea).
- Control "Trial Pro N zile" (POST /admin/set-trial via accounts.set_trial):
  acorda/prelungeste trial fara a schimba tier-ul de baza.

Teste: signup (consent obligatoriu, requested_plan persistat, tier ramane free),
panou admin (set-tier incheie trial, free opreste Pro imediat, set-trial, validari
+ CSRF). Call-site-urile existente POST /signup actualizate cu consent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-29 16:02:37 +00:00
parent a29896a790
commit 851f76ca16
14 changed files with 528 additions and 42 deletions

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from datetime import datetime, timezone
from pathlib import Path
from fastapi import APIRouter, Form, Request
@@ -9,7 +10,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from .. import __version__
from ..accounts import create_account
from ..accounts import VALID_TIERS, create_account
from ..auth import create_api_key
from ..config import get_settings
from ..db import get_connection
@@ -47,10 +48,18 @@ async def signup_post(
cui: str = Form(default=""),
email: str = Form(default=""),
parola: str = Form(default=""),
plan: str = Form(default=""),
consent: str = Form(default=""),
csrf_token: str = Form(default=""),
):
verify_csrf(request, csrf_token)
# Planul CERUT (intentie, nu drept): pastram doar valori valide; orice altceva -> 'free'.
# `tier`-ul real ramane 'free' la creare; planul ales se onoreaza dupa plata (admin/webhook).
requested_plan = plan.strip().lower() if plan else ""
if requested_plan not in VALID_TIERS:
requested_plan = "free"
settings = get_settings()
ip = request.client.host if request.client else "unknown"
if not check_rate_limit(ip, settings.signup_rate_max, settings.signup_rate_window_s):
@@ -58,7 +67,7 @@ async def signup_post(
request,
csrf_token=get_csrf_token(request),
error=_RATE_MSG,
name=name, cui=cui, email=email,
name=name, cui=cui, email=email, plan=requested_plan,
), status_code=429)
if len(parola) < _PASSWORD_MIN:
@@ -66,7 +75,7 @@ async def signup_post(
request,
csrf_token=get_csrf_token(request),
error=f"Parola trebuie sa aiba cel putin {_PASSWORD_MIN} caractere.",
name=name, cui=cui, email=email,
name=name, cui=cui, email=email, plan=requested_plan,
), status_code=422)
# CUI obligatoriu la signup (US-001, PRD 5.12)
@@ -76,9 +85,19 @@ async def signup_post(
request,
csrf_token=get_csrf_token(request),
error="CUI-ul firmei este obligatoriu.",
name=name, cui=cui, email=email,
name=name, cui=cui, email=email, plan=requested_plan,
), status_code=422)
# Consimtamant Termeni + GDPR obligatoriu (proba). Checkbox bifat -> valoare ne-goala.
if not (consent and consent.strip()):
return _TMPL.TemplateResponse(request, "signup.html", _ctx(
request,
csrf_token=get_csrf_token(request),
error="Trebuie sa accepti Termenii si prelucrarea datelor (GDPR) pentru a crea cont.",
name=name, cui=cui, email=email, plan=requested_plan,
), status_code=422)
consent_at = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
# Bootstrap admin: count_admins se citeste INAUNTRUL tranzactiei BEGIN IMMEDIATE,
# astfel lock-ul RESERVED serializeaza scriitorii si al doilea signup vede count==1.
conn = get_connection()
@@ -86,7 +105,10 @@ async def signup_post(
conn.execute("BEGIN IMMEDIATE")
try:
is_first = count_admins(conn) == 0
account_id = create_account(conn, name, cui=cui_norm, email=email, active=False)
account_id = create_account(
conn, name, cui=cui_norm, email=email, active=False,
requested_plan=requested_plan, consent_at=consent_at,
)
user_id = create_user(conn, account_id, email, parola, is_admin=is_first)
api_key = create_api_key(conn, account_id)
conn.execute("COMMIT")
@@ -121,7 +143,7 @@ async def signup_post(
request,
csrf_token=get_csrf_token(request),
error=error_msg,
name=name, cui=cui, email=email,
name=name, cui=cui, email=email, plan=requested_plan,
), status_code=422)
except Exception as exc:
conn.execute("ROLLBACK")
@@ -129,7 +151,7 @@ async def signup_post(
request,
csrf_token=get_csrf_token(request),
error=str(exc),
name=name, cui=cui, email=email,
name=name, cui=cui, email=email, plan=requested_plan,
), status_code=422)
finally:
conn.close()