diff --git a/rest_auth/registration/serializers.py b/rest_auth/registration/serializers.py index 4f99c18..1d40dfe 100644 --- a/rest_auth/registration/serializers.py +++ b/rest_auth/registration/serializers.py @@ -215,3 +215,7 @@ class RegisterSerializer(serializers.Serializer): class VerifyEmailSerializer(serializers.Serializer): key = serializers.CharField() + + +class ResendConfirmationEmailSerializer(serializers.Serializer): + email = serializers.EmailField() diff --git a/rest_auth/registration/urls.py b/rest_auth/registration/urls.py index 1004695..4c7fae2 100644 --- a/rest_auth/registration/urls.py +++ b/rest_auth/registration/urls.py @@ -1,10 +1,18 @@ from django.views.generic import TemplateView from django.conf.urls import url -from .views import RegisterView, VerifyEmailView +from .views import ( + RegisterView, + VerifyEmailView, + ResendConfirmationEmailView, +) + urlpatterns = [ url(r'^$', RegisterView.as_view(), name='rest_register'), + url(r'^resend-confirmation-email/$', + ResendConfirmationEmailView.as_view(), + name='rest_resend_confirmation_email'), url(r'^verify-email/$', VerifyEmailView.as_view(), name='rest_verify_email'), # This url is used by django-allauth and empty TemplateView is diff --git a/rest_auth/registration/views.py b/rest_auth/registration/views.py index 0e0ab0d..9e49403 100644 --- a/rest_auth/registration/views.py +++ b/rest_auth/registration/views.py @@ -15,6 +15,7 @@ from allauth.account.adapter import get_adapter from allauth.account.views import ConfirmEmailView from allauth.account.utils import complete_signup from allauth.account import app_settings as allauth_settings +from allauth.account.models import EmailAddress from allauth.socialaccount import signals from allauth.socialaccount.adapter import get_adapter as get_social_adapter from allauth.socialaccount.models import SocialAccount @@ -23,10 +24,13 @@ from rest_auth.app_settings import (TokenSerializer, JWTSerializer, create_token) from rest_auth.models import TokenModel -from rest_auth.registration.serializers import (VerifyEmailSerializer, - SocialLoginSerializer, - SocialAccountSerializer, - SocialConnectSerializer) +from rest_auth.registration.serializers import ( + VerifyEmailSerializer, + SocialLoginSerializer, + SocialAccountSerializer, + SocialConnectSerializer, + ResendConfirmationEmailSerializer, +) from rest_auth.utils import jwt_encode from rest_auth.views import LoginView from .app_settings import RegisterSerializer, register_permission_classes @@ -184,3 +188,22 @@ class SocialAccountDisconnectView(GenericAPIView): ) return Response(self.get_serializer(account).data) + + +class ResendConfirmationEmailView(GenericAPIView): + serializer_class = ResendConfirmationEmailSerializer + permission_classes = (AllowAny,) + allowed_methods = ('POST', 'OPTIONS', 'HEAD') + + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + email = serializer.validated_data.get('email') + + try: + email_address = EmailAddress.objects.get(email__exact=email, verified=False) + email_address.send_confirmation(self.request, True) + except EmailAddress.DoesNotExist: + pass + + return Response({'detail': _('Verification e-mail sent.')}) diff --git a/rest_auth/tests/test_api.py b/rest_auth/tests/test_api.py index 9c5fd9e..16e2616 100644 --- a/rest_auth/tests/test_api.py +++ b/rest_auth/tests/test_api.py @@ -5,6 +5,7 @@ from django.conf import settings from django.utils.encoding import force_text from allauth.account import app_settings as account_app_settings +from allauth.account.models import EmailAddress from rest_framework import status from rest_framework.test import APIRequestFactory @@ -516,3 +517,66 @@ class APIBasicTests(TestsMixin, TestCase): self.post(self.login_url, data=payload, status_code=status.HTTP_200_OK) self.get(self.logout_url, status_code=status.HTTP_405_METHOD_NOT_ALLOWED) + + @override_settings(ACCOUNT_EMAIL_VERIFICATION='mandatory') + def test_resend_account_verification_email(self): + self.post( + self.register_url, + data=self.REGISTRATION_DATA_WITH_EMAIL, + status_code=status.HTTP_201_CREATED + ) + + self.assertEqual(EmailAddress.objects.count(), 1) + self.assertEqual(EmailAddress.objects.first().email, self.EMAIL) + self.assertEqual(EmailAddress.objects.first().verified, False) + + self.post( + reverse('rest_resend_confirmation_email'), + data={ + 'email': self.EMAIL, + }, + status_code=status.HTTP_200_OK + ) + + self.assertEqual(len(mail.outbox), 2) + + @override_settings(ACCOUNT_EMAIL_VERIFICATION='mandatory') + def test_resend_not_registered_account_verification_email(self): + self.assertEqual(EmailAddress.objects.count(), 0) + + self.post( + reverse('rest_resend_confirmation_email'), + data={ + 'email': self.EMAIL, + }, + status_code=status.HTTP_200_OK + ) + + self.assertEqual(len(mail.outbox), 0) + + @override_settings(ACCOUNT_EMAIL_VERIFICATION='mandatory') + def test_resend_already_verified_account_verification_email(self): + self.post( + self.register_url, + data=self.REGISTRATION_DATA_WITH_EMAIL, + status_code=status.HTTP_201_CREATED + ) + + self.assertEqual(EmailAddress.objects.count(), 1) + self.assertEqual(EmailAddress.objects.first().email, self.EMAIL) + self.assertEqual(EmailAddress.objects.first().verified, False) + self.assertEqual(len(mail.outbox), 1) + + email_address = EmailAddress.objects.first() + email_address.verified = True + email_address.save() + + self.post( + reverse('rest_resend_confirmation_email'), + data={ + 'email': self.EMAIL + }, + status_code=status.HTTP_200_OK + ) + + self.assertEqual(len(mail.outbox), 1)