Saltar al contenido

Restablecimiento de contraseña de Django REST-Auth

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í:

  1. 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.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *