Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to send customized email template for password reset endpoint? #9

Closed
mohmyo opened this issue Mar 12, 2020 · 33 comments
Closed

How to send customized email template for password reset endpoint? #9

mohmyo opened this issue Mar 12, 2020 · 33 comments

Comments

@mohmyo
Copy link
Contributor

mohmyo commented Mar 12, 2020

My main goal is to customize the URL returned to the user in the mail so it uses the base URL of my frontend not the base URL of my backend

@iMerica
Copy link
Owner

iMerica commented Mar 12, 2020

This is how I do it on my projects (DRF API Server + React SPA or Next.js):


Declare a custom serializer for registration that inherits from the base. Then you have complete control over what is included in the email.

from dj_rest_auth.registration.serializers import RegisterSerializer

class CustomRegistrationSerializer(RegisterSerializer):
  def save(self, request):
     # handle all setup logic and email here. 

then, in your Django settings:

REST_AUTH_REGISTER_SERIALIZERS = {
    'REGISTER_SERIALIZER': 'path.to.your.CustomRegistrationSerializer'
}

@mohmyo
Copy link
Contributor Author

mohmyo commented Mar 13, 2020

I will try it and get back to you, thanks

@mohmyo
Copy link
Contributor Author

mohmyo commented Mar 13, 2020

I was able to do it similarly as you suggested.
I will share it here for anyone who comes later

Create your custom password reset serializer

from dj_rest_auth.serializers import PasswordResetSerializer

class CustomPasswordResetSerializer(PasswordResetSerializer):
    def save(self):
        request = self.context.get('request')
        # Set some values to trigger the send_email method.
        opts = {
            'use_https': request.is_secure(),
            'from_email': 'example@yourdomain.com',
            'request': request,
            # here I have set my desired template to be used
            # don't forget to add your templates directory in settings to be found
            'email_template_name': 'password_reset_email.html'
        }

        opts.update(self.get_email_options())
        self.reset_form.save(**opts)

UPDATE:

If you only want to customize email parameters nothing more or less, you can ignore overriding save() method and override get_email_options() instead, check it in source code here.

Ex:

from dj_rest_auth.serializers import PasswordResetSerializer

class MyPasswordResetSerializer(PasswordResetSerializer):

    def get_email_options(self) :
      
        return {
            'email_template_name': 'password_reset_email.html'
        }

get_email_options() is being called in save() just to update opts dict with any other options before triggering reset_form, here in source code.

then point to your custom serializer in settings.py

REST_AUTH_SERIALIZERS = {
    'PASSWORD_RESET_SERIALIZER': 'path.to.your.CustomPasswordResetSerializer'
}

@mohmyo mohmyo closed this as completed Mar 15, 2020
@aaronamelgar
Copy link

@mohmyo did you create your own base class for PasswordResetSerializer?

I only see RegisterSerializer in dj_rest_auth.registration.serializers

@mohmyo
Copy link
Contributor Author

mohmyo commented May 8, 2020

@aaronamelgar Nope, I have updated my above code snippet regarding the base class, check it out.
the reason you are seeing this is that registration is a separated module in dj_rest_auth

@Dilipsa
Copy link

Dilipsa commented Sep 29, 2020

please help me for password reset, its giving error as following

NoReverseMatch at /dj-rest-auth/password/reset/
Reverse for 'password_reset_confirm' with keyword arguments '{'uidb64': 'MQ', 'token': 'aay6u3-6409cacfd1a2284e6bfb54f61514d66b'}' not found. 1 pattern(s) tried: ['password-reset/confirm/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$']

@mohmyo
Copy link
Contributor Author

mohmyo commented Sep 29, 2020

This has been answered many times, see closed issues and you will find so many helpful discussions with the solution

@mohmyo
Copy link
Contributor Author

mohmyo commented Sep 29, 2020

Like this
#118 (comment)

@ShahriarDhruvo
Copy link

How to do the same (Edit the email template) for email sent to verify the email after registration?

@mohmyo
Copy link
Contributor Author

mohmyo commented Oct 1, 2020

This part is controlled by django-allauth, see

@ShahriarDhruvo
Copy link

Thanks, it is done 😎. If anyone having trouble getting the key sent at the end of the url you can use {{ key }} in that template to access the key and later goto the verify email address and post the key by frontend.

@colonder
Copy link

colonder commented Oct 3, 2020

@mohmyo How does your template look like? Where do you exactly put a custom URL pointing to the frontend?

@mohmyo
Copy link
Contributor Author

mohmyo commented Oct 4, 2020

@colonder can you be more specific?

@Abishek05
Copy link

Abishek05 commented Oct 7, 2020

I was able to do it similarly as you suggested.
I will share it here for anyone comes later

Create your custom password reset serializer

from dj_rest_auth.serializers import PasswordResetSerializer

class CustomPasswordResetSerializer(PasswordResetSerializer):
    def save(self):
        request = self.context.get('request')
        # Set some values to trigger the send_email method.
        opts = {
            'use_https': request.is_secure(),
            'from_email': 'example@yourdomain.com',
            'request': request,
            # here I have set my desired template to be used
            # don't forget to add your templates directory in settings to be found
            'email_template_name': 'password_reset_email.html'
        }

        opts.update(self.get_email_options())
        self.reset_form.save(**opts)

UPDATE:

If you only want to customize email parameters nothing more or less, you can ignore overriding save() method and override get_email_options() instead, check it is source code here.

Ex:

from dj_rest_auth.serializers import PasswordResetSerializer

class MyPasswordResetSerializer(PasswordResetSerializer):

    def get_email_options(self) :
      
        return {
            'email_template_name': 'password_reset_email.html'
        }

get_email_options() is being called in save() just to update opts dict with any other options before triggering reset_form, here in source code.

then point to your custom serializer in settings.py

REST_AUTH_SERIALIZERS = {
    'PASSWORD_RESET_SERIALIZER': 'path.to.your.CustomPasswordResetSerializer'
}

@mohmyo I have followed this and the template variables such as {{ password_reset_url }} are not being rendered in the email.

Email I receive looks like this

Hello fro !

You're receiving this e-mail because you or someone else has requested a password for your user account.
It can be safely ignored if you did not request a password reset. Click the link below to reset your password.



Thank you for using !

Am I missing something??

@mohmyo
Copy link
Contributor Author

mohmyo commented Oct 8, 2020

As password_reset_url parameter isn't passed to Django PasswordResetForm then I will assume that you have created your custom template, right?

@Abishek05
Copy link

I was able to do it similarly as you suggested.
I will share it here for anyone comes later

Create your custom password reset serializer

from dj_rest_auth.serializers import PasswordResetSerializer

class CustomPasswordResetSerializer(PasswordResetSerializer):
    def save(self):
        request = self.context.get('request')
        # Set some values to trigger the send_email method.
        opts = {
            'use_https': request.is_secure(),
            'from_email': 'example@yourdomain.com',
            'request': request,
            # here I have set my desired template to be used
            # don't forget to add your templates directory in settings to be found
            'email_template_name': 'password_reset_email.html'
        }

        opts.update(self.get_email_options())
        self.reset_form.save(**opts)

UPDATE:

If you only want to customize email parameters nothing more or less, you can ignore overriding save() method and override get_email_options() instead, check it is source code here.

Ex:

from dj_rest_auth.serializers import PasswordResetSerializer

class MyPasswordResetSerializer(PasswordResetSerializer):

    def get_email_options(self) :
      
        return {
            'email_template_name': 'password_reset_email.html'
        }

get_email_options() is being called in save() just to update opts dict with any other options before triggering reset_form, here in source code.

then point to your custom serializer in settings.py

REST_AUTH_SERIALIZERS = {
    'PASSWORD_RESET_SERIALIZER': 'path.to.your.CustomPasswordResetSerializer'
}

After some digging, I found that the password reset email template is coming from django/contrib/admin/templates/registration/password_reset_email.html.

Overwriting templates\registration\password_reset_email.html with your own custom content works fine without custom Serialization.

Although, to send the HTML content you will have to write a custom password reset serializer as @mohmyo suggested and replace this 'email_template_name': 'password_reset_email.html' with 'html_email_template_name': 'password_reset_email.html'.

@mohmyo Thanks for your Solution;)

@mohmyo
Copy link
Contributor Author

mohmyo commented Oct 8, 2020

You are welcome @Abishek05, glad it helped.

@lkbhitesh07
Copy link

lkbhitesh07 commented Jan 5, 2021

Hello @mohmyo and @Abishek05 , even after doing it with your approach I'm getting error which is
django_1 | [pid: 56|app: 0|req: 3/3] 172.18.0.1 () {42 vars in 696 bytes} [Tue Jan 5 20:09:35 2021] OPTIONS /api/auth/password/reset/ => generated 0 bytes in 0 msecs (HTTP/1.1 200) 7 headers in 365 bytes (1 switches on core 0) django_1 | [pid: 56|app: 0|req: 4/4] 172.18.0.1 () {42 vars in 696 bytes} [Tue Jan 5 20:09:35 2021] POST /api/auth/password/reset/ => generated 222495 bytes in 113 msecs (HTTP/1.1 500) 5 headers in 170 bytes (1 switches on core 0)

I'm contributing to an open-source org, Now let me tell you about the structure of the org, basically apps/accounts/templates/account/password_reset_email.html and serializer file apps/accounts/serializers.py where I defined my CustomPasswordResetSerializer

serializer.py
`class CustomPasswordResetSerializer(PasswordResetSerializer):

def get_email_options(self) :
  
    return {
        'html_email_template_name': 'account/password_reset_email.html'
    }`

settings.py file

REST_AUTH_SERIALIZERS = { "PASSWORD_RESET_SERIALIZER": "accounts.serializers.CustomPasswordResetSerializer" }

Will you please help me with what to do in this situation, Also if I need to do the complete process by overwriting how would I do that because here we have templates for each app so we cannot define the TEMPLATE_DIR in the settings file?

I've been stuck to this issue for the past 4-5 days, please help me out.
Thanks

@mohmyo
Copy link
Contributor Author

mohmyo commented Jan 9, 2021

Hello @lkbhitesh07, is there a better error trace than the one you posted? I'm not sure where to look, and there is a lot of things that can be the cause of the issue here.

So, let's start with a better error trace and the file structure of your project.

@lkbhitesh07
Copy link

lkbhitesh07 commented Jan 9, 2021

@mohmyo Thanks for replying also, I just solved this issue a few minutes back but I'm experiencing a very strange problem. I mean after customizing the PasswordResetSerializer, I made a folder templates/registration and put the templates inside, the strange part is, it's not getting recognized. I mean I have to define it like this -

class CustomPasswordResetSerializer(PasswordResetSerializer):
    def get_email_options(self):
        super().get_email_options()
        return {
            'subject_template_name': 'registration/password_reset_subject.txt',
            'email_template_name': 'registration/password_reset_email.txt',
        }

Otherwise, it's not getting recognized if I'll just do password_reset_subject.txt it will throw me an error that password_reset_subject.txt is not defined.
also one more interesting thing is that I have to put this file password_reset_email.txt as the txt extension, if I'll put it in html extension the variables will not pass.
Please do let me know if you need any more information
Thanks

@mohmyo
Copy link
Contributor Author

mohmyo commented Jan 9, 2021

There is no need to do super().get_email_options(), If you look into get_email_options() you will find that it is already empty and it got called later at save() to inject any additional options later on. In other words, get_email_options() has nothing to do with the flow unless it is being overridden.

So it should be like what I have posted earlier here

from dj_rest_auth.serializers import PasswordResetSerializer

class MyPasswordResetSerializer(PasswordResetSerializer):

    def get_email_options(self) :
      
        return {
            'email_template_name': 'password_reset_email.html'
        }

The issue for template folder not recognized may be related to some configuration to your project.
here an example of a project structure and the right config for it
project structure

|-app1
|-core
  |-settings
    |-base.py
|-templates
  |-password_reset_email.html

settings.py

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, '../', 'templates'), ], # note that ../ is here because our base.py is under two levels
        'APP_DIRS': True,
        'OPTIONS': {
            ........
        },
    },
]

@lkbhitesh07
Copy link

@mohmyo Just one more small help please, I mean we have a directory just like below so how should I add that? What should I put in 'DIRS' as I have templates folder according to different apps.

|-apps
 |-app1
  |-templates
    |-registration
 |-app2
  |-templates
|-core
|-settings
  |-common.py

Thanks for all your help.

@mohmyo
Copy link
Contributor Author

mohmyo commented Jan 9, 2021

I think this will help you a lot to pick the right DIRS for your project
https://docs.djangoproject.com/en/3.1/howto/overriding-templates/

@lkbhitesh07
Copy link

@mohmyo Yeah actually I already tried that out but didn't get it working that's why I asked you in case I was making some mistake. The error looks something like this -

django_1     | ERROR 2021-01-10 00:01:04,301 log 59 140635120949120 Internal Server Error: /api/auth/password/reset/
django_1     | Traceback (most recent call last):
django_1     |   File "/usr/local/lib/python3.7/site-packages/django/core/handlers/exception.py", line 34, in inner
django_1     |     response = get_response(request)
django_1     |   File "/usr/local/lib/python3.7/site-packages/django/core/handlers/base.py", line 115, in _get_response
django_1     |     response = self.process_exception_by_middleware(e, request)
django_1     |   File "/usr/local/lib/python3.7/site-packages/django/core/handlers/base.py", line 113, in _get_response
django_1     |     response = wrapped_callback(request, *callback_args, **callback_kwargs)
django_1     |   File "/usr/local/lib/python3.7/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
django_1     |     return view_func(*args, **kwargs)
django_1     |   File "/usr/local/lib/python3.7/site-packages/django/views/generic/base.py", line 71, in view
django_1     |     return self.dispatch(request, *args, **kwargs)
django_1     |   File "/usr/local/lib/python3.7/site-packages/rest_framework/views.py", line 505, in dispatch
django_1     |     response = self.handle_exception(exc)
django_1     |   File "/usr/local/lib/python3.7/site-packages/rest_framework/views.py", line 465, in handle_exception
django_1     |     self.raise_uncaught_exception(exc)
django_1     |   File "/usr/local/lib/python3.7/site-packages/rest_framework/views.py", line 476, in raise_uncaught_exception
django_1     |     raise exc
django_1     |   File "/usr/local/lib/python3.7/site-packages/rest_framework/views.py", line 502, in dispatch
django_1     |     response = handler(request, *args, **kwargs)
django_1     |   File "/usr/local/lib/python3.7/site-packages/dj_rest_auth/views.py", line 233, in post
django_1     |     serializer.save()
django_1     |   File "/usr/local/lib/python3.7/site-packages/dj_rest_auth/serializers.py", line 194, in save
django_1     |     self.reset_form.save(**opts)
django_1     |   File "/usr/local/lib/python3.7/site-packages/django/contrib/auth/forms.py", line 311, in save
django_1     |     user_email, html_email_template_name=html_email_template_name,
django_1     |   File "/usr/local/lib/python3.7/site-packages/django/contrib/auth/forms.py", line 252, in send_mail
django_1     |     body = loader.render_to_string(email_template_name, context)
django_1     |   File "/usr/local/lib/python3.7/site-packages/django/template/loader.py", line 61, in render_to_string
django_1     |     template = get_template(template_name, using=using)
django_1     |   File "/usr/local/lib/python3.7/site-packages/django/template/loader.py", line 19, in get_template
django_1     |     raise TemplateDoesNotExist(template_name, chain=chain)
django_1     | django.template.exceptions.TemplateDoesNotExist: password_reset_email.html

Thanks for your support.

@mohmyo
Copy link
Contributor Author

mohmyo commented Jan 10, 2021

can you share your TEMPLATES value?

@lkbhitesh07
Copy link

Here, this is the complete file, and it is in, BASE_DIR/apps/accounts/registration/password_reset_email.html.

{% load i18n %}{% autoescape off %}Hello from {{ site_name }}!

You're receiving this e-mail because you or someone else has requested a password for your user account at {{ site_domain }}.
It can be safely ignored if you did not request a password reset.
{% trans "Please go to the following page and choose a new password:" %}
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
{% endblock %}
{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }}

{% trans "Thanks for using our site!" %}

{% blocktrans %}The {{ site_name }} team{% endblocktrans %}

{% endautoescape %}

Thanks

@mohmyo
Copy link
Contributor Author

mohmyo commented Jan 10, 2021

I think you can try this

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, '../', 'apps/app1/templates'), ], 
        'APP_DIRS': True,
        'OPTIONS': {
            ........
        },
    },
]
def get_email_options(self) :
      
        return {
            'email_template_name': 'registration/password_reset_email.html'
        }

@AbhisarSaraswat
Copy link

Hi @mohmyo ,

I've a question that in PasswordResetView , Initially Link going on email is "http://127.0.0.1:8000/........". I want to change it to the other URL(i.e. url on which Frontend(Angular) is hosted on). How can I do it?

Thanks in advance.

@haccks
Copy link

haccks commented Oct 23, 2022

Just a heads-up: this works till version 2.1.4. Later versions will ignore get_email_options override. Checkout the reported bug.

@NemanjaNecke
Copy link

Here is how I got it running. These are custom serializers:

from django.conf import settings
from dj_rest_auth.forms import AllAuthPasswordResetForm
from django.utils.encoding import force_str
from allauth.account.forms import default_token_generator
from django.utils.http import urlsafe_base64_decode as uid_decoder
from django.contrib.auth.forms import PasswordResetForm


class PasswordResetSerializer(PasswordResetSerializer):
    @property
    def password_reset_form_class(self):
        use_custom_email_template = bool(self.get_email_options().get("html_email_template_name", ''))
        if 'allauth' in settings.INSTALLED_APPS and not use_custom_email_template:
            return AllAuthPasswordResetForm
        else:
            return PasswordResetForm
    def get_email_options(self):
        return {
            'html_email_template_name': 'account/email/password_reset_email.html',
        }

class CustomPasswordResetConfirmSerializer(PasswordResetConfirmSerializer):
    def validate(self, attrs):
        # Decode the uidb64 (allauth use base36) to uid to get User object
        try:
            uid = force_str(uid_decoder(attrs['uid']))
            self.user = Account.objects.get(pk=uid)
        except (TypeError, ValueError, OverflowError, ObjectDoesNotExist):
            raise ValidationError({'uid': ['Invalid value']})
        # Have to use allauth default token generator instead of django's default one
        if not default_token_generator.check_token(self.user, attrs['token']):
            raise ValidationError({'token': ['Invalid value']})

        self.custom_validation(attrs)
        # Construct SetPasswordForm instance
        self.set_password_form = self.set_password_form_class(
            user=self.user, data=attrs,
        )
        if not self.set_password_form.is_valid():
            raise serializers.ValidationError(self.set_password_form.errors)

        return attrs

You also have to set custom serializers in settings.py
I combined approaches from this answer #345
and this one #428

@Erwol
Copy link

Erwol commented Jan 13, 2024

For anyone attempting to do this with the following dependencies:

django-allauth = "==0.50.0"
dj-rest-auth = {extras = ["with_social"], version = "==4.0.0"}

None of the above answers fully worked for me as most options were deprecated after dj-rest-auth ~2.x. Instead, after some debugging, I found this solution:

Customising reset password URL

settings.py

REST_AUTH = {
    'PASSWORD_RESET_SERIALIZER': 'yourAuthApp.serializers.CustomPasswordResetSerializer'
}

yourAuthApp serializers

from dj_rest_auth.serializers import PasswordResetSerializer

from decouple import config

RESET_PASSWORD_REDIRECT_URL = config('RESET_PASSWORD_REDIRECT_URL')


def custom_url_generator(request, user, temp_key):
    return f'{RESET_PASSWORD_REDIRECT_URL}?token={temp_key}'


class CustomPasswordResetSerializer(PasswordResetSerializer):
    def get_email_options(self):
        return {
            'url_generator': custom_url_generator
        }

.env

# Reset password settings
RESET_PASSWORD_REDIRECT_URL=http://localhost:3000/auth/reset

You don't need to use environment variables, but I encourage doing so. The above code allows customising the URL that will be sent to your users while using the default email template provided by allauth. Not sure this is documented somewhere, but the official docs cannot find a definition of get_email_options: https://dj-rest-auth.readthedocs.io/en/4.0.1/search.html?q=get_email_option&check_keywords=yes&area=default.

Using a custom mail template

Two ways to achieve this:

Easy: overriding the original mail structure

On your project's root directory, create a templates > account > email directory. Now add a base_message.txt and base_message.txt files. You can take the structure from the official allauth templates directory: https://github.com/pennersr/django-allauth/tree/main/allauth/templates/account/email

And that's it; if you leave the {{ password_reset_url }} intact, its value should be taken from the custom_url_generator we defined before.

A bit more messy

You will have to override your CustomPasswordResetSerializer password_reset_form_class function to use a CustomAllAuthPasswordResetForm that should inherit from from dj_rest_auth.forms import AllAuthPasswordResetForm. You will then need to override the forms save method to update the following line:

            get_adapter(request).send_mail(
                'account/email/password_reset_key', email, context
            )

with the path to your custom template.

Neither method seems perfect to me but I went for the first as I find it more maintainable. If someone finds a reference to how to do this in a better way please let me know.

@Sinanaltundag
Copy link

Sinanaltundag commented Mar 5, 2024

I add uid
def custom_url_generator(self, request, user, temp_key):
return f'{RESET_PASSWORD_REDIRECT_URL}?uid={user.id}&token={temp_key}'

base_message.txt and password_reset_key_message.txt files can added for custom template (base_message.txt duplicated)
I can't manage override template files with first method because django loads app directory templates first. so I have to move custom templates in my custom app then I move app name in settings.py over "allauth" app. I did not try second method for change template

The answer above was the most suitable for me. (django 5 dj-rest-auth 5)

@kumawataryan
Copy link

Hey @Erwol, thank you so much! What you suggested really helped me out. I was stuck on this problem for a few days, but now I've got it sorted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests