From 10ae7acac924258b073f7fbf47b342b1436c93bc Mon Sep 17 00:00:00 2001 From: Roman Gorbil Date: Tue, 24 Nov 2015 17:11:46 +0700 Subject: [PATCH 01/23] Rewrite registration logic --- docs/api_endpoints.rst | 13 +--- docs/configuration.rst | 8 +++ rest_auth/registration/app_settings.py | 11 ++++ rest_auth/registration/serializers.py | 50 ++++++++++++++ rest_auth/registration/views.py | 90 ++++++++------------------ 5 files changed, 98 insertions(+), 74 deletions(-) create mode 100644 rest_auth/registration/app_settings.py diff --git a/docs/api_endpoints.rst b/docs/api_endpoints.rst index 48e475e..86a30c3 100644 --- a/docs/api_endpoints.rst +++ b/docs/api_endpoints.rst @@ -51,20 +51,9 @@ Registration - /rest-auth/registration/ (POST) - username - - password1 - - password2 + - password - email - .. note:: This endpoint is based on ``allauth.account.views.SignupView`` and uses the same form as in this view. To override fields you have to create custom Signup Form and define it in django settings: - - .. code-block:: python - - ACCOUNT_FORMS = { - 'signup': 'path.to.custom.SignupForm' - } - - See allauth documentation for more details. - - /rest-auth/registration/verify-email/ (POST) - key diff --git a/docs/configuration.rst b/docs/configuration.rst index 282f326..079c710 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -29,6 +29,14 @@ Configuration ... } +- **REST_AUTH_REGISTRATION_SERIALIZERS** + + You can define your custom serializers for registration endpoint. + Possible key values: + + - REGISTER_SERIALIZER - serializer class in ``rest_auth.register.views.RegisterView``, default value ``rest_auth.register.serializers.RegisterSerializer`` + + - **REST_SESSION_LOGIN** - Enable session login in Login API view (default: True) diff --git a/rest_auth/registration/app_settings.py b/rest_auth/registration/app_settings.py new file mode 100644 index 0000000..227b45b --- /dev/null +++ b/rest_auth/registration/app_settings.py @@ -0,0 +1,11 @@ +from django.conf import settings + +from rest_auth.registration.serializers import ( + RegisterSerializer as DefaultRegisterSerializer) +from ..utils import import_callable + + +serializers = getattr(settings, 'REST_AUTH_REGISTER_SERIALIZERS', {}) + +RegisterSerializer = import_callable( + serializers.get('REGISTER_SERIALIZER', DefaultRegisterSerializer)) diff --git a/rest_auth/registration/serializers.py b/rest_auth/registration/serializers.py index 5f5efd6..68ba702 100644 --- a/rest_auth/registration/serializers.py +++ b/rest_auth/registration/serializers.py @@ -1,6 +1,15 @@ from django.http import HttpRequest from django.conf import settings +try: + from allauth.account import app_settings as allauth_settings + from allauth.utils import (email_address_exists, + get_username_max_length) + from allauth.account.adapter import get_adapter + from allauth.account.utils import setup_user_email +except ImportError: + raise ImportError('allauth needs to be added to INSTALLED_APPS.') + from rest_framework import serializers from requests.exceptions import HTTPError # Import is needed only if we are using social login, in which @@ -109,3 +118,44 @@ class SocialLoginSerializer(serializers.Serializer): attrs['user'] = login.account.user return attrs + + +class RegisterSerializer(serializers.Serializer): + username = serializers.CharField( + max_length=get_username_max_length(), + min_length=allauth_settings.USERNAME_MIN_LENGTH, + required=allauth_settings.USERNAME_REQUIRED) + email = serializers.EmailField(required=allauth_settings.EMAIL_REQUIRED) + password = serializers.CharField(required=True, write_only=True) + + def validate_username(self, username): + username = get_adapter().clean_username(username) + return username + + def validate_email(self, email): + email = get_adapter().clean_email(email) + if allauth_settings.UNIQUE_EMAIL: + if email and email_address_exists(email): + raise serializers.ValidationError( + "A user is already registered with this e-mail address.") + return email + + def validate_password(self, password): + return get_adapter().clean_password(password) + + def custom_signup(self, request, user): + pass + + def save(self, request): + adapter = get_adapter() + user = adapter.new_user(request) + self.cleaned_data = self.validated_data + self.cleaned_data['password1'] = self.cleaned_data['password'] + adapter.save_user(request, user, self) + self.custom_signup(request, user) + setup_user_email(request, user, []) + return user + + +class VerifyEmailSerializer(serializers.Serializer): + key = serializers.CharField() diff --git a/rest_auth/registration/views.py b/rest_auth/registration/views.py index e700706..f7113f2 100644 --- a/rest_auth/registration/views.py +++ b/rest_auth/registration/views.py @@ -1,77 +1,41 @@ -from django.http import HttpRequest from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import AllowAny +from rest_framework.generics import CreateAPIView from rest_framework import status from rest_framework.authtoken.models import Token +from rest_framework.exceptions import MethodNotAllowed -from allauth.account.views import SignupView, ConfirmEmailView +from allauth.account.views import ConfirmEmailView from allauth.account.utils import complete_signup -from allauth.account import app_settings +from allauth.account import app_settings as allauth_settings from rest_auth.app_settings import TokenSerializer -from rest_auth.registration.serializers import SocialLoginSerializer +from rest_auth.registration.serializers import (SocialLoginSerializer, + VerifyEmailSerializer) +from .app_settings import RegisterSerializer from rest_auth.views import LoginView -class RegisterView(APIView, SignupView): - """ - Accepts the credentials and creates a new user - if user does not exist already - Return the REST Token if the credentials are valid and authenticated. - Calls allauth complete_signup method +class RegisterView(CreateAPIView): + serializer_class = RegisterSerializer - Accept the following POST parameters: username, email, password - Return the REST Framework Token Object's key. - """ + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response(TokenSerializer(user.auth_token).data, + status=status.HTTP_201_CREATED, + headers=headers) - permission_classes = (AllowAny,) - allowed_methods = ('POST', 'OPTIONS', 'HEAD') - token_model = Token - serializer_class = TokenSerializer - - def get(self, *args, **kwargs): - return Response({}, status=status.HTTP_405_METHOD_NOT_ALLOWED) - - def put(self, *args, **kwargs): - return Response({}, status=status.HTTP_405_METHOD_NOT_ALLOWED) - - def form_valid(self, form): - self.user = form.save(self.request) - self.token, created = self.token_model.objects.get_or_create( - user=self.user - ) - if isinstance(self.request, HttpRequest): - request = self.request - else: - request = self.request._request - return complete_signup(request, self.user, - app_settings.EMAIL_VERIFICATION, - self.get_success_url()) - - def get_form_kwargs(self, *args, **kwargs): - kwargs = super(RegisterView, self).get_form_kwargs(*args, **kwargs) - kwargs['data'] = self.request.data - return kwargs - - def post(self, request, *args, **kwargs): - self.initial = {} - form_class = self.get_form_class() - self.form = self.get_form(form_class) - if self.form.is_valid(): - self.form_valid(self.form) - return self.get_response() - else: - return self.get_response_with_errors() - - def get_response(self): - # serializer = self.user_serializer_class(instance=self.user) - serializer = self.serializer_class(instance=self.token, - context={'request': self.request}) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - def get_response_with_errors(self): - return Response(self.form.errors, status=status.HTTP_400_BAD_REQUEST) + def perform_create(self, serializer): + user = serializer.save(self.request) + Token.objects.get_or_create(user=user) + complete_signup(self.request._request, user, + allauth_settings.EMAIL_VERIFICATION, + '/') + return user class VerifyEmailView(APIView, ConfirmEmailView): @@ -80,10 +44,12 @@ class VerifyEmailView(APIView, ConfirmEmailView): allowed_methods = ('POST', 'OPTIONS', 'HEAD') def get(self, *args, **kwargs): - return Response({}, status=status.HTTP_405_METHOD_NOT_ALLOWED) + raise MethodNotAllowed('GET') def post(self, request, *args, **kwargs): - self.kwargs['key'] = self.request.data.get('key', '') + serializer = VerifyEmailSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + self.kwargs['key'] = serializer.validated_data['key'] confirmation = self.get_object() confirmation.confirm(self.request) return Response({'message': 'ok'}, status=status.HTTP_200_OK) From 30fd6414ceb94a766401952ec7cfd03cb00ef882 Mon Sep 17 00:00:00 2001 From: ron8mcr Date: Tue, 24 Nov 2015 21:07:20 +0700 Subject: [PATCH 02/23] Explict Allow Any for register view --- rest_auth/registration/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_auth/registration/views.py b/rest_auth/registration/views.py index f7113f2..ada25c4 100644 --- a/rest_auth/registration/views.py +++ b/rest_auth/registration/views.py @@ -19,6 +19,7 @@ from rest_auth.views import LoginView class RegisterView(CreateAPIView): serializer_class = RegisterSerializer + permission_classes = (AllowAny, ) def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) From 65b29d351560f82d6ea0b2a95cf06d4d2bb7296f Mon Sep 17 00:00:00 2001 From: ron8mcr Date: Tue, 24 Nov 2015 21:16:39 +0700 Subject: [PATCH 03/23] None as success_url for complete_signup in RegisterView --- rest_auth/registration/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_auth/registration/views.py b/rest_auth/registration/views.py index ada25c4..81fd951 100644 --- a/rest_auth/registration/views.py +++ b/rest_auth/registration/views.py @@ -35,7 +35,7 @@ class RegisterView(CreateAPIView): Token.objects.get_or_create(user=user) complete_signup(self.request._request, user, allauth_settings.EMAIL_VERIFICATION, - '/') + None) return user From 52f04ba22447eee8629923eb61ef0337dcf9b541 Mon Sep 17 00:00:00 2001 From: ron8mcr Date: Tue, 24 Nov 2015 22:04:57 +0700 Subject: [PATCH 04/23] Update tests and fix register serializer --- rest_auth/registration/serializers.py | 10 ++++++++-- rest_auth/tests.py | 6 ++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/rest_auth/registration/serializers.py b/rest_auth/registration/serializers.py index 68ba702..d3fcf1d 100644 --- a/rest_auth/registration/serializers.py +++ b/rest_auth/registration/serializers.py @@ -146,11 +146,17 @@ class RegisterSerializer(serializers.Serializer): def custom_signup(self, request, user): pass + def get_cleaned_data(self): + return { + 'username': self.validated_data.get('username', ''), + 'password1': self.validated_data.get('password', ''), + 'email': self.validated_data.get('email', '') + } + def save(self, request): adapter = get_adapter() user = adapter.new_user(request) - self.cleaned_data = self.validated_data - self.cleaned_data['password1'] = self.cleaned_data['password'] + self.cleaned_data = self.get_cleaned_data() adapter.save_user(request, user, self) self.custom_signup(request, user) setup_user_email(request, user, []) diff --git a/rest_auth/tests.py b/rest_auth/tests.py index 4e5a1c2..3fa749d 100644 --- a/rest_auth/tests.py +++ b/rest_auth/tests.py @@ -138,8 +138,7 @@ class APITestCase1(TestCase, BaseAPITestCase): # data without user profile REGISTRATION_DATA = { "username": USERNAME, - "password1": PASS, - "password2": PASS + "password": PASS, } REGISTRATION_DATA_WITH_EMAIL = REGISTRATION_DATA.copy() @@ -432,8 +431,7 @@ class TestSocialAuth(TestCase, BaseAPITestCase): EMAIL = "person1@world.com" REGISTRATION_DATA = { "username": USERNAME, - "password1": PASS, - "password2": PASS, + "password": PASS, "email": EMAIL } From cc963ca1a169506ee46c926fd7e7bc41f0b46780 Mon Sep 17 00:00:00 2001 From: anyone_j Date: Thu, 26 Nov 2015 14:38:25 +0500 Subject: [PATCH 05/23] fix import complete_social_login --- rest_auth/registration/serializers.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rest_auth/registration/serializers.py b/rest_auth/registration/serializers.py index 5f5efd6..90c8ab5 100644 --- a/rest_auth/registration/serializers.py +++ b/rest_auth/registration/serializers.py @@ -5,13 +5,13 @@ from rest_framework import serializers from requests.exceptions import HTTPError # Import is needed only if we are using social login, in which # case the allauth.socialaccount will be declared -try: - from allauth.socialaccount.helpers import complete_social_login -except ImportError: - raise ImportError('allauth.socialaccount needs to be installed.') -if 'allauth.socialaccount' not in settings.INSTALLED_APPS: - raise ImportError('allauth.socialaccount needs to be added to INSTALLED_APPS.') +if 'allauth.socialaccount' in settings.INSTALLED_APPS: + try: + from allauth.socialaccount.helpers import complete_social_login + except ImportError: + pass + class SocialLoginSerializer(serializers.Serializer): From a93b7f5cec9873a6eb97a92f055306b11330548b Mon Sep 17 00:00:00 2001 From: Poderyagin Egor Date: Sun, 13 Dec 2015 22:24:27 +0300 Subject: [PATCH 06/23] Added test case for reset by email in different case --- rest_auth/tests/test_api.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rest_auth/tests/test_api.py b/rest_auth/tests/test_api.py index 7adcf71..b64cf8c 100644 --- a/rest_auth/tests/test_api.py +++ b/rest_auth/tests/test_api.py @@ -224,6 +224,15 @@ class APITestCase1(TestCase, BaseAPITestCase): } self.post(self.login_url, data=payload, status_code=200) + def test_password_reset_with_email_in_different_case(self): + user = get_user_model().objects.create_user(self.USERNAME, self.EMAIL.lower(), self.PASS) + + # call password reset in upper case + mail_count = len(mail.outbox) + payload = {'email': self.EMAIL.upper()} + self.post(self.password_reset_url, data=payload, status_code=200) + self.assertEqual(len(mail.outbox), mail_count + 1) + def test_password_reset_with_invalid_email(self): get_user_model().objects.create_user(self.USERNAME, self.EMAIL, self.PASS) From bb2fb65f7d82775122b21c65cf6797b1ff5a2505 Mon Sep 17 00:00:00 2001 From: Poderyagin Egor Date: Sun, 13 Dec 2015 23:43:33 +0300 Subject: [PATCH 07/23] Auth by email --- rest_auth/serializers.py | 13 ++++++++--- rest_auth/tests/test_api.py | 46 +++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/rest_auth/serializers.py b/rest_auth/serializers.py index a2d1a82..5bf1ac2 100644 --- a/rest_auth/serializers.py +++ b/rest_auth/serializers.py @@ -50,11 +50,18 @@ class LoginSerializer(serializers.Serializer): msg = _('Must include either "username" or "email" and "password".') raise exceptions.ValidationError(msg) - elif username and password: - user = authenticate(username=username, password=password) + elif username or email and password: + # Try get username if we have in request email + if email and not username: + try: + username = UserModel.objects.get(email__iexact=email).username + except UserModel.DoesNotExist: + user = None + if username: + user = authenticate(username=username, password=password) else: - msg = _('Must include "username" and "password".') + msg = _('Must include either "username" or "email" and "password".') raise exceptions.ValidationError(msg) # Did we get back an active user? diff --git a/rest_auth/tests/test_api.py b/rest_auth/tests/test_api.py index 7adcf71..8420d2d 100644 --- a/rest_auth/tests/test_api.py +++ b/rest_auth/tests/test_api.py @@ -2,6 +2,7 @@ from django.core.urlresolvers import reverse from django.test import TestCase from django.contrib.auth import get_user_model from django.core import mail +from django.conf import settings from django.test.utils import override_settings from django.utils.encoding import force_text @@ -90,6 +91,51 @@ class APITestCase1(TestCase, BaseAPITestCase): # test empty payload self.post(self.login_url, data={}, status_code=400) + def test_login_by_email(self): + # starting test without allauth app + settings.INSTALLED_APPS.remove('allauth') + + payload = { + "email": self.EMAIL.lower(), + "password": self.PASS + } + # there is no users in db so it should throw error (400) + self.post(self.login_url, data=payload, status_code=400) + + self.post(self.password_change_url, status_code=403) + + # create user + user = get_user_model().objects.create_user(self.USERNAME, self.EMAIL, self.PASS) + + # test auth by email + self.post(self.login_url, data=payload, status_code=200) + self.assertEqual('key' in self.response.json.keys(), True) + self.token = self.response.json['key'] + + # test auth by email in different case + payload = { + "email": self.EMAIL.upper(), + "password": self.PASS + } + self.post(self.login_url, data=payload, status_code=200) + self.assertEqual('key' in self.response.json.keys(), True) + self.token = self.response.json['key'] + + # test inactive user + user.is_active = False + user.save() + self.post(self.login_url, data=payload, status_code=400) + + # test wrong email/password + payload = { + "email": 't' + self.EMAIL, + "password": self.PASS + } + self.post(self.login_url, data=payload, status_code=400) + + # test empty payload + self.post(self.login_url, data={}, status_code=400) + def test_password_change(self): login_payload = { "username": self.USERNAME, From 411cc298b3a43a772ef906d9a773d37712a473fc Mon Sep 17 00:00:00 2001 From: Will Date: Mon, 21 Dec 2015 11:01:41 -0500 Subject: [PATCH 08/23] Update demo.rst With requirements file of django >= 1.7.0, change command of `syncdb` to `migrate` (`syncdb` is deprecated). --- docs/demo.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/demo.rst b/docs/demo.rst index ed74750..877ca33 100644 --- a/docs/demo.rst +++ b/docs/demo.rst @@ -11,7 +11,7 @@ Do these steps to make it running (ideally in virtualenv). git clone https://github.com/Tivix/django-rest-auth.git cd django-rest-auth/demo/ pip install -r requirements.pip - python manage.py syncdb --settings=demo.settings --noinput + python manage.py migrate --settings=demo.settings --noinput python manage.py runserver --settings=demo.settings Now, go to ``http://127.0.0.1:8000/`` in your browser. From 4d9e33e9a82082c12358c10b87a6ffd4dffad0b2 Mon Sep 17 00:00:00 2001 From: Will Liu Date: Mon, 21 Dec 2015 16:32:53 -0500 Subject: [PATCH 09/23] fix demo by add csrf and modify account settings In response to Issue 116 at https://github.com/Tivix/django-rest-auth/issues/116 * Add csrf_token tags on demo templates (was returning CSRF page) * Update settings file for the demo login (was returning message that email was required when template only shows username and password fields) --- demo/demo/settings.py | 4 ++-- demo/templates/fragments/email_verification_form.html | 2 +- demo/templates/fragments/login_form.html | 2 +- demo/templates/fragments/password_change_form.html | 3 +-- demo/templates/fragments/password_reset_confirm_form.html | 2 +- demo/templates/fragments/password_reset_form.html | 2 +- demo/templates/fragments/signup_form.html | 2 +- demo/templates/fragments/user_details_form.html | 2 +- 8 files changed, 9 insertions(+), 10 deletions(-) diff --git a/demo/demo/settings.py b/demo/demo/settings.py index c82f703..f559d5c 100644 --- a/demo/demo/settings.py +++ b/demo/demo/settings.py @@ -106,9 +106,9 @@ TEMPLATE_DIRS = [os.path.join(BASE_DIR, 'templates')] REST_SESSION_LOGIN = False EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' SITE_ID = 1 -ACCOUNT_EMAIL_REQUIRED = True +ACCOUNT_EMAIL_REQUIRED = False ACCOUNT_AUTHENTICATION_METHOD = 'username' -ACCOUNT_EMAIL_VERIFICATION = 'mandatory' +ACCOUNT_EMAIL_VERIFICATION = 'optional' REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( diff --git a/demo/templates/fragments/email_verification_form.html b/demo/templates/fragments/email_verification_form.html index 2298d0e..a718e0b 100644 --- a/demo/templates/fragments/email_verification_form.html +++ b/demo/templates/fragments/email_verification_form.html @@ -1,5 +1,5 @@ -
+{% csrf_token %}
diff --git a/demo/templates/fragments/login_form.html b/demo/templates/fragments/login_form.html index ee02b53..46aba62 100644 --- a/demo/templates/fragments/login_form.html +++ b/demo/templates/fragments/login_form.html @@ -1,5 +1,5 @@ - +{% csrf_token %}
diff --git a/demo/templates/fragments/password_change_form.html b/demo/templates/fragments/password_change_form.html index 9e64ea4..c8dfda5 100644 --- a/demo/templates/fragments/password_change_form.html +++ b/demo/templates/fragments/password_change_form.html @@ -1,6 +1,5 @@ - - +{% csrf_token %}
diff --git a/demo/templates/fragments/password_reset_confirm_form.html b/demo/templates/fragments/password_reset_confirm_form.html index 973b05d..5a9c395 100644 --- a/demo/templates/fragments/password_reset_confirm_form.html +++ b/demo/templates/fragments/password_reset_confirm_form.html @@ -1,5 +1,5 @@ - +{% csrf_token %}
diff --git a/demo/templates/fragments/password_reset_form.html b/demo/templates/fragments/password_reset_form.html index 6840193..0f61344 100644 --- a/demo/templates/fragments/password_reset_form.html +++ b/demo/templates/fragments/password_reset_form.html @@ -1,5 +1,5 @@ - +{% csrf_token %}
diff --git a/demo/templates/fragments/signup_form.html b/demo/templates/fragments/signup_form.html index 9a7e43e..d60b99b 100644 --- a/demo/templates/fragments/signup_form.html +++ b/demo/templates/fragments/signup_form.html @@ -1,5 +1,5 @@ - +{% csrf_token %}
diff --git a/demo/templates/fragments/user_details_form.html b/demo/templates/fragments/user_details_form.html index 405403b..7fafc0a 100644 --- a/demo/templates/fragments/user_details_form.html +++ b/demo/templates/fragments/user_details_form.html @@ -1,5 +1,5 @@ - +{% csrf_token %}
From 23eb6e5be5897ebd5c60003d24597c94a2288763 Mon Sep 17 00:00:00 2001 From: mario Date: Tue, 22 Dec 2015 09:27:17 +0100 Subject: [PATCH 10/23] Added `coverage_html` to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8a04b95..85446ba 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ htmlcov/ .cache nosetests.xml coverage.xml +coverage_html # Translations *.mo From 4c8db510b0fa526732c94deabfa3e5bf5c98951e Mon Sep 17 00:00:00 2001 From: mario Date: Tue, 22 Dec 2015 09:28:35 +0100 Subject: [PATCH 11/23] Fixed test exception. --- rest_auth/tests/settings.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/rest_auth/tests/settings.py b/rest_auth/tests/settings.py index 5c7a940..b09b496 100644 --- a/rest_auth/tests/settings.py +++ b/rest_auth/tests/settings.py @@ -70,7 +70,3 @@ INSTALLED_APPS = [ SECRET_KEY = "38dh*skf8sjfhs287dh&^hd8&3hdg*j2&sd" ACCOUNT_ACTIVATION_DAYS = 1 SITE_ID = 1 - -MIGRATION_MODULES = { - 'authtoken': 'authtoken.migrations', -} From eb61b24087ad61f5218e7f07dc744668eb096aa8 Mon Sep 17 00:00:00 2001 From: Mateus Caruccio Date: Thu, 31 Dec 2015 19:10:52 -0200 Subject: [PATCH 12/23] Add support for custom Token model --- docs/configuration.rst | 4 +++- rest_auth/app_settings.py | 4 +++- rest_auth/models.py | 9 ++++++++- rest_auth/registration/views.py | 4 ++-- rest_auth/serializers.py | 5 +++-- rest_auth/utils.py | 6 ++++++ rest_auth/views.py | 9 ++++----- 7 files changed, 29 insertions(+), 12 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index 282f326..336abb5 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -29,10 +29,12 @@ Configuration ... } +- **REST_AUTH_TOKEN_MODEL** - model class for tokens, default value ``rest_framework.authtoken.models`` + +- **REST_AUTH_TOKEN_CREATOR** - callable to create tokens, default value ``rest_auth.utils.default_create_token``. - **REST_SESSION_LOGIN** - Enable session login in Login API view (default: True) - - **OLD_PASSWORD_FIELD_ENABLED** - set it to True if you want to have old password verification on password change enpoint (default: False) - **LOGOUT_ON_PASSWORD_CHANGE** - set to False if you want to keep the current user logged in after a password change diff --git a/rest_auth/app_settings.py b/rest_auth/app_settings.py index e0340b7..b77d1d2 100644 --- a/rest_auth/app_settings.py +++ b/rest_auth/app_settings.py @@ -7,8 +7,10 @@ from rest_auth.serializers import ( PasswordResetSerializer as DefaultPasswordResetSerializer, PasswordResetConfirmSerializer as DefaultPasswordResetConfirmSerializer, PasswordChangeSerializer as DefaultPasswordChangeSerializer) -from .utils import import_callable +from .utils import import_callable, default_create_token +create_token = import_callable( + getattr(settings, 'REST_AUTH_TOKEN_CREATOR', default_create_token)) serializers = getattr(settings, 'REST_AUTH_SERIALIZERS', {}) diff --git a/rest_auth/models.py b/rest_auth/models.py index e703865..a132f9c 100644 --- a/rest_auth/models.py +++ b/rest_auth/models.py @@ -1,3 +1,10 @@ -# from django.db import models +from django.conf import settings + +from rest_framework.authtoken.models import Token as DefaultTokenModel + +from .utils import import_callable # Register your models here. + +TokenModel = import_callable( + getattr(settings, 'REST_AUTH_TOKEN_MODEL', DefaultTokenModel)) diff --git a/rest_auth/registration/views.py b/rest_auth/registration/views.py index e700706..57a1327 100644 --- a/rest_auth/registration/views.py +++ b/rest_auth/registration/views.py @@ -3,7 +3,6 @@ from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import AllowAny from rest_framework import status -from rest_framework.authtoken.models import Token from allauth.account.views import SignupView, ConfirmEmailView from allauth.account.utils import complete_signup @@ -12,6 +11,7 @@ from allauth.account import app_settings from rest_auth.app_settings import TokenSerializer from rest_auth.registration.serializers import SocialLoginSerializer from rest_auth.views import LoginView +from rest_auth.models import TokenModel class RegisterView(APIView, SignupView): @@ -27,7 +27,7 @@ class RegisterView(APIView, SignupView): permission_classes = (AllowAny,) allowed_methods = ('POST', 'OPTIONS', 'HEAD') - token_model = Token + token_model = TokenModel serializer_class = TokenSerializer def get(self, *args, **kwargs): diff --git a/rest_auth/serializers.py b/rest_auth/serializers.py index a2d1a82..2dd92b8 100644 --- a/rest_auth/serializers.py +++ b/rest_auth/serializers.py @@ -6,8 +6,9 @@ from django.utils.http import urlsafe_base64_decode as uid_decoder from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import force_text +from .models import TokenModel + from rest_framework import serializers, exceptions -from rest_framework.authtoken.models import Token from rest_framework.exceptions import ValidationError # Get the UserModel @@ -84,7 +85,7 @@ class TokenSerializer(serializers.ModelSerializer): """ class Meta: - model = Token + model = TokenModel fields = ('key',) diff --git a/rest_auth/utils.py b/rest_auth/utils.py index a32da60..e5bbf7c 100644 --- a/rest_auth/utils.py +++ b/rest_auth/utils.py @@ -9,3 +9,9 @@ def import_callable(path_or_callable): assert isinstance(path_or_callable, string_types) package, attr = path_or_callable.rsplit('.', 1) return getattr(import_module(package), attr) + + +def default_create_token(token_model, serializer): + user = serializer.validated_data['user'] + token, _ = token_model.objects.get_or_create(user=user) + return token diff --git a/rest_auth/views.py b/rest_auth/views.py index 3af1557..6f3c413 100644 --- a/rest_auth/views.py +++ b/rest_auth/views.py @@ -7,14 +7,14 @@ from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.generics import GenericAPIView from rest_framework.permissions import IsAuthenticated, AllowAny -from rest_framework.authtoken.models import Token from rest_framework.generics import RetrieveUpdateAPIView from .app_settings import ( TokenSerializer, UserDetailsSerializer, LoginSerializer, PasswordResetSerializer, PasswordResetConfirmSerializer, - PasswordChangeSerializer + PasswordChangeSerializer, create_token ) +from .models import TokenModel class LoginView(GenericAPIView): @@ -30,13 +30,12 @@ class LoginView(GenericAPIView): """ permission_classes = (AllowAny,) serializer_class = LoginSerializer - token_model = Token + token_model = TokenModel response_serializer = TokenSerializer def login(self): self.user = self.serializer.validated_data['user'] - self.token, created = self.token_model.objects.get_or_create( - user=self.user) + self.token = create_token(self.token_model, self.serializer) if getattr(settings, 'REST_SESSION_LOGIN', True): login(self.request, self.user) From c9d55f768c2142d1c3e7201bb46067a90d7b8bd0 Mon Sep 17 00:00:00 2001 From: Mateus Caruccio Date: Thu, 31 Dec 2015 19:10:52 -0200 Subject: [PATCH 13/23] Add support for custom Token model --- docs/configuration.rst | 4 +++- rest_auth/app_settings.py | 4 +++- rest_auth/models.py | 9 ++++++++- rest_auth/registration/views.py | 4 ++-- rest_auth/serializers.py | 5 +++-- rest_auth/utils.py | 6 ++++++ rest_auth/views.py | 9 ++++----- 7 files changed, 29 insertions(+), 12 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index 282f326..336abb5 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -29,10 +29,12 @@ Configuration ... } +- **REST_AUTH_TOKEN_MODEL** - model class for tokens, default value ``rest_framework.authtoken.models`` + +- **REST_AUTH_TOKEN_CREATOR** - callable to create tokens, default value ``rest_auth.utils.default_create_token``. - **REST_SESSION_LOGIN** - Enable session login in Login API view (default: True) - - **OLD_PASSWORD_FIELD_ENABLED** - set it to True if you want to have old password verification on password change enpoint (default: False) - **LOGOUT_ON_PASSWORD_CHANGE** - set to False if you want to keep the current user logged in after a password change diff --git a/rest_auth/app_settings.py b/rest_auth/app_settings.py index e0340b7..b77d1d2 100644 --- a/rest_auth/app_settings.py +++ b/rest_auth/app_settings.py @@ -7,8 +7,10 @@ from rest_auth.serializers import ( PasswordResetSerializer as DefaultPasswordResetSerializer, PasswordResetConfirmSerializer as DefaultPasswordResetConfirmSerializer, PasswordChangeSerializer as DefaultPasswordChangeSerializer) -from .utils import import_callable +from .utils import import_callable, default_create_token +create_token = import_callable( + getattr(settings, 'REST_AUTH_TOKEN_CREATOR', default_create_token)) serializers = getattr(settings, 'REST_AUTH_SERIALIZERS', {}) diff --git a/rest_auth/models.py b/rest_auth/models.py index e703865..a132f9c 100644 --- a/rest_auth/models.py +++ b/rest_auth/models.py @@ -1,3 +1,10 @@ -# from django.db import models +from django.conf import settings + +from rest_framework.authtoken.models import Token as DefaultTokenModel + +from .utils import import_callable # Register your models here. + +TokenModel = import_callable( + getattr(settings, 'REST_AUTH_TOKEN_MODEL', DefaultTokenModel)) diff --git a/rest_auth/registration/views.py b/rest_auth/registration/views.py index e700706..57a1327 100644 --- a/rest_auth/registration/views.py +++ b/rest_auth/registration/views.py @@ -3,7 +3,6 @@ from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import AllowAny from rest_framework import status -from rest_framework.authtoken.models import Token from allauth.account.views import SignupView, ConfirmEmailView from allauth.account.utils import complete_signup @@ -12,6 +11,7 @@ from allauth.account import app_settings from rest_auth.app_settings import TokenSerializer from rest_auth.registration.serializers import SocialLoginSerializer from rest_auth.views import LoginView +from rest_auth.models import TokenModel class RegisterView(APIView, SignupView): @@ -27,7 +27,7 @@ class RegisterView(APIView, SignupView): permission_classes = (AllowAny,) allowed_methods = ('POST', 'OPTIONS', 'HEAD') - token_model = Token + token_model = TokenModel serializer_class = TokenSerializer def get(self, *args, **kwargs): diff --git a/rest_auth/serializers.py b/rest_auth/serializers.py index a2d1a82..2dd92b8 100644 --- a/rest_auth/serializers.py +++ b/rest_auth/serializers.py @@ -6,8 +6,9 @@ from django.utils.http import urlsafe_base64_decode as uid_decoder from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import force_text +from .models import TokenModel + from rest_framework import serializers, exceptions -from rest_framework.authtoken.models import Token from rest_framework.exceptions import ValidationError # Get the UserModel @@ -84,7 +85,7 @@ class TokenSerializer(serializers.ModelSerializer): """ class Meta: - model = Token + model = TokenModel fields = ('key',) diff --git a/rest_auth/utils.py b/rest_auth/utils.py index a32da60..e5bbf7c 100644 --- a/rest_auth/utils.py +++ b/rest_auth/utils.py @@ -9,3 +9,9 @@ def import_callable(path_or_callable): assert isinstance(path_or_callable, string_types) package, attr = path_or_callable.rsplit('.', 1) return getattr(import_module(package), attr) + + +def default_create_token(token_model, serializer): + user = serializer.validated_data['user'] + token, _ = token_model.objects.get_or_create(user=user) + return token diff --git a/rest_auth/views.py b/rest_auth/views.py index 3af1557..6f3c413 100644 --- a/rest_auth/views.py +++ b/rest_auth/views.py @@ -7,14 +7,14 @@ from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.generics import GenericAPIView from rest_framework.permissions import IsAuthenticated, AllowAny -from rest_framework.authtoken.models import Token from rest_framework.generics import RetrieveUpdateAPIView from .app_settings import ( TokenSerializer, UserDetailsSerializer, LoginSerializer, PasswordResetSerializer, PasswordResetConfirmSerializer, - PasswordChangeSerializer + PasswordChangeSerializer, create_token ) +from .models import TokenModel class LoginView(GenericAPIView): @@ -30,13 +30,12 @@ class LoginView(GenericAPIView): """ permission_classes = (AllowAny,) serializer_class = LoginSerializer - token_model = Token + token_model = TokenModel response_serializer = TokenSerializer def login(self): self.user = self.serializer.validated_data['user'] - self.token, created = self.token_model.objects.get_or_create( - user=self.user) + self.token = create_token(self.token_model, self.serializer) if getattr(settings, 'REST_SESSION_LOGIN', True): login(self.request, self.user) From d36a9bc1cbd43af52f43f36dedeed7b6c1e8f7c0 Mon Sep 17 00:00:00 2001 From: Matt d'Entremont Date: Mon, 4 Jan 2016 10:17:47 -0400 Subject: [PATCH 14/23] #131: Do not raise 400 when resetting password for non-existing account - Do not raises validation error if email doesn't exist - Update unit test --- rest_auth/serializers.py | 3 --- rest_auth/tests/test_api.py | 5 ++++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rest_auth/serializers.py b/rest_auth/serializers.py index a2d1a82..4896d3c 100644 --- a/rest_auth/serializers.py +++ b/rest_auth/serializers.py @@ -115,9 +115,6 @@ class PasswordResetSerializer(serializers.Serializer): if not self.reset_form.is_valid(): raise serializers.ValidationError(_('Error')) - if not UserModel.objects.filter(email__iexact=value).exists(): - raise serializers.ValidationError(_('Invalid e-mail address')) - return value def save(self): diff --git a/rest_auth/tests/test_api.py b/rest_auth/tests/test_api.py index b64cf8c..222178c 100644 --- a/rest_auth/tests/test_api.py +++ b/rest_auth/tests/test_api.py @@ -234,12 +234,15 @@ class APITestCase1(TestCase, BaseAPITestCase): self.assertEqual(len(mail.outbox), mail_count + 1) def test_password_reset_with_invalid_email(self): + """ + Invalid email should not raise error, as this would leak users + """ get_user_model().objects.create_user(self.USERNAME, self.EMAIL, self.PASS) # call password reset mail_count = len(mail.outbox) payload = {'email': 'nonexisting@email.com'} - self.post(self.password_reset_url, data=payload, status_code=400) + self.post(self.password_reset_url, data=payload, status_code=200) self.assertEqual(len(mail.outbox), mail_count) def test_user_details(self): From 1d9c2d647e640809fc4d4974fa9468185d53825d Mon Sep 17 00:00:00 2001 From: Matt d'Entremont Date: Mon, 4 Jan 2016 10:27:42 -0400 Subject: [PATCH 15/23] #86: Add missing dependencies to setup.py - Add django-allauth as an extra, and update documentation - Ensure django-allauth and responses are present when running tests --- docs/installation.rst | 2 +- setup.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index 38eaadf..92251dc 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -38,7 +38,7 @@ You're good to go now! Registration (optional) ----------------------- -1. If you want to enable standard registration process you will need to install ``django-allauth`` - see this doc for installation http://django-allauth.readthedocs.org/en/latest/installation.html. +1. If you want to enable standard registration process you will need to install ``django-allauth`` by using ``pip install django-rest-auth[extras]`` or ``pip install django-rest-auth[with_social]``. 2. Add ``allauth``, ``allauth.account`` and ``rest_auth.registration`` apps to INSTALLED_APPS in your django settings.py: diff --git a/setup.py b/setup.py index 6b61e1c..d85b2d2 100644 --- a/setup.py +++ b/setup.py @@ -32,6 +32,13 @@ setup( 'djangorestframework>=3.1.0', 'six>=1.9.0', ], + extras_require={ + 'with_social': ['django-allauth>=0.24.1'], + }, + tests_require=[ + 'responses>=0.5.0', + 'django-allauth>=0.24.1', + ], test_suite='runtests.runtests', include_package_data=True, # cmdclass={}, From 0e3fb4a5c943bf25cc9501ec9003e8063f73cc21 Mon Sep 17 00:00:00 2001 From: Tabatha Memmott Date: Mon, 4 Jan 2016 16:29:47 -0800 Subject: [PATCH 16/23] closes PR #134 and adds same syntax to demo --- demo/demo/urls.py | 6 +++--- rest_auth/registration/urls.py | 7 +++---- rest_auth/urls.py | 7 +++---- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/demo/demo/urls.py b/demo/demo/urls.py index c83bb8f..54d06ad 100644 --- a/demo/demo/urls.py +++ b/demo/demo/urls.py @@ -1,8 +1,8 @@ -from django.conf.urls import patterns, include, url +from django.conf.urls import include, url from django.contrib import admin from django.views.generic import TemplateView, RedirectView -urlpatterns = patterns('', +urlpatterns = [ url(r'^$', TemplateView.as_view(template_name="home.html"), name='home'), url(r'^signup/$', TemplateView.as_view(template_name="signup.html"), name='signup'), @@ -36,4 +36,4 @@ urlpatterns = patterns('', url(r'^account/', include('allauth.urls')), url(r'^admin/', include(admin.site.urls)), url(r'^accounts/profile/$', RedirectView.as_view(url='/', permanent=True), name='profile-redirect'), -) +] diff --git a/rest_auth/registration/urls.py b/rest_auth/registration/urls.py index abdd8b5..9e56c3b 100644 --- a/rest_auth/registration/urls.py +++ b/rest_auth/registration/urls.py @@ -1,10 +1,9 @@ from django.views.generic import TemplateView -from django.conf.urls import patterns, url +from django.conf.urls import url from .views import RegisterView, VerifyEmailView -urlpatterns = patterns( - '', +urlpatterns = [ url(r'^$', RegisterView.as_view(), name='rest_register'), url(r'^verify-email/$', VerifyEmailView.as_view(), name='rest_verify_email'), @@ -21,4 +20,4 @@ urlpatterns = patterns( # djang-allauth https://github.com/pennersr/django-allauth/blob/master/allauth/account/views.py#L190 url(r'^account-confirm-email/(?P\w+)/$', TemplateView.as_view(), name='account_confirm_email'), -) +] diff --git a/rest_auth/urls.py b/rest_auth/urls.py index d753c44..7a35e9b 100644 --- a/rest_auth/urls.py +++ b/rest_auth/urls.py @@ -1,12 +1,11 @@ -from django.conf.urls import patterns, url +from django.conf.urls import url from rest_auth.views import ( LoginView, LogoutView, UserDetailsView, PasswordChangeView, PasswordResetView, PasswordResetConfirmView ) -urlpatterns = patterns( - '', +urlpatterns = [ # URLs that do not require a session or valid token url(r'^password/reset/$', PasswordResetView.as_view(), name='rest_password_reset'), @@ -18,4 +17,4 @@ urlpatterns = patterns( url(r'^user/$', UserDetailsView.as_view(), name='rest_user_details'), url(r'^password/change/$', PasswordChangeView.as_view(), name='rest_password_change'), -) +] From 99c4dc9d05aa3385ed2a4574fde64b553e4d043f Mon Sep 17 00:00:00 2001 From: mario Date: Tue, 5 Jan 2016 14:56:11 +0100 Subject: [PATCH 17/23] Brought back pass verification + added test --- docs/api_endpoints.rst | 3 ++- rest_auth/registration/serializers.py | 19 +++++++++++++------ rest_auth/tests/test_api.py | 9 ++++++++- rest_auth/tests/test_social.py | 3 ++- 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/docs/api_endpoints.rst b/docs/api_endpoints.rst index 86a30c3..9185cab 100644 --- a/docs/api_endpoints.rst +++ b/docs/api_endpoints.rst @@ -51,7 +51,8 @@ Registration - /rest-auth/registration/ (POST) - username - - password + - password1 + - password2 - email - /rest-auth/registration/verify-email/ (POST) diff --git a/rest_auth/registration/serializers.py b/rest_auth/registration/serializers.py index d3fcf1d..b27d7bd 100644 --- a/rest_auth/registration/serializers.py +++ b/rest_auth/registration/serializers.py @@ -122,11 +122,13 @@ class SocialLoginSerializer(serializers.Serializer): class RegisterSerializer(serializers.Serializer): username = serializers.CharField( - max_length=get_username_max_length(), - min_length=allauth_settings.USERNAME_MIN_LENGTH, - required=allauth_settings.USERNAME_REQUIRED) + max_length=get_username_max_length(), + min_length=allauth_settings.USERNAME_MIN_LENGTH, + required=allauth_settings.USERNAME_REQUIRED + ) email = serializers.EmailField(required=allauth_settings.EMAIL_REQUIRED) - password = serializers.CharField(required=True, write_only=True) + password1 = serializers.CharField(required=True, write_only=True) + password2 = serializers.CharField(required=True, write_only=True) def validate_username(self, username): username = get_adapter().clean_username(username) @@ -140,16 +142,21 @@ class RegisterSerializer(serializers.Serializer): "A user is already registered with this e-mail address.") return email - def validate_password(self, password): + def validate_password1(self, password): return get_adapter().clean_password(password) + def validate(self, data): + if data['password1'] != data['password2']: + raise serializers.ValidationError("The two password fields didn't match.") + return data + def custom_signup(self, request, user): pass def get_cleaned_data(self): return { 'username': self.validated_data.get('username', ''), - 'password1': self.validated_data.get('password', ''), + 'password1': self.validated_data.get('password1', ''), 'email': self.validated_data.get('email', '') } diff --git a/rest_auth/tests/test_api.py b/rest_auth/tests/test_api.py index fef9fdf..d5ec105 100644 --- a/rest_auth/tests/test_api.py +++ b/rest_auth/tests/test_api.py @@ -28,7 +28,8 @@ class APITestCase1(TestCase, BaseAPITestCase): # data without user profile REGISTRATION_DATA = { "username": USERNAME, - "password": PASS, + "password1": PASS, + "password2": PASS } REGISTRATION_DATA_WITH_EMAIL = REGISTRATION_DATA.copy() @@ -271,6 +272,12 @@ class APITestCase1(TestCase, BaseAPITestCase): self._login() self._logout() + def test_registration_with_invalid_password(self): + data = self.REGISTRATION_DATA.copy() + data['password2'] = 'foobar' + + self.post(self.register_url, data=data, status_code=400) + @override_settings( ACCOUNT_EMAIL_VERIFICATION='mandatory', ACCOUNT_EMAIL_REQUIRED=True diff --git a/rest_auth/tests/test_social.py b/rest_auth/tests/test_social.py index b95b487..19509ef 100644 --- a/rest_auth/tests/test_social.py +++ b/rest_auth/tests/test_social.py @@ -21,7 +21,8 @@ class TestSocialAuth(TestCase, BaseAPITestCase): EMAIL = "person1@world.com" REGISTRATION_DATA = { "username": USERNAME, - "password": PASS, + "password1": PASS, + "password2": PASS, "email": EMAIL } From 073dd3e76513b9b11ceda7de75e8ca9550783f79 Mon Sep 17 00:00:00 2001 From: mario Date: Tue, 5 Jan 2016 15:09:31 +0100 Subject: [PATCH 18/23] Fixed flake8 warnings --- rest_auth/tests/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_auth/tests/test_api.py b/rest_auth/tests/test_api.py index b64cf8c..bb15e01 100644 --- a/rest_auth/tests/test_api.py +++ b/rest_auth/tests/test_api.py @@ -225,7 +225,7 @@ class APITestCase1(TestCase, BaseAPITestCase): self.post(self.login_url, data=payload, status_code=200) def test_password_reset_with_email_in_different_case(self): - user = get_user_model().objects.create_user(self.USERNAME, self.EMAIL.lower(), self.PASS) + get_user_model().objects.create_user(self.USERNAME, self.EMAIL.lower(), self.PASS) # call password reset in upper case mail_count = len(mail.outbox) From 55fb36ec9108bbdf78d73b1abced4a0d12c299c0 Mon Sep 17 00:00:00 2001 From: Tabatha Memmott Date: Tue, 5 Jan 2016 11:46:01 -0800 Subject: [PATCH 19/23] url change for tests --- rest_auth/tests/urls.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/rest_auth/tests/urls.py b/rest_auth/tests/urls.py index b80541c..d922f7f 100644 --- a/rest_auth/tests/urls.py +++ b/rest_auth/tests/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import patterns, url, include +from django.conf.urls import url, include from django.views.generic import TemplateView from . import django_urls @@ -11,8 +11,7 @@ from rest_auth.registration.views import SocialLoginView class FacebookLogin(SocialLoginView): adapter_class = FacebookOAuth2Adapter -urlpatterns += patterns( - '', +urlpatterns += [ url(r'^rest-registration/', include('rest_auth.registration.urls')), url(r'^test-admin/', include(django_urls)), url(r'^account-email-verification-sent/$', TemplateView.as_view(), @@ -21,4 +20,4 @@ urlpatterns += patterns( name='account_confirm_email'), url(r'^social-login/facebook/$', FacebookLogin.as_view(), name='fb_login'), url(r'^accounts/', include('allauth.socialaccount.urls')) -) +] From 54eb54ad653e9d10cc7ad0d8fa243b0d3ac8cde6 Mon Sep 17 00:00:00 2001 From: mario Date: Wed, 6 Jan 2016 01:18:13 +0100 Subject: [PATCH 20/23] Cleaned up LoginSerializer codebase --- docs/api_endpoints.rst | 1 + rest_auth/serializers.py | 77 ++++++++++++++++++++++++++-------------- 2 files changed, 51 insertions(+), 27 deletions(-) diff --git a/docs/api_endpoints.rst b/docs/api_endpoints.rst index 48e475e..6141653 100644 --- a/docs/api_endpoints.rst +++ b/docs/api_endpoints.rst @@ -7,6 +7,7 @@ Basic - /rest-auth/login/ (POST) - username (string) + - email (string) - password (string) diff --git a/rest_auth/serializers.py b/rest_auth/serializers.py index 5bf1ac2..0fd6eab 100644 --- a/rest_auth/serializers.py +++ b/rest_auth/serializers.py @@ -19,50 +19,73 @@ class LoginSerializer(serializers.Serializer): email = serializers.EmailField(required=False, allow_blank=True) password = serializers.CharField(style={'input_type': 'password'}) + def _validate_email(self, email, password): + user = None + + if email and password: + user = authenticate(email=email, password=password) + else: + msg = _('Must include "email" and "password".') + raise exceptions.ValidationError(msg) + + return user + + def _validate_username(self, username, password): + user = None + + if username and password: + user = authenticate(username=username, password=password) + else: + msg = _('Must include "username" and "password".') + raise exceptions.ValidationError(msg) + + return user + + def _validate_username_email(self, username, email, password): + user = None + + if email and password: + user = authenticate(email=email, password=password) + elif username and password: + user = authenticate(username=username, password=password) + else: + msg = _('Must include either "username" or "email" and "password".') + raise exceptions.ValidationError(msg) + + return user + def validate(self, attrs): username = attrs.get('username') email = attrs.get('email') password = attrs.get('password') + user = None + if 'allauth' in settings.INSTALLED_APPS: from allauth.account import app_settings + # Authentication through email if app_settings.AUTHENTICATION_METHOD == app_settings.AuthenticationMethod.EMAIL: - if email and password: - user = authenticate(email=email, password=password) - else: - msg = _('Must include "email" and "password".') - raise exceptions.ValidationError(msg) + user = self._validate_email(email, password) + # Authentication through username - elif app_settings.AUTHENTICATION_METHOD == app_settings.AuthenticationMethod.USERNAME: - if username and password: - user = authenticate(username=username, password=password) - else: - msg = _('Must include "username" and "password".') - raise exceptions.ValidationError(msg) + if app_settings.AUTHENTICATION_METHOD == app_settings.AuthenticationMethod.USERNAME: + user = self._validate_username(username, password) + # Authentication through either username or email else: - if email and password: - user = authenticate(email=email, password=password) - elif username and password: - user = authenticate(username=username, password=password) - else: - msg = _('Must include either "username" or "email" and "password".') - raise exceptions.ValidationError(msg) + user = self._validate_username_email(username, email, password) - elif username or email and password: - # Try get username if we have in request email - if email and not username: + else: + # Authentication without using allauth + if email: try: username = UserModel.objects.get(email__iexact=email).username except UserModel.DoesNotExist: - user = None - if username: - user = authenticate(username=username, password=password) + pass - else: - msg = _('Must include either "username" or "email" and "password".') - raise exceptions.ValidationError(msg) + if username: + user = self._validate_username_email(username, '', password) # Did we get back an active user? if user: From ae8a26b708f96cf1b945d892ff42ebf052698a1c Mon Sep 17 00:00:00 2001 From: mario Date: Mon, 11 Jan 2016 22:33:14 +0100 Subject: [PATCH 21/23] Return token only when verification is mandatory --- rest_auth/registration/views.py | 12 +++++++++--- rest_auth/tests/test_api.py | 7 +++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/rest_auth/registration/views.py b/rest_auth/registration/views.py index af6d7b6..fa95e7d 100644 --- a/rest_auth/registration/views.py +++ b/rest_auth/registration/views.py @@ -23,14 +23,20 @@ class RegisterView(CreateAPIView): permission_classes = (AllowAny, ) token_model = TokenModel + def get_response_data(self, user): + if allauth_settings.EMAIL_VERIFICATION == \ + allauth_settings.EmailVerificationMethod.MANDATORY: + return {} + + return TokenSerializer(user.auth_token).data + def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) user = self.perform_create(serializer) headers = self.get_success_headers(serializer.data) - return Response(TokenSerializer(user.auth_token).data, - status=status.HTTP_201_CREATED, - headers=headers) + + return Response(self.get_response_data(user), status=status.HTTP_201_CREATED, headers=headers) def perform_create(self, serializer): user = serializer.save(self.request) diff --git a/rest_auth/tests/test_api.py b/rest_auth/tests/test_api.py index 222b3a7..0e0ced2 100644 --- a/rest_auth/tests/test_api.py +++ b/rest_auth/tests/test_api.py @@ -310,8 +310,10 @@ class APITestCase1(TestCase, BaseAPITestCase): # test empty payload self.post(self.register_url, data={}, status_code=400) - self.post(self.register_url, data=self.REGISTRATION_DATA, status_code=201) + result = self.post(self.register_url, data=self.REGISTRATION_DATA, status_code=201) + self.assertIn('key', result.data) self.assertEqual(get_user_model().objects.all().count(), user_count + 1) + new_user = get_user_model().objects.latest('id') self.assertEqual(new_user.username, self.REGISTRATION_DATA['username']) @@ -339,11 +341,12 @@ class APITestCase1(TestCase, BaseAPITestCase): status_code=status.HTTP_400_BAD_REQUEST ) - self.post( + result = self.post( self.register_url, 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') From af9dcbd79b890fafa76bb3c2efdcde9b055ea545 Mon Sep 17 00:00:00 2001 From: mario Date: Thu, 14 Jan 2016 22:36:58 +0100 Subject: [PATCH 22/23] Added FAQ issue. --- docs/faq.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/faq.rst b/docs/faq.rst index 5faeee6..ff04c13 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -17,7 +17,12 @@ FAQ djang-allauth https://github.com/pennersr/django-allauth/blob/master/allauth/account/views.py#L190 -2. How can I update UserProfile assigned to User model? +2. I get an error: Reverse for 'password_reset_confirm' not found. + + You need to add `password_reset_confirm` url into your ``urls.py`` (at the top of any other included urls). Please check the ``urls.py`` module inside demo app example for more details. + + +3. How can I update UserProfile assigned to User model? Assuming you already have UserProfile model defined like this From 7e85667208cd77a8ace6cdd1d7a1dd54e66069a8 Mon Sep 17 00:00:00 2001 From: mario Date: Thu, 14 Jan 2016 23:42:02 +0100 Subject: [PATCH 23/23] Made e-mail options more extendible for `PasswordResetSerializer` --- rest_auth/serializers.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/rest_auth/serializers.py b/rest_auth/serializers.py index 735c188..dbe4f11 100644 --- a/rest_auth/serializers.py +++ b/rest_auth/serializers.py @@ -140,6 +140,11 @@ class PasswordResetSerializer(serializers.Serializer): password_reset_form_class = PasswordResetForm + def get_email_options(self): + """ Override this method to change default e-mail options + """ + return {} + def validate_email(self, value): # Create PasswordResetForm with the serializer self.reset_form = self.password_reset_form_class(data=self.initial_data) @@ -159,6 +164,8 @@ class PasswordResetSerializer(serializers.Serializer): 'from_email': getattr(settings, 'DEFAULT_FROM_EMAIL'), 'request': request, } + + opts.update(self.get_email_options()) self.reset_form.save(**opts)