Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: resend verification #332

Merged
merged 1 commit into from
Nov 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions demo/demo/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
url(r'^password-change/$',
TemplateView.as_view(template_name="password_change.html"),
name='password-change'),
url(r'^resend-email-verification/$',
TemplateView.as_view(template_name="resend_email_verification.html"),
name='resend-email-verification'),


# this url is used to generate email content
Expand Down
1 change: 1 addition & 0 deletions demo/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<!-- these pages don't require user token -->
<li><a href="{% url 'signup' %}">Signup</a></li>
<li><a href="{% url 'email-verification' %}">E-mail verification</a></li>
<li><a href="{% url 'resend-email-verification' %}">Resend E-mail verification</a></li>
<li><a href="{% url 'login' %}">Login</a></li>
<li><a href="{% url 'password-reset' %}">Password Reset</a></li>
<li><a href="{% url 'password-reset-confirm' %}">Password Reset Confirm</a></li>
Expand Down
16 changes: 16 additions & 0 deletions demo/templates/fragments/resend_email_verification_form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!-- Signup form -->
<form class="form-horizontal ajax-post" role="form" action="{% url 'rest_resend_email' %}">{% csrf_token %}
<div class="form-group">
<label for="email" class="col-sm-2 control-label">E-mail</label>
<div class="col-sm-10">
<input name="email" type="text" class="form-control" id="email" placeholder="Email">
</div>
</div>

<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-default">Resend</button>
</div>
</div>
<div class="form-group api-response"></div>
</form>
8 changes: 8 additions & 0 deletions demo/templates/resend_email_verification.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{% extends "base.html" %}

{% block content %}
<div class="row">
<h3>Resend E-mail verification</h3><hr/>
{% include "fragments/resend_email_verification_form.html" %}
</div>
{% endblock %}
12 changes: 4 additions & 8 deletions dj_rest_auth/registration/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,23 +112,19 @@ def post(self, request, *args, **kwargs):
class ResendEmailVerificationView(CreateAPIView):
permission_classes = (AllowAny,)
serializer_class = ResendEmailVerificationSerializer
queryset = EmailAddress.objects.all()

def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)

email = EmailAddress.objects.get(**serializer.validated_data)
if not email:
raise ValidationError("Account does not exist")
email = EmailAddress.objects.filter(**serializer.validated_data).first()
Copy link

@merwok merwok Feb 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line could use self.queryset.filter(...) or better self.get_queryset(), so that if a subclass changes the queryset attribute or the get_queryset method, that wouldn’t be ignored.

if email and not email.verified:
email.send_confirmation(request)

if email.verified:
raise ValidationError("Account is already verified")

email.send_confirmation()
return Response({'detail': _('ok')}, status=status.HTTP_200_OK)



class SocialLoginView(LoginView):
"""
class used for social authentications
Expand Down
74 changes: 44 additions & 30 deletions dj_rest_auth/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from dj_rest_auth.models import get_token_model
from .mixins import CustomPermissionClass, TestsMixin


try:
from django.urls import reverse
except ImportError: # pragma: no cover
Expand Down Expand Up @@ -513,17 +512,21 @@ def test_registration_with_email_verification(self):
data=self.REGISTRATION_DATA_WITH_EMAIL,
status_code=status.HTTP_201_CREATED,
)

self.assertNotIn('key', result.data)
self.assertEqual(get_user_model().objects.all().count(), user_count + 1)
self.assertEqual(len(mail.outbox), mail_count + 1)
new_user = get_user_model().objects.latest('id')
self.assertEqual(new_user.username, self.REGISTRATION_DATA['username'])

# increment count
mail_count += 1

# test browsable endpoint
result = self.get(
self.verify_email_url,
status_code=status.HTTP_405_METHOD_NOT_ALLOWED
)
self.assertEqual(result.status_code, 405)
self.assertEqual(result.json['detail'], 'Method "GET" not allowed.')

# email is not verified yet
Expand All @@ -543,6 +546,8 @@ def test_registration_with_email_verification(self):
data={'email': self.EMAIL},
status_code=status.HTTP_200_OK
)
# check mail count
self.assertEqual(len(mail.outbox), mail_count + 1)

# verify email
email_confirmation = new_user.emailaddress_set.get(email=self.EMAIL) \
Expand All @@ -557,6 +562,21 @@ def test_registration_with_email_verification(self):
self._login()
self._logout()

def test_should_not_resend_email_verification_for_nonexistent_email(self):
# mail count before resend
mail_count = len(mail.outbox)

# resend non-existent email
resend_email_result = self.post(
self.resend_email_url,
data={'email': '[email protected]'},
status_code=status.HTTP_200_OK
)

self.assertEqual(resend_email_result.status_code, status.HTTP_200_OK)
# verify that mail count did not increment
self.assertEqual(mail_count, len(mail.outbox))

@override_settings(ACCOUNT_LOGOUT_ON_GET=True)
def test_logout_on_get(self):
payload = {
Expand Down Expand Up @@ -709,7 +729,6 @@ def test_custom_jwt_claims(self):
self.assertEquals(claims['name'], 'person')
self.assertEquals(claims['email'], '[email protected]')


@override_settings(REST_USE_JWT=True)
@override_settings(JWT_AUTH_COOKIE='jwt-auth')
@override_settings(
Expand Down Expand Up @@ -741,7 +760,6 @@ def test_custom_jwt_claims_cookie_w_authentication(self):
resp = self.get('/protected-view/')
self.assertEquals(resp.status_code, 200)


@override_settings(REST_USE_JWT=True)
@override_settings(JWT_AUTH_COOKIE='jwt-auth')
@override_settings(JWT_AUTH_COOKIE_USE_CSRF=False)
Expand All @@ -754,8 +772,8 @@ def test_custom_jwt_claims_cookie_w_authentication(self):
),
)
@override_settings(REST_SESSION_LOGIN=False)
@override_settings(CSRF_COOKIE_SECURE =True)
@override_settings(CSRF_COOKIE_HTTPONLY =True)
@override_settings(CSRF_COOKIE_SECURE=True)
@override_settings(CSRF_COOKIE_HTTPONLY=True)
def test_wo_csrf_enforcement(self):
from .mixins import APIClient
payload = {
Expand All @@ -772,9 +790,9 @@ def test_wo_csrf_enforcement(self):
## TEST WITH JWT AUTH HEADER
jwtclient = APIClient(enforce_csrf_checks=True)
token = resp.data['access_token']
resp = jwtclient.get('/protected-view/', HTTP_AUTHORIZATION='Bearer '+token)
resp = jwtclient.get('/protected-view/', HTTP_AUTHORIZATION='Bearer ' + token)
self.assertEquals(resp.status_code, 200)
resp = jwtclient.post('/protected-view/', {}, HTTP_AUTHORIZATION='Bearer '+token)
resp = jwtclient.post('/protected-view/', {}, HTTP_AUTHORIZATION='Bearer ' + token)
self.assertEquals(resp.status_code, 200)

## TEST WITH COOKIES
Expand All @@ -784,7 +802,6 @@ def test_wo_csrf_enforcement(self):
resp = client.post('/protected-view/', {})
self.assertEquals(resp.status_code, 200)


@override_settings(REST_USE_JWT=True)
@override_settings(JWT_AUTH_COOKIE='jwt-auth')
@override_settings(JWT_AUTH_COOKIE_USE_CSRF=True)
Expand All @@ -797,8 +814,8 @@ def test_wo_csrf_enforcement(self):
),
)
@override_settings(REST_SESSION_LOGIN=False)
@override_settings(CSRF_COOKIE_SECURE =True)
@override_settings(CSRF_COOKIE_HTTPONLY =True)
@override_settings(CSRF_COOKIE_SECURE=True)
@override_settings(CSRF_COOKIE_HTTPONLY=True)
def test_csrf_wo_login_csrf_enforcement(self):
from .mixins import APIClient
payload = {
Expand All @@ -821,29 +838,28 @@ def test_csrf_wo_login_csrf_enforcement(self):
token = resp.data['access_token']
resp = jwtclient.get('/protected-view/')
self.assertEquals(resp.status_code, 403)
resp = jwtclient.get('/protected-view/', HTTP_AUTHORIZATION='Bearer '+token)
resp = jwtclient.get('/protected-view/', HTTP_AUTHORIZATION='Bearer ' + token)
self.assertEquals(resp.status_code, 200)
resp = jwtclient.post('/protected-view/', {})
self.assertEquals(resp.status_code, 403)
resp = jwtclient.post('/protected-view/', {}, HTTP_AUTHORIZATION='Bearer '+token)
resp = jwtclient.post('/protected-view/', {}, HTTP_AUTHORIZATION='Bearer ' + token)
self.assertEquals(resp.status_code, 200)

## TEST WITH COOKIES
# TEST WITH COOKIES
resp = client.get('/protected-view/')
self.assertEquals(resp.status_code, 200)
#fail w/o csrftoken in payload
# fail w/o csrftoken in payload
resp = client.post('/protected-view/', {})
self.assertEquals(resp.status_code, 403)

csrfparam = {'csrfmiddlewaretoken': csrftoken}
resp = client.post('/protected-view/', csrfparam)
self.assertEquals(resp.status_code, 200)


@override_settings(REST_USE_JWT=True)
@override_settings(JWT_AUTH_COOKIE='jwt-auth')
@override_settings(JWT_AUTH_COOKIE_USE_CSRF=True)
@override_settings(JWT_AUTH_COOKIE_ENFORCE_CSRF_ON_UNAUTHENTICATED=True) #True at your own risk
@override_settings(JWT_AUTH_COOKIE_ENFORCE_CSRF_ON_UNAUTHENTICATED=True) # True at your own risk
@override_settings(
REST_FRAMEWORK=dict(
DEFAULT_AUTHENTICATION_CLASSES=[
Expand All @@ -852,8 +868,8 @@ def test_csrf_wo_login_csrf_enforcement(self):
),
)
@override_settings(REST_SESSION_LOGIN=False)
@override_settings(CSRF_COOKIE_SECURE =True)
@override_settings(CSRF_COOKIE_HTTPONLY =True)
@override_settings(CSRF_COOKIE_SECURE=True)
@override_settings(CSRF_COOKIE_HTTPONLY=True)
def test_csrf_w_login_csrf_enforcement(self):
from .mixins import APIClient
payload = {
Expand All @@ -866,7 +882,7 @@ def test_csrf_w_login_csrf_enforcement(self):
client.get(reverse('getcsrf'))
csrftoken = client.cookies['csrftoken'].value

#fail w/o csrftoken in payload
# fail w/o csrftoken in payload
resp = client.post(self.login_url, payload)
self.assertEquals(resp.status_code, 403)

Expand All @@ -881,19 +897,18 @@ def test_csrf_w_login_csrf_enforcement(self):
## TEST WITH COOKIES
resp = client.get('/protected-view/')
self.assertEquals(resp.status_code, 200)
#fail w/o csrftoken in payload
# fail w/o csrftoken in payload
resp = client.post('/protected-view/', {})
self.assertEquals(resp.status_code, 403)

csrfparam = {'csrfmiddlewaretoken': csrftoken}
resp = client.post('/protected-view/', csrfparam)
self.assertEquals(resp.status_code, 200)


@override_settings(REST_USE_JWT=True)
@override_settings(JWT_AUTH_COOKIE='jwt-auth')
@override_settings(JWT_AUTH_COOKIE_USE_CSRF=False)
@override_settings(JWT_AUTH_COOKIE_ENFORCE_CSRF_ON_UNAUTHENTICATED=True) #True at your own risk
@override_settings(JWT_AUTH_COOKIE_ENFORCE_CSRF_ON_UNAUTHENTICATED=True) # True at your own risk
@override_settings(
REST_FRAMEWORK=dict(
DEFAULT_AUTHENTICATION_CLASSES=[
Expand All @@ -902,8 +917,8 @@ def test_csrf_w_login_csrf_enforcement(self):
),
)
@override_settings(REST_SESSION_LOGIN=False)
@override_settings(CSRF_COOKIE_SECURE =True)
@override_settings(CSRF_COOKIE_HTTPONLY =True)
@override_settings(CSRF_COOKIE_SECURE=True)
@override_settings(CSRF_COOKIE_HTTPONLY=True)
def test_csrf_w_login_csrf_enforcement_2(self):
from .mixins import APIClient
payload = {
Expand All @@ -916,7 +931,7 @@ def test_csrf_w_login_csrf_enforcement_2(self):
client.get(reverse('getcsrf'))
csrftoken = client.cookies['csrftoken'].value

#fail w/o csrftoken in payload
# fail w/o csrftoken in payload
resp = client.post(self.login_url, payload)
self.assertEquals(resp.status_code, 403)

Expand All @@ -926,12 +941,12 @@ def test_csrf_w_login_csrf_enforcement_2(self):
self.assertTrue('csrftoken' in list(client.cookies.keys()))
self.assertEquals(resp.status_code, 200)

## TEST WITH JWT AUTH HEADER does not make sense
# TEST WITH JWT AUTH HEADER does not make sense

## TEST WITH COOKIES
# TEST WITH COOKIES
resp = client.get('/protected-view/')
self.assertEquals(resp.status_code, 200)
#fail w/o csrftoken in payload
# fail w/o csrftoken in payload
resp = client.post('/protected-view/', {})
self.assertEquals(resp.status_code, 403)

Expand Down Expand Up @@ -1018,7 +1033,6 @@ def test_rest_session_login_sets_session_cookie(self):
resp = self.post(self.login_url, data=payload, status_code=200)
self.assertTrue(settings.SESSION_COOKIE_NAME in resp.cookies.keys())


@modify_settings(INSTALLED_APPS={'remove': ['rest_framework.authtoken']})
def test_misconfigured_token_model(self):
# default token model, but authtoken app not installed raises error
Expand Down