Django REST-Auth Password Reset
Luckily, I found a nice library which made my life so easy today:
https://github.com/anx-ckreuzberger/django-rest-passwordreset
pip install django-rest-passwordreset
Got it working like this:
- Followed instructions on their website.
My accounts/urls.py
now has the following paths:
# project/accounts/urls.py
from django.urls import path, include
from . import views as acc_views
app_name = 'accounts'
urlpatterns = [
path('', acc_views.UserListView.as_view(), name='user-list'),
path('login/', acc_views.UserLoginView.as_view(), name='login'),
path('logout/', acc_views.UserLogoutView.as_view(), name='logout'),
path('register/', acc_views.CustomRegisterView.as_view(), name='register'),
# NEW: custom verify-token view which is not included in django-rest-passwordreset
path('reset-password/verify-token/', acc_views.CustomPasswordTokenVerificationView.as_view(), name='password_reset_verify_token'),
# NEW: The django-rest-passwordreset urls to request a token and confirm pw-reset
path('reset-password/', include('django_rest_passwordreset.urls', namespace='password_reset')),
path('<int:pk>/', acc_views.UserDetailView.as_view(), name='user-detail')
]
Then I also added a little TokenSerializer for my CustomTokenVerification:
# project/accounts/serializers.py
from rest_framework import serializers
class CustomTokenSerializer(serializers.Serializer):
token = serializers.CharField()
Then I added a Signal Receiver in the previous derived CustomPasswordResetView
, which now is no longer derived from rest_auth.views.PasswordResetView
AND added a new view CustomPasswordTokenVerificationView
:
# project/accounts/views.py
from django.dispatch import receiver
from django_rest_passwordreset.signals import reset_password_token_created
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from vuedj.constants import site_url, site_full_name, site_shortcut_name
from rest_framework.views import APIView
from rest_framework import parsers, renderers, status
from rest_framework.response import Response
from .serializers import CustomTokenSerializer
from django_rest_passwordreset.models import ResetPasswordToken
from django_rest_passwordreset.views import get_password_reset_token_expiry_time
from django.utils import timezone
from datetime import timedelta
class CustomPasswordResetView:
@receiver(reset_password_token_created)
def password_reset_token_created(sender, reset_password_token, *args, **kwargs):
"""
Handles password reset tokens
When a token is created, an e-mail needs to be sent to the user
"""
# send an e-mail to the user
context = {
'current_user': reset_password_token.user,
'username': reset_password_token.user.username,
'email': reset_password_token.user.email,
'reset_password_url': "{}/password-reset/{}".format(site_url, reset_password_token.key),
'site_name': site_shortcut_name,
'site_domain': site_url
}
# render email text
email_html_message = render_to_string('email/user_reset_password.html', context)
email_plaintext_message = render_to_string('email/user_reset_password.txt', context)
msg = EmailMultiAlternatives(
# title:
"Password Reset for {}".format(site_full_name),
# message:
email_plaintext_message,
# from:
"noreply@{}".format(site_url),
# to:
[reset_password_token.user.email]
)
msg.attach_alternative(email_html_message, "text/html")
msg.send()
class CustomPasswordTokenVerificationView(APIView):
"""
An Api View which provides a method to verifiy that a given pw-reset token is valid before actually confirming the
reset.
"""
throttle_classes = ()
permission_classes = ()
parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
renderer_classes = (renderers.JSONRenderer,)
serializer_class = CustomTokenSerializer
def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
token = serializer.validated_data['token']
# get token validation time
password_reset_token_validation_time = get_password_reset_token_expiry_time()
# find token
reset_password_token = ResetPasswordToken.objects.filter(key=token).first()
if reset_password_token is None:
return Response({'status': 'invalid'}, status=status.HTTP_404_NOT_FOUND)
# check expiry date
expiry_date = reset_password_token.created_at + timedelta(hours=password_reset_token_validation_time)
if timezone.now() > expiry_date:
# delete expired token
reset_password_token.delete()
return Response({'status': 'expired'}, status=status.HTTP_404_NOT_FOUND)
# check if user has password to change
if not reset_password_token.user.has_usable_password():
return Response({'status': 'irrelevant'})
return Response({'status': 'OK'})
Now my frontend will provide an option to request the pw-reset link, so the frontend will send a post request to django like this:
// urls.js
const SERVER_URL = 'http://localhost:8000/' // FIXME: change at production (https and correct IP and port)
const API_URL = 'api/v1/'
const API_AUTH = 'auth/'
API_AUTH_PASSWORD_RESET = API_AUTH + 'reset-password/'
// api.js
import axios from 'axios'
import urls from './urls'
axios.defaults.baseURL = urls.SERVER_URL + urls.API_URL
axios.defaults.headers.post['Content-Type'] = 'application/json'
axios.defaults.xsrfHeaderName = 'X-CSRFToken'
axios.defaults.xsrfCookieName = 'csrftoken'
const api = {
get,
post,
patch,
put,
head,
delete: _delete
}
function post (url, request) {
return axios.post(url, request)
.then((response) => Promise.resolve(response))
.catch((error) => Promise.reject(error))
}
// user.service.js
import api from '@/_api/api'
import urls from '@/_api/urls'
api.post(`${urls.API_AUTH_PASSWORD_RESET}`, email)
.then( /* handle success */ )
.catch( /* handle error */ )
And the created email will contain a link like this:
Click the link below to reset your password.
localhost:8000/password-reset/4873759c229f17a94546a63eb7c3d482e73983495fa40c7ec2a3d9ca1adcf017
... which is not defined in the django-urls by intention! Django will let every unknown url pass through and the vue router will decide if the url makes sense or not. Then I let the frontend send the token to see if it is valid, so the user can already see if the token is already used, expired, or whatever...
// urls.js
const API_AUTH_PASSWORD_RESET_VERIFY_TOKEN = API_AUTH + 'reset-password/verify-token/'
// users.service.js
api.post(`${urls.API_AUTH_PASSWORD_RESET_VERIFY_TOKEN}`, pwResetToken)
.then( /* handle success */ )
.catch( /* handle error */ )
Now the user will get an error message through Vue, or password-input fields, where they can finally reset the password, which will be sent by the frontend like this:
// urls.js
const API_AUTH_PASSWORD_RESET_CONFIRM = API_AUTH + 'reset-password/confirm/'
// users.service.js
api.post(`${urls.API_AUTH_PASSWORD_RESET_CONFIRM}`, {
token: state[token], // (vuex state)
password: state[password] // (vuex state)
})
.then( /* handle success */ )
.catch( /* handle error */ )
This is the main code. I used custom vue routes to decouple the django rest-endpoints from the frontend visible routes. The rest is done with api requests and handling their responses.
Hope this helps anybody who will have struggles like me in the future.