diff --git a/.travis.yml b/.travis.yml index 5152e3c..f07f4a3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,9 @@ language: python python: - "2.7" env: - - DJANGO=1.7.7 - - DJANGO=1.8 + - DJANGO=1.7.11 + - DJANGO=1.8.9 + - DJANGO=1.9.2 install: - pip install -q Django==$DJANGO --use-mirrors - pip install coveralls diff --git a/demo/demo/urls.py b/demo/demo/urls.py index 54d06ad..3813e5a 100644 --- a/demo/demo/urls.py +++ b/demo/demo/urls.py @@ -11,6 +11,8 @@ urlpatterns = [ name='email-verification'), url(r'^login/$', TemplateView.as_view(template_name="login.html"), name='login'), + url(r'^logout/$', TemplateView.as_view(template_name="logout.html"), + name='logout'), url(r'^password-reset/$', TemplateView.as_view(template_name="password_reset.html"), name='password-reset'), diff --git a/demo/requirements.pip b/demo/requirements.pip index f1c5057..4949275 100644 --- a/demo/requirements.pip +++ b/demo/requirements.pip @@ -1,4 +1,4 @@ django>=1.7.0 -django-rest-auth==0.6.0 +django-rest-auth==0.7.0 django-allauth==0.24.1 six==1.9.0 diff --git a/demo/templates/base.html b/demo/templates/base.html index 8a0b0ed..03a2b73 100644 --- a/demo/templates/base.html +++ b/demo/templates/base.html @@ -40,6 +40,7 @@
  • User details
  • +
  • Logout
  • Password change
  • diff --git a/demo/templates/fragments/logout_form.html b/demo/templates/fragments/logout_form.html new file mode 100644 index 0000000..7fd281d --- /dev/null +++ b/demo/templates/fragments/logout_form.html @@ -0,0 +1,20 @@ +{% block content %} + +
    {% csrf_token %} +
    + +
    + +

    Token received after login

    +
    +
    + +
    +
    + +
    +
    + +
    +
    +{% endblock %} diff --git a/demo/templates/logout.html b/demo/templates/logout.html new file mode 100644 index 0000000..2ae28e2 --- /dev/null +++ b/demo/templates/logout.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    Logout


    + {% include "fragments/logout_form.html" %} +
    +{% endblock %} diff --git a/docs/api_endpoints.rst b/docs/api_endpoints.rst index 1f9660f..05c2196 100644 --- a/docs/api_endpoints.rst +++ b/docs/api_endpoints.rst @@ -11,7 +11,11 @@ Basic - password (string) -- /rest-auth/logout/ (POST) +- /rest-auth/logout/ (POST, GET) + + .. note:: ``ACCOUNT_LOGOUT_ON_GET = True`` to allow logout using GET (this is the exact same conf from allauth) + + - token - /rest-auth/password/reset/ (POST) @@ -70,3 +74,8 @@ Basing on example from installation section :doc:`Installation ` - access_token - code + +- /rest-auth/twitter/ (POST) + + - access_token + - token_secret diff --git a/docs/changelog.rst b/docs/changelog.rst index 41e982d..d1f64a3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,9 +1,15 @@ Changelog ========= -0.7.0 +0.8.0 ----- - added support for django-rest-framework-jwt +0.7.0 +----- +- Wrapped API returned strings in ugettext_lazy +- Fixed not using ``get_username`` which caused issues when using custom user model without username field +- Django 1.9 support +- Added ``TwitterLoginSerializer`` 0.6.0 ----- @@ -61,4 +67,4 @@ Changelog - changed password reset confim url - uid and token should be sent in POST - increase test coverage - made compatibile with django 1.7 -- removed user profile support +- removed user profile support \ No newline at end of file diff --git a/docs/configuration.rst b/docs/configuration.rst index d726a99..0728e11 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -31,7 +31,7 @@ Configuration ... } -- **REST_AUTH_REGISTRATION_SERIALIZERS** +- **REST_AUTH_REGISTER_SERIALIZERS** You can define your custom serializers for registration endpoint. Possible key values: diff --git a/docs/index.rst b/docs/index.rst index 2ad4a05..dc25e83 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,7 +6,7 @@ Welcome to django-rest-auth's documentation! ============================================ -.. warning:: Updating django-rest-auth to version **0.3.4** is highly recommended because of a security issue in PasswordResetConfirmation validation method. +.. warning:: Updating django-rest-auth from version **0.3.3** is highly recommended because of a security issue in PasswordResetConfirmation validation method. .. note:: django-rest-auth from v0.3.3 supports django-rest-framework v3.0 diff --git a/docs/installation.rst b/docs/installation.rst index 1118826..9e47a47 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -65,11 +65,11 @@ Registration (optional) Social Authentication (optional) -------------------------------- -Using ``django-allauth``, ``django-rest-auth`` provides helpful class for creating social media authentication view. Below is an example with Facebook authentication. +Using ``django-allauth``, ``django-rest-auth`` provides helpful class for creating social media authentication view. -.. note:: Points 1, 2 and 3 are related with ``django-allauth`` configuration, so if you have already configured social authentication, then please go to step 4. See ``django-allauth`` documentation for more details. +.. note:: Points 1 and 2 are related to ``django-allauth`` configuration, so if you have already configured social authentication, then please go to step 3. See ``django-allauth`` documentation for more details. -1. Add ``allauth.socialaccount`` and ``allauth.socialaccount.providers.facebook`` apps to INSTALLED_APPS in your django settings.py: +1. Add ``allauth.socialaccount`` and ``allauth.socialaccount.providers.facebook`` or ``allauth.socialaccount.providers.twitter`` apps to INSTALLED_APPS in your django settings.py: .. code-block:: python @@ -85,10 +85,15 @@ Using ``django-allauth``, ``django-rest-auth`` provides helpful class for creati ..., 'allauth.socialaccount', 'allauth.socialaccount.providers.facebook', + 'allauth.socialaccount.providers.twitter', + ) 2. Add Social Application in django admin panel +Facebook +######## + 3. Create new view as a subclass of ``rest_auth.registration.views.SocialLoginView`` with ``FacebookOAuth2Adapter`` adapter as an attribute: .. code-block:: python @@ -108,6 +113,32 @@ Using ``django-allauth``, ``django-rest-auth`` provides helpful class for creati url(r'^rest-auth/facebook/$', FacebookLogin.as_view(), name='fb_login') ) + +Twitter +####### + +If you are using Twitter for your social authentication, it is a bit different since Twitter uses OAuth 1.0. + +3. Create new view as a subclass of ``rest_auth.views.LoginView`` with ``TwitterOAuthAdapter`` adapter and ``TwitterLoginSerializer`` as an attribute: + +.. code-block:: python + + from allauth.socialaccount.providers.twitter.views import TwitterOAuthAdapter + from rest_auth.views import LoginView + from rest_auth.social_serializers import TwitterLoginSerializer + + class TwitterLogin(LoginView): + serializer_class = TwitterLoginSerializer + adapter_class = TwitterOAuthAdapter + +4. Create url for TwitterLogin view: + +.. code-block:: python + + urlpatterns += pattern('', + ..., + url(r'^rest-auth/twitter/$', TwitterLogin.as_view(), name='twitter_login') + ) .. note:: Starting from v0.21.0, django-allauth has dropped support for context processors. Check out http://django-allauth.readthedocs.org/en/latest/changelog.html#from-0-21-0 for more details. diff --git a/rest_auth/locale/de/LC_MESSAGES/django.po b/rest_auth/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..3ad22d0 --- /dev/null +++ b/rest_auth/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,99 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-02-02 14:11+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: registration/serializers.py:54 +msgid "View is not defined, pass it as a context variable" +msgstr "\"View\" ist nicht definiert, übergib es als Contextvariable" + +#: registration/serializers.py:59 +msgid "Define adapter_class in view" +msgstr "Definier \"adapter_class\" in view" + +#: registration/serializers.py:78 +msgid "Define callback_url in view" +msgstr "Definier \"callback_url\" in view" + +#: registration/serializers.py:82 +msgid "Define client_class in view" +msgstr "Definier \"client_class\" in view" + +#: registration/serializers.py:102 +msgid "Incorrect input. access_token or code is required." +msgstr "Falsche Eingabe. \"access_token\" oder \"code\" erforderlich." + +#: registration/serializers.py:111 +msgid "Incorrect value" +msgstr "Falscher Wert." + +#: registration/serializers.py:140 +msgid "A user is already registered with this e-mail address." +msgstr "Ein User mit dieser E-Mail Adresse ist schon registriert." + +#: registration/serializers.py:148 +msgid "The two password fields didn't match." +msgstr "Die beiden Passwörter sind nicht identisch." + +#: registration/views.py:64 +msgid "ok" +msgstr "Ok" + +#: serializers.py:29 +msgid "Must include \"email\" and \"password\"." +msgstr "Muss \"email\" und \"password\" enthalten." + +#: serializers.py:40 +msgid "Must include \"username\" and \"password\"." +msgstr "Muss \"username\" und \"password\" enthalten." + +#: serializers.py:53 +msgid "Must include either \"username\" or \"email\" and \"password\"." +msgstr "Muss entweder \"username\" oder \"email\" und password \"password\"" + +#: serializers.py:94 +msgid "User account is disabled." +msgstr "Der Useraccount ist deaktiviert." + +#: serializers.py:97 +msgid "Unable to log in with provided credentials." +msgstr "Kann nicht mit den angegeben Zugangsdaten anmelden." + +#: serializers.py:106 +msgid "E-mail is not verified." +msgstr "E-Mail Adresse ist nicht verifiziert." + +#: serializers.py:152 +msgid "Error" +msgstr "Fehler" + +#: views.py:71 +msgid "Successfully logged out." +msgstr "Erfolgreich ausgeloggt." + +#: views.py:111 +msgid "Password reset e-mail has been sent." +msgstr "Die E-Mail zum Zurücksetzen des Passwortes wurde verschickt." + +#: views.py:132 +msgid "Password has been reset with the new password." +msgstr "Das Passwort wurde mit dem neuen Passwort ersetzt." + +#: views.py:150 +msgid "New password has been saved." +msgstr "Das neue Passwort wurde gespeichert." diff --git a/rest_auth/registration/serializers.py b/rest_auth/registration/serializers.py index e3c83fb..2390b05 100644 --- a/rest_auth/registration/serializers.py +++ b/rest_auth/registration/serializers.py @@ -1,5 +1,6 @@ from django.http import HttpRequest from django.conf import settings +from django.utils.translation import ugettext_lazy as _ try: from allauth.account import app_settings as allauth_settings @@ -22,7 +23,6 @@ if 'allauth.socialaccount' in settings.INSTALLED_APPS: pass - class SocialLoginSerializer(serializers.Serializer): access_token = serializers.CharField(required=False, allow_blank=True) code = serializers.CharField(required=False, allow_blank=True) @@ -53,12 +53,12 @@ class SocialLoginSerializer(serializers.Serializer): if not view: raise serializers.ValidationError( - 'View is not defined, pass it as a context variable' + _('View is not defined, pass it as a context variable') ) adapter_class = getattr(view, 'adapter_class', None) if not adapter_class: - raise serializers.ValidationError('Define adapter_class in view') + raise serializers.ValidationError(_('Define adapter_class in view')) adapter = adapter_class() app = adapter.get_provider().get_app(request) @@ -77,11 +77,11 @@ class SocialLoginSerializer(serializers.Serializer): if not self.callback_url: raise serializers.ValidationError( - 'Define callback_url in view' + _('Define callback_url in view') ) if not self.client_class: raise serializers.ValidationError( - 'Define client_class in view' + _('Define client_class in view') ) code = attrs.get('code') @@ -101,7 +101,7 @@ class SocialLoginSerializer(serializers.Serializer): access_token = token['access_token'] else: - raise serializers.ValidationError('Incorrect input. access_token or code is required.') + raise serializers.ValidationError(_('Incorrect input. access_token or code is required.')) token = adapter.parse_token({'access_token': access_token}) token.app = app @@ -110,7 +110,7 @@ class SocialLoginSerializer(serializers.Serializer): login = self.get_social_login(adapter, app, token, access_token) complete_social_login(request, login) except HTTPError: - raise serializers.ValidationError('Incorrect value') + raise serializers.ValidationError(_('Incorrect value')) if not login.is_existing: login.lookup() @@ -139,7 +139,7 @@ class RegisterSerializer(serializers.Serializer): 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.") + _("A user is already registered with this e-mail address.")) return email def validate_password1(self, password): @@ -147,7 +147,7 @@ class RegisterSerializer(serializers.Serializer): def validate(self, data): if data['password1'] != data['password2']: - raise serializers.ValidationError("The two password fields didn't match.") + raise serializers.ValidationError(_("The two password fields didn't match.")) return data def custom_signup(self, request, user): diff --git a/rest_auth/registration/views.py b/rest_auth/registration/views.py index 2689ec2..40316dd 100644 --- a/rest_auth/registration/views.py +++ b/rest_auth/registration/views.py @@ -1,4 +1,4 @@ -from django.conf import settings +from django.utils.translation import ugettext_lazy as _ from rest_framework.views import APIView from rest_framework.response import Response @@ -75,7 +75,7 @@ class VerifyEmailView(APIView, ConfirmEmailView): self.kwargs['key'] = serializer.validated_data['key'] confirmation = self.get_object() confirmation.confirm(self.request) - return Response({'message': 'ok'}, status=status.HTTP_200_OK) + return Response({'message': _('ok')}, status=status.HTTP_200_OK) class SocialLoginView(LoginView): @@ -102,4 +102,4 @@ class SocialLoginView(LoginView): ------------- """ - serializer_class = SocialLoginSerializer + serializer_class = SocialLoginSerializer \ No newline at end of file diff --git a/rest_auth/serializers.py b/rest_auth/serializers.py index f1e8173..a414d23 100644 --- a/rest_auth/serializers.py +++ b/rest_auth/serializers.py @@ -81,7 +81,7 @@ class LoginSerializer(serializers.Serializer): # Authentication without using allauth if email: try: - username = UserModel.objects.get(email__iexact=email).username + username = UserModel.objects.get(email__iexact=email).get_username() except UserModel.DoesNotExist: pass @@ -103,7 +103,7 @@ class LoginSerializer(serializers.Serializer): if app_settings.EMAIL_VERIFICATION == app_settings.EmailVerificationMethod.MANDATORY: email_address = user.emailaddress_set.get(email=user.email) if not email_address.verified: - raise serializers.ValidationError('E-mail is not verified.') + raise serializers.ValidationError(_('E-mail is not verified.')) attrs['user'] = user return attrs diff --git a/rest_auth/social_serializers.py b/rest_auth/social_serializers.py new file mode 100644 index 0000000..087e6e1 --- /dev/null +++ b/rest_auth/social_serializers.py @@ -0,0 +1,78 @@ +from django.http import HttpRequest +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: + pass + +from allauth.socialaccount.models import SocialToken + + +class TwitterLoginSerializer(serializers.Serializer): + access_token = serializers.CharField(required=True) + token_secret = serializers.CharField(required=True) + + def _get_request(self): + request = self.context.get('request') + if not isinstance(request, HttpRequest): + request = request._request + return request + + def get_social_login(self, adapter, app, token, response): + """ + + :param adapter: allauth.socialaccount Adapter subclass. Usually OAuthAdapter or Auth2Adapter + :param app: `allauth.socialaccount.SocialApp` instance + :param token: `allauth.socialaccount.SocialToken` instance + :param response: Provider's response for OAuth1. Not used in the + :return: :return: A populated instance of the `allauth.socialaccount.SocialLoginView` instance + """ + request = self._get_request() + social_login = adapter.complete_login(request, app, token, response=response) + social_login.token = token + return social_login + + def validate(self, attrs): + view = self.context.get('view') + request = self._get_request() + + if not view: + raise serializers.ValidationError( + 'View is not defined, pass it as a context variable' + ) + + adapter_class = getattr(view, 'adapter_class', None) + if not adapter_class: + raise serializers.ValidationError('Define adapter_class in view') + + adapter = adapter_class() + app = adapter.get_provider().get_app(request) + + if('access_token' in attrs) and ('token_secret' in attrs): + access_token = attrs.get('access_token') + token_secret = attrs.get('token_secret') + else: + raise serializers.ValidationError('Incorrect input. access_token and token_secret are required.') + + request.session['oauth_api.twitter.com_access_token'] = { + 'oauth_token': access_token, + 'oauth_token_secret': token_secret, + } + token = SocialToken(token=access_token, token_secret=token_secret) + token.app = app + + try: + login = self.get_social_login(adapter, app, token, access_token) + complete_social_login(request, login) + except HTTPError: + raise serializers.ValidationError('Incorrect value') + + if not login.is_existing: + login.lookup() + login.save(request, connect=True) + attrs['user'] = login.account.user + + return attrs diff --git a/rest_auth/tests/test_api.py b/rest_auth/tests/test_api.py index 9903c9d..f75322c 100644 --- a/rest_auth/tests/test_api.py +++ b/rest_auth/tests/test_api.py @@ -421,3 +421,29 @@ class APITestCase1(TestCase, BaseAPITestCase): # try to login again self._login() self._logout() + + @override_settings(ACCOUNT_LOGOUT_ON_GET=True) + def test_logout_on_get(self): + payload = { + "username": self.USERNAME, + "password": self.PASS + } + + # create user + user = get_user_model().objects.create_user(self.USERNAME, '', self.PASS) + + self.post(self.login_url, data=payload, status_code=200) + self.get(self.logout_url, status=status.HTTP_200_OK) + + @override_settings(ACCOUNT_LOGOUT_ON_GET=False) + def test_logout_on_post_only(self): + payload = { + "username": self.USERNAME, + "password": self.PASS + } + + # create user + user = get_user_model().objects.create_user(self.USERNAME, '', self.PASS) + + 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) diff --git a/rest_auth/views.py b/rest_auth/views.py index 5951a93..25f47f2 100644 --- a/rest_auth/views.py +++ b/rest_auth/views.py @@ -1,6 +1,7 @@ from django.contrib.auth import login, logout from django.conf import settings from django.core.exceptions import ObjectDoesNotExist +from django.utils.translation import ugettext_lazy as _ from rest_framework import status from rest_framework.views import APIView @@ -9,6 +10,8 @@ from rest_framework.generics import GenericAPIView from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.generics import RetrieveUpdateAPIView +from allauth.account import app_settings as allauth_settings + from .app_settings import ( TokenSerializer, UserDetailsSerializer, LoginSerializer, PasswordResetSerializer, PasswordResetConfirmSerializer, @@ -84,7 +87,23 @@ class LogoutView(APIView): """ permission_classes = (AllowAny,) + def get(self, request, *args, **kwargs): + try: + if allauth_settings.LOGOUT_ON_GET: + response = self.logout(request) + else: + response = self.http_method_not_allowed(request, *args, **kwargs) + except Exception as exc: + response = self.handle_exception(exc) + + return self.finalize_response(request, response, *args, **kwargs) + self.response = self.finalize_response(request, response, *args, **kwargs) + return self.response + def post(self, request): + return self.logout(request) + + def logout(self, request): try: request.user.auth_token.delete() except (AttributeError, ObjectDoesNotExist): @@ -92,12 +111,11 @@ class LogoutView(APIView): logout(request) - return Response({"success": "Successfully logged out."}, + return Response({"success": _("Successfully logged out.")}, status=status.HTTP_200_OK) class UserDetailsView(RetrieveUpdateAPIView): - """ Returns User's details in JSON format. @@ -134,13 +152,12 @@ class PasswordResetView(GenericAPIView): serializer.save() # Return the success message with OK HTTP status return Response( - {"success": "Password reset e-mail has been sent."}, + {"success": _("Password reset e-mail has been sent.")}, status=status.HTTP_200_OK ) class PasswordResetConfirmView(GenericAPIView): - """ Password reset e-mail link is confirmed, therefore this resets the user's password. @@ -156,11 +173,10 @@ class PasswordResetConfirmView(GenericAPIView): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) serializer.save() - return Response({"success": "Password has been reset with the new password."}) + return Response({"success": _("Password has been reset with the new password.")}) class PasswordChangeView(GenericAPIView): - """ Calls Django Auth SetPasswordForm save method. @@ -175,4 +191,4 @@ class PasswordChangeView(GenericAPIView): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) serializer.save() - return Response({"success": "New password has been saved."}) + return Response({"success": _("New password has been saved.")})