Volver a la wiki

Servicio signup-activation — flujo de registro de nueva organización

Ubicación y dependencias

Módulo: core.api.signup
Endpoints:

Dependencias principales:

Responsabilidades

  1. Crear una nueva organización a partir de organization_name
  2. Asignar plan y módulos basados en flags (add_racks, add_network, add_monitoring)
  3. Crear el usuario admin sin password (set vía email)
  4. 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:

CampoTipoRequeridoNota
organization_namestrSíNombre único de la org
emailstrSíEmail del admin creator
usernamestrSíUsername único
add_racksboolNo (default False)Incluir módulo Racks
add_networkboolNo (default False)Incluir módulo Network
add_monitoringboolNo (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:

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:

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):

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

CasoCódigoMensaje
Org name ya existe400“Organization with this name already exists”
Email inválido400(Pydantic validation)
Username duplicado400(Django constraint)
Plan no existe500“Plan not found” (no debe ocurrir en prod)

Contexto histórico

v1.65.2

v1.65.3

Testing

File: tests/api/test_signup_activation.py (109 líneas, v1.65.3)

Coverage:

  1. test_activation_link_token_is_valid: el flujo completo y el token del email valida
  2. test_email_links_respect_site_url: con SITE_URL fijada, los enlaces salen correctos
  3. test_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

Subir