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 HTML | Label i18n | Validación | Notas |
|---|---|---|---|
invitation_code | “Invitation code” | required, max 64 chars | Código de invitación (será normalizado a UPPERCASE por el servicio) |
organization_name | “Company Name” | required, max 255 chars | Nombre de la organización (único case-insensitive) |
email | “Email” | required, email format | Email del admin (único case-insensitive, recibe enlace de activación) |
username | “Username” | required, max 150 chars | Username (ú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) yTURNSTILE_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-responsetoken - Vista llama
verify_turnstile()ANTES decreate_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.comsolo en/signup - Frame-src: Agrega
https://challenges.cloudflare.comsolo 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:
- Código de invitación (text input)
- Nombre empresa (text input)
- Email (email input)
- Username (text input)
- Widget Turnstile (iframe, condicional)
- Pie: “Contact us” →
hello@crearack.com - Link desde login: “Have an invitation code?” (discreto)
Errores re-renderizados:
- Por campo:
form.field.errorsbajo el input - Globales:
form.non_field_errorsal 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]]
Referenciado desde
- El email de bienvenida ya trae un enlace que funciona
- Modelo InvitationCode — código invite-only para registro F1
- Servicio core.services.signup — alta de organización invite-only (F1 PR-A)
- Servicio send_welcome_email — email de bienvenida con enlace de set password
- Servicio signup-activation — flujo de registro de nueva organización