CreaRack-SL

Endpoint GET/POST /signup — página pública de registro invite-only (F1 PR-A)

Descripción

Página web pública (form-post server-side clásico) donde nuevos clientes introducen su código de invitación y crean su organización.

Ruta: GET/POST /signup (sin slash final, exacto) Auth: Público / anónimo Template: account/signup.html Vista: core/views_signup.py::signup_view (60 LOC) Middleware: EXENTO del TenantRLSMiddleware (verificado en plan v5)

Interfaz y flujo

GET /signup

Renderiza el formulario vacío con estética de login (account/base.html).

POST /signup

Procesa el form-post, valida, y:

  • Éxito: Renderiza página de confirmación con el email de activación
  • Error: Re-renderiza el formulario con errores por campo y globales

Campos del formulario

Campo HTMLLabel i18nValidaciónNotas
invitation_code“Invitation code”required, max 64 charsCódigo de invitación (será normalizado a UPPERCASE por el servicio)
organization_name“Company Name”required, max 255 charsNombre de la organización (único case-insensitive)
email“Email”required, email formatEmail del admin (único case-insensitive, recibe enlace de activación)
username“Username”required, max 150 charsUsername (único)

i18n: Reutiliza msgids existentes — “Username”, “Email”, “Company Name” están ya en locale/es/LC_MESSAGES/django.po.

Clase SignupForm (Django Form)

class SignupForm(forms.Form):
    invitation_code = forms.CharField(label=_("Invitation code"), max_length=64)
    organization_name = forms.CharField(label=_("Company Name"), max_length=255)
    email = forms.EmailField(label=_("Email"))
    username = forms.CharField(label=_("Username"), max_length=150)

Protecciones de seguridad

1. Cloudflare Turnstile

  • Tipo: Server-side challenge (widget iframe en la página)
  • Fail mode: Fail-closed (timeout 2s → rechazar)
  • Config: TURNSTILE_SITE_KEY (widget) y TURNSTILE_SECRET (validación)
  • Dev/test: Sin secret configurado → skip con log
  • Widget condicional: Solo se renderiza si TURNSTILE_SITE_KEY != "" — dev sin claves no ve widget

Integración:

  • POST → HTML POST with cf-turnstile-response token
  • Vista llama verify_turnstile() ANTES de create_signup()
  • Falla silenciosamente si Turnstile está down (log, pero no bloquea)

2. CSRF

Django CSRF ({% csrf_token %} en template) — token per-session.

3. Rate limiting

  • Path exacto: /signup
  • Límite: 15/min por IP (cuenta GETs + POSTs)
  • Fallback: STRICT_ENDPOINTS en core/middleware/rate_limit.py
  • Respuesta 429: errors/rate_limited.html (página legible) cuando el cliente Accept HTML
    • Patrón igual a account_paused/module_disabled (no requiere nonce CSP)

4. CSP (Content Security Policy)

  • Flag específico: needs_turnstile = path in _TURNSTILE_EXACT
  • Script-src: Agrega https://challenges.cloudflare.com solo en /signup
  • Frame-src: Agrega https://challenges.cloudflare.com solo en /signup
  • Isolado: localhost:5050 del agente NO heredado a /signup (public page)

Context y renderizado

Funciones auxiliar _ctx():

def _ctx(form, **extra):
    return {"form": form, "turnstile_site_key": settings.TURNSTILE_SITE_KEY, **extra}

Context variables:

  • form: Django Form con errores (si aplica)
  • turnstile_site_key: Clave pública de Turnstile (empty string → no widget)
  • signup_done: True si success (renderiza página de confirmación)
  • signup_email: Email del usuario creado (si success)

Flujo de vista detallado

def signup_view(request):
    if GET:
        return render(signup_form_vacia)
    
    # POST
    form = SignupForm(request.POST)
    if form.invalid:
        return render(formulario_con_errores)
    
    # Validar Turnstile (ANTES de crear nada)
    if not verify_turnstile(request.POST["cf-turnstile-response"]):
        form.add_error(None, "Human verification failed...")
        return render(formulario_con_error_global)
    
    # Crear org + user (tx atómica interna)
    try:
        org, user = create_signup(
            code=form.cleaned_data["invitation_code"],
            org_name=form.cleaned_data["organization_name"],
            email=form.cleaned_data["email"],
            username=form.cleaned_data["username"],
        )
    except SignupError as exc:
        form.add_error(None, str(exc))
        return render(formulario_con_error_global)
    
    # Éxito — renderizar confirmación
    return render(success_page)

Template account/signup.html

Campos del form renderizados:

  1. Código de invitación (text input)
  2. Nombre empresa (text input)
  3. Email (email input)
  4. Username (text input)
  5. Widget Turnstile (iframe, condicional)
  6. Pie: “Contact us” → hello@crearack.com
  7. Link desde login: “Have an invitation code?” (discreto)

Errores re-renderizados:

  • Por campo: form.field.errors bajo el input
  • Globales: form.non_field_errors al tope del form

Estado de éxito (si signup_done == True):

  • Oculta el form, muestra mensaje
  • “Confirmation email sent to {{ signup_email }}”
  • “Check your inbox for the activation link”

Integración con auth y signals

Post-create email: El modelo User tiene un post_save signal que dispara el welcome email. Solo ocurre tras on_commit() (tx completada):

# core/signals.py
@receiver(post_save, sender=User)
def send_welcome_email(sender, instance, created, **kwargs):
    if created:
        # El password es inusable; el email lleva link para setear
        transaction.on_commit(lambda: ...enviar_email...)

Estado actual

  • ✅ Endpoint GET/POST funcional
  • ✅ Form-post clásico (sin JS propio)
  • ✅ Turnstile server-side (fail-closed)
  • ✅ Rate limiting HTML 429
  • ✅ CSP aislado (challenges.cloudflare.com SOLO aquí)
  • ✅ i18n EN+ES (msgids reutilizados)
  • ✅ Email de activación automático
  • ✅ Tests con carrera del último código (42 tests, todos verdes)

Eliminaciones

Endpoint antiguo: POST /api/signup/ fue RETIRADO:

  • Nadie lo consumía (sin clientes públicos)
  • Menos superficie anónima
  • La lógica vive ahora en el servicio + vista web

Véase también

  • [[entity—core—service—signup]]
  • [[entity—core—model—invitation-code]]
  • [[feature—core—registro-invite-only-plan-f1]]
  • [[concept—saas—multi-tenancy]]
  • [[entity—core—service—signup-activation]]