Servicio signup-activation — flujo de registro de nueva organización
Ubicación y dependencias
Módulo: core.api.signup
Endpoints:
POST /api/signup/(viewset handlersignup(request, payload: SignupInput))
Dependencias principales:
core.models: User, Organization, SaaSModule, Plancore.email_service: send_welcome_email (vía post_save signal)django.db.transaction: para commit de estado finalallauth.account.models: PasswordResetTokenGenerator (en el email, no aquí)
Responsabilidades
- Crear una nueva organización a partir de
organization_name - Asignar plan y módulos basados en flags (
add_racks,add_network,add_monitoring) - Crear el usuario admin sin password (set vía email)
- Enviar email de bienvenida con enlace de set password (vía señal post_save)
Diagrama de flujo (v1.65.3)
POST /api/signup/
├─ Valida payload: organization_name, email, username, flags
├─ Verifica no existe org con mismo name
├─ Crea Organization (extra_modules vacío inicialmente)
├─ Selecciona Plan según flags (Core Free, Core Pro, etc.)
├─ Asigna módulos de la app (racks, network, monitoring, signage)
├─ Asigna módulos de la suscripción del plan
├─ Crea User con password=None (inusable)
│ └─ POST_SAVE SIGNAL DISPARA → transaction.on_commit(send_welcome_email)
└─ Return: 201 + organization_id, user_id, username
Nota (v1.65.3): El password se fija una sola vez en create_user(password=None). La línea user.set_unusable_password(); user.save(update_fields=["password"]) fue eliminada porque invalidaba el token del email (el token hashea el password).
Sub-componentes
Input: SignupInput
Pydantic model con campos:
| Campo | Tipo | Requerido | Nota |
|---|---|---|---|
organization_name | str | Sí | Nombre único de la org |
email | str | Sí | Email del admin creator |
username | str | Sí | Username único |
add_racks | bool | No (default False) | Incluir módulo Racks |
add_network | bool | No (default False) | Incluir módulo Network |
add_monitoring | bool | No (default False) | Incluir módulo Monitoring |
Paso 1: Validación y creación de Organization
# Verificar unicidad
if Organization.objects.filter(name=organization_name).exists():
raise BusinessLogicException(...)
# Crear org
org = Organization.objects.create(
name=organization_name,
created_by_user_id=None, # nadie, es el primer actor
)
Modelos involucrados:
Organization(PK, name, extra_modules ManyToMany)
Paso 2: Selección de Plan
# Flag-driven plan selection
if payload.add_racks or payload.add_network or payload.add_monitoring:
plan = Plan.objects.get(slug="core-pro")
else:
plan = Plan.objects.get(slug="core-free")
Datos:
- Plan.modules → ManyToMany a SaaSModule
- Plan.slug → identificador (ej
core-pro,core-free)
Paso 3: Asignación de módulos
# Módulos de la app (core)
core_pks = set(SaaSModule.objects.filter(app="core").values_list("pk", flat=True))
# Módulos de la suscripción
plan_pks = set(plan.modules.values_list("pk", flat=True))
# Asignar a org (union)
org.extra_modules.set(core_pks | plan_pks)
Nota: Los módulos se guardan en la ManyToMany Organization.extra_modules (campo through).
Paso 4: Creación del usuario
User.objects.create_user(
username=username,
email=email,
password=None, # ← inusable, set vía email
first_name="",
role="admin",
organization=org,
)
# NO hacer set_unusable_password() aquí; el password ya es inusable
# La señal post_save dispara send_welcome_email vía transaction.on_commit
Por qué NO set_unusable_password() después (v1.65.3):
create_user(password=None)ya deja el password inusable- Si se toca el password de nuevo en la misma transacción, el token del email (que se calcula dentro de post_save) se invalida
- Solución: enviar el email vía
transaction.on_commit, no inmediatamente
Señal post_save: envío del email
Invocador: core.signals.send_welcome_on_create
@receiver(post_save, sender="core.User")
def send_welcome_on_create(sender, instance, created, **kwargs):
if created and instance.email:
from .email_service import send_welcome_email
transaction.on_commit(lambda: send_welcome_email(instance))
El email (send_welcome_email) genera un token que hashea el password del usuario. Si el password cambia entre el save y el envío, el token muere. Solución: transaction.on_commit garantiza que el email sale cuando la transacción ya se escribió en BD.
Email: Contiene un enlace “Crea tu contraseña” que apunta a /accounts/password/reset/key/{uidb36}-{token}/ (endpoint de allauth).
Datos de entrada y salida
Entrada (request body JSON)
{
"organization_name": "Acme Datacenters",
"email": "founder@acme.example",
"username": "acmefounder",
"add_racks": true,
"add_network": true,
"add_monitoring": false
}
Salida (201 Created)
{
"organization_id": 42,
"user_id": 123,
"username": "acmefounder",
"email": "founder@acme.example"
}
Errores y excepciones
| Caso | Código | Mensaje |
|---|---|---|
| Org name ya existe | 400 | “Organization with this name already exists” |
| Email inválido | 400 | (Pydantic validation) |
| Username duplicado | 400 | (Django constraint) |
| Plan no existe | 500 | “Plan not found” (no debe ocurrir en prod) |
Contexto histórico
v1.65.2
- Se cerró una puerta de registro no intencionada (endpoint sin UI)
- Endpoint
/api/signup/aún no tiene página pública, así que el high fue limitado
v1.65.3
- Fix: El enlace del email de bienvenida nacía inválido debido a recalculado del password
- Causa: El endpoint hacía
set_unusable_password()después decreate_user(), invalidando el token - Solución:
- Password fijado una sola vez en
create_user(password=None) - Email enviado vía
transaction.on_commit(garantiza estado final de BD)
- Password fijado una sola vez en
- Testing: Nuevo test suite
tests/api/test_signup_activation.pycaza el bug
Testing
File: tests/api/test_signup_activation.py (109 líneas, v1.65.3)
Coverage:
test_activation_link_token_is_valid: el flujo completo y el token del email validatest_email_links_respect_site_url: con SITE_URL fijada, los enlaces salen correctostest_admin_created_user_token_also_valid: crear user por admin también genera tokens válidos
Setup crítico: django_capture_on_commit_callbacks(execute=True) en cada test, porque transaction.on_commit no dispara bajo pytest sin esto.
Verificación en rojo: El test pasa en verde (con fix) y pasa en rojo (reproduce el bug original del token muerto).
Véase también
- [[feature—auth—email-welcome-activation-link]]
- [[entity—core—service—send-welcome-email]]
- [[entity—core—model—user]]
- [[entity—core—model—organization]]
- [[entity—core—endpoint—signup]]