Volver a la wiki

Servicio send_welcome_email — email de bienvenida con enlace de set password

Ubicación y dependencias

Archivo: core/email_service.py
Función pública: send_welcome_email(user: User) → bool
Dependencias:

Responsabilidades

  1. Generar el token del enlace “Set password”:

    • _generate_set_password_url(user) crea un URL tipo {SITE_URL}/accounts/password/reset/key/{uidb36}-{token}/
    • El token se calcula sobre el hash del password del usuario y su PK → si el password cambia después, el token se invalida
    • Nota arquitectónica (v1.65.3): el email debe enviarse DESPUÉS de que el usuario esté persistido en BD con su password final. Ver [[entity—core—service—send-welcome-emailmecanica-de-llamada-post-save-con-transaction-on-commit]]
  2. Construir el contexto de template:

    • user_name, username, organization_name, role_display
    • login_url: base pública del SaaS desde settings.SITE_URL
    • set_password_url: el enlace con token
    • expires_days: caducidad del enlace en días (derivado de PASSWORD_RESET_TIMEOUT, default 3)
    • year: para el pie de página
  3. Renderizar y enviar:

    • Template: templates/emails/welcome.html (MJML-based, responsive)
    • Backend de email: usa ANYMAIL + RESEND_API_KEY en producción; en desarrollo/tests usa locmem
    • Corta en silencio sin RESEND_API_KEY (no fallar en dev)

Sub-componentes

_generate_set_password_url(user: User) → str

Genera la URL del enlace de set password.

Lógica:

token_generator = EmailAwarePasswordResetTokenGenerator()
token = token_generator.make_token(user)  # hash(user.password + user.pk + ...)
uidb36 = int_to_base36(user.pk)           # PK codificado en base 36
return f"{settings.SITE_URL}/accounts/password/reset/key/{uidb36}-{token}/"

Dependencia de SITE_URL (v1.65.3): El hardcode anterior https://crearack.com se reemplazó para permitir que STAGE/dev generen URLs hacia su propio host.

send_welcome_email(user: User) → bool

Función pública que renderiza y envía el email.

Flujo:

  1. Verifica que el usuario tenga email no vacío
  2. Calcula expires_days a partir de PASSWORD_RESET_TIMEOUT
  3. Renderiza el template con contexto (incluye set_password_url y expires_days)
  4. Envía vía anymail.send()
  5. Retorna True si OK, False si no hay RESEND_API_KEY o error

Errores silenciosos: Si RESEND_API_KEY no está configurada (dev/tests sin override), retorna False sin lanzar excepción. Los tests que necesitan observar el outbox usan override_settings(ANYMAIL={"RESEND_API_KEY": "test-key"}, EMAIL_BACKEND="locmem").

Mecánica de llamada: post_save con transaction.on_commit

Invoker: Señal send_welcome_on_create en core/signals.py.

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

Por qué transaction.on_commit (v1.65.3, fix grave):

Contexto: Bug cazado en plan-review de F1 (task #137). El endpoint de signup hacía set_unusable_password() después del create_user, invalidando el token. Fijo en v1.65.3.

Datos de entrada y salida

Entrada

CampoTipoOrigenNota
user.emailstrModel UserObligatorio para enviar
user.first_namestrModel UserMostrado como “Hola {first_name}” en el email; fallback a username
user.organization.namestrFK a OrganizationContexto del email
user.rolestrModel UserEnum (admin/operator/viewer/member); mostrado via ROLE_LABELS
user.password (hash)strModel UserInput para el token; debe estar final
settings.SITE_URLstrConfigDefault https://crearack.com
settings.PASSWORD_RESET_TIMEOUTintConfigDefault 259200 (3 días), en segundos

Salida

ArtefactoMedioNota
Email HTML/textRESEND (prod) o locmem (tests)Template responsive MJML, i18n ES/EN
Return valueboolTrue si enviado, False si no hay API key

Template

Archivo: templates/emails/welcome.html

Variables esperadas:

Traducciones: El email tiene bloques {% blocktrans %} y {% trans %} para ES/EN. La caducidad está i18n desde v1.65.3.

Testing

Test file: tests/api/test_signup_activation.py

Test cases:

  1. test_activation_link_token_is_valid: flujo completo (signup → email real → token valida)
  2. test_email_links_respect_site_url: con SITE_URL fijada, los enlaces apuntan a ese host
  3. test_admin_created_user_token_also_valid: creación de usuario por admin también genera tokens válidos

Setup necesario: django_capture_on_commit_callbacks(execute=True) en cada test, porque transaction.on_commit no dispara automáticamente bajo pytest.

Véase también

Subir