Solución:
Afortunadamente, Encontré una bonita biblioteca que me facilitó la vida hoy:
https://github.com/anx-ckreuzberger/django-rest-passwordreset
pip install django-rest-passwordreset
Lo tengo funcionando así:
- Seguí las instrucciones en su sitio web.
Mi accounts/urls.py
ahora tiene las siguientes rutas:
# 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")
]
Luego también agregué un pequeño TokenSerializer para mi CustomTokenVerification:
# project/accounts/serializers.py
from rest_framework import serializers
class CustomTokenSerializer(serializers.Serializer):
token = serializers.CharField()
Luego agregué un receptor de señal en el derivado anterior CustomPasswordResetView
, que ahora ya no se deriva de rest_auth.views.PasswordResetView
Y agregó una nueva vista 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:
"[email protected]{}".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'})
Ahora mi interfaz proporcionará una opción para solicitar el enlace pw-reset, por lo que la interfaz enviará una solicitud de publicación a django como esta:
// 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 */ )
Y el correo electrónico creado contendrá un enlace como este:
Click the link below to reset your password.
localhost:8000/password-reset/4873759c229f17a94546a63eb7c3d482e73983495fa40c7ec2a3d9ca1adcf017
… que no está definido en django-urls por intencion!
Django permitirá que todas las URL desconocidas pasen y el enrutador vue decidirá si la URL tiene sentido o no. Luego dejo que la interfaz envíe el token para ver si es válido, para que el usuario ya pueda ver si el token ya está usado, vencido o lo que sea …
// 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 */ )
Ahora el usuario recibirá un mensaje de error a través de Vue, o campos de entrada de contraseña, donde finalmente puede restablecer la contraseña, que será enviada por la interfaz de la siguiente manera:
// 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 */ )
Este es el código principal. Usé rutas vue personalizadas para desacoplar los puntos finales de descanso de django de las rutas visibles de la interfaz. El resto se hace con las solicitudes de API y el manejo de sus respuestas.
Espero que esto ayude a cualquiera que tenga luchas como yo en el futuro.