iMerica / dj-rest-auth

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

Password reset email - no value for uid and token #302

Open anuj9196 opened 3 years ago

anuj9196 commented 3 years ago

I have a custom template to send password reset email with the following content

You’re attempting to reset the password of your {{APP_NAME}} account.
Please click on the button below to reset password:

https://{{ domain }}password_reset_confirm/{{ uid }}/{{ token }}

and using a custom serializer

class CustomPasswordResetSerializer(PasswordResetSerializer):
    def get_email_options(self):
        return {
            'subject_template_name': 'account/email/password_reset_key_subject.txt',
            'email_template_name': 'account/email/password_reset_key_message.txt',
        }

in settings.py

REST_AUTH_SERIALIZERS = {
    'PASSWORD_RESET_SERIALIZER': 'authentication.serializers.CustomPasswordResetSerializer'
}

But the value for uid and token as well domain is blank and it appears like below in the email

https://password_reset_confirm//
anuj9196 commented 3 years ago

The PasswordResetForm inside dj_rest_auth/forms.py, overrides the allauth save method

path = reverse(
    'password_reset_confirm',
    args=[user_pk_to_url_str(user), temp_key],
)
url = build_absolute_uri(request, path)

context = {
    'current_site': current_site,
    'user': user,
    'password_reset_url': url,
    'request': request,
}

password_reset_url in the context is using the API endpoint host, i.e., api.host.com, instead it should have the URL of the frontend application where the user will be landed to reset the password.

Also it does not exposes uid and token in the context, which can be used to generate custom URL.

SilvioMessi commented 2 years ago

Similar problem here. In latest version of the library the form AllAuthPasswordResetForm is used when allauth is include in the installed apps and it does not allow any customization since the **kwargs received in the save method are basically ignored. I think it would be great to have an easy way to override the form class or be able to inject via kwargs the email template name and the template context.

nibon commented 2 years ago

During a recent upgrade of both allauth and dj-rest-auth I had the same problem with non functional password reset emails. As other mentioned I wanted the url to link to the pwa client domain and not the api domain, and through the client and dj-rest-auth call the password-reset-confirm endpoint.

It was possible to override the serializer form and this is my complete solution that gave me back full control of the template, form and context. I've replaced the template path and default context names which is not necessary but because I have multiple client app domains using the same api, I've some extra steps that just makes the renaming more reasonable.

# serializers.py
from dj_rest_auth.serializers import PasswordResetSerializer
from myapp.models import ClientApp
class CustomPasswordResetSerializer(PasswordResetSerializer):
    @property
    def password_reset_form_class(self):
        return CustomResetPasswordForm

    def get_email_options(self):
        return {
            "email_template": "email/account/user_password_reset",
            "extra_email_context": {"client_app": ClientApp.from_request(self.context.get("request"))},
        }

# forms.py
from allauth.account.adapter import get_adapter
from allauth.account.forms import ResetPasswordForm
from allauth.account.utils import user_pk_to_url_str
class CustomResetPasswordForm(ResetPasswordForm):
    def save(self, request, **kwargs):
        email = self.cleaned_data['email']
        token_generator = kwargs.get('token_generator')
        template = kwargs.get("email_template")
        extra = kwargs.get("extra_email_context", {})
        client_app = extra["client_app"]
        for user in self.users:
            uid = user_pk_to_url_str(user)
            token = token_generator.make_token(user)
            reset_url = f"https://{client_app.domain}/{client_app.reset_path}/{uid}/{token}"
            context = {"user": user, "request": request, "email": email, "reset_url": reset_url}
            context.update(extra)
            with translation.override(user.locale):
                get_adapter(request).send_mail(template, email, context)
        return email

# templates/email/account/user_password_reset_(message.txt | message.html | subject.txt)
Hi from {{ client_app.name }}!
{{ reset_url }}
...

I've removed some generic steps so this is specialized for my situation. But just doing the context.update(kwargs.get("extra_email_context", {})) in the dj_rest_auth.forms.AllAuthPasswordResetForm save method would probably solve the general problem for this library. Just allowing the api host for password reset urls I feel is somewhat limiting and probably not the intention.