iMerica / dj-rest-auth

Authentication for Django Rest Framework
https://dj-rest-auth.readthedocs.io/en/latest/index.html
MIT License
1.67k stars 313 forks source link

`get_email_options` not work when `AllAuthPasswordResetForm` is used #651

Open ugnelis opened 2 months ago

ugnelis commented 2 months ago

I am trying to have a custom email template. When I use PasswordResetForm, everything seems to work fine:

from django.contrib.auth.forms import PasswordResetForm

class CustomPasswordResetSerializer(PasswordResetSerializer):
    password_reset_form_class = PasswordResetForm

    def get_email_options(self):
        return {
            "subject_template_name": "account/email/password_reset_subject.txt",
            "email_template_name": "account/email/password_reset_message.html",
            "html_email_template_name": "account/email/password_reset_message.html",
            "extra_email_context": {
                "settings": settings,
            }
        }

I am using allauth. In this case, in order to have the correct uid and token, I need to use AllAuthPasswordResetForm.

from dj_rest_auth.forms import AllAuthPasswordResetForm

class CustomPasswordResetSerializer(PasswordResetSerializer):
    password_reset_form_class = AllAuthPasswordResetForm

    def get_email_options(self):
        return {
            "subject_template_name": "account/email/password_reset_subject.txt",
            "email_template_name": "account/email/password_reset_message.html",
            "html_email_template_name": "account/email/password_reset_message.html",
            "extra_email_context": {
                "settings": settings,
            }
        }

But then get_email_options not working. It keeps sending the default template.

My settings look like this:

REST_AUTH = {
    "USE_JWT": True,
    "JWT_AUTH_HTTPONLY": False,
    "JWT_AUTH_COOKIE": "authToken",
    "JWT_AUTH_REFRESH_COOKIE": "refreshToken",
    "JWT_TOKEN_CLAIMS_SERIALIZER":
        "apps.authentication.serializers.CustomTokenObtainPairSerializer",
    "REGISTER_SERIALIZER":
        "apps.authentication.serializers.CustomRegisterSerializer",
    "PASSWORD_RESET_SERIALIZER":
        "apps.authentication.serializers.CustomPasswordResetSerializer",
}

Dependencies:

dj-rest-auth[with_social]==6.0.0
django-allauth==0.61.1
djangorestframework-simplejwt==5.2.2
djangorestframework==3.14.0

How to use my custom email template?

ugnelis commented 2 months ago

Based on dj_rest_auth/forms.py:


class AllAuthPasswordResetForm(DefaultPasswordResetForm):
    # ...
    def save(self, request, **kwargs):
        # ...
            for user in self.users:
                # ...
                if (
                    allauth_account_settings.AUTHENTICATION_METHOD != allauth_account_settings.AuthenticationMethod.EMAIL
                 ):
                    context['username'] = user_username(user)
                    get_adapter(request).send_mail(
                        'account/email/password_reset_key', email, context
                    )

It expects the template to be in account/email/password_reset_key, so having files like this:

solves the problem.

get_email_options pretty much does nothing even though it is being called

ugnelis commented 2 months ago

My solution is to override DefaultPasswordResetForm logic:

forms.py

from allauth.account import app_settings as allauth_account_settings
from allauth.account.adapter import get_adapter
from allauth.account.forms import ResetPasswordForm as DefaultPasswordResetForm
from allauth.account.forms import default_token_generator
from allauth.account.utils import (
    filter_users_by_email,
    user_pk_to_url_str,
    user_username,
)
from dj_rest_auth.forms import default_url_generator

class CustomPasswordResetForm(DefaultPasswordResetForm):
    def clean_email(self):
        email = self.cleaned_data["email"]
        email = get_adapter().clean_email(email)
        self.users = filter_users_by_email(email, is_active=True)
        return self.cleaned_data["email"]

    def save(self, request, **kwargs):
        email = self.cleaned_data["email"]
        token_generator = kwargs.get("token_generator", default_token_generator)

        for user in self.users:

            temp_key = token_generator.make_token(user)
            url_generator = kwargs.get("url_generator", default_url_generator)
            url = url_generator(request, user, temp_key)
            uid = user_pk_to_url_str(user)

            context = {
                "user": user,
                "password_reset_url": url,
                "request": request,
                "token": temp_key,
                "uid": uid,
            }
            extra_email_context = kwargs.get("extra_email_context", {})
            context.update(extra_email_context)

            if (
                allauth_account_settings.AUTHENTICATION_METHOD !=
                allauth_account_settings.AuthenticationMethod.EMAIL
            ):
                context["username"] = user_username(user)
            get_adapter(request).send_mail(
                "account/email/password_reset_key", email, context
            )

        return self.cleaned_data["email"]

serializers.py

from dj_rest_auth.serializers import PasswordResetSerializer

from config import settings
from .forms import CustomPasswordResetForm

class CustomPasswordResetSerializer(PasswordResetSerializer):
    password_reset_form_class = CustomPasswordResetForm

    def get_email_options(self):
        return {
            "extra_email_context": {
                "settings": settings,
            }
        }

and have files templates/account/email/password_reset_key_message.html and templates/account/email/password_reset_key_subject.txt

settings.py

REST_AUTH = {
    # ...
    "PASSWORD_RESET_SERIALIZER":
        "apps.authentication.serializers.CustomPasswordResetSerializer",
}