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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user