From fed6b9840c46eefd99cb695687de307e2855cc4a Mon Sep 17 00:00:00 2001 From: Maxim Kukhtenkov Date: Thu, 18 Jan 2018 21:08:41 -0500 Subject: [PATCH] Refactor social connect views and serializers # 347 --- rest_auth/registration/serializers.py | 78 ++++++------ rest_auth/registration/views.py | 163 +++++++++++++------------- rest_auth/social_serializers.py | 111 +++++++++--------- 3 files changed, 169 insertions(+), 183 deletions(-) diff --git a/rest_auth/registration/serializers.py b/rest_auth/registration/serializers.py index 160b4d6..4f99c18 100644 --- a/rest_auth/registration/serializers.py +++ b/rest_auth/registration/serializers.py @@ -1,5 +1,4 @@ from django.http import HttpRequest -from django.conf import settings from django.utils.translation import ugettext_lazy as _ from django.contrib.auth import get_user_model @@ -9,6 +8,9 @@ try: get_username_max_length) from allauth.account.adapter import get_adapter from allauth.account.utils import setup_user_email + from allauth.socialaccount.helpers import complete_social_login + from allauth.socialaccount.models import SocialAccount + from allauth.socialaccount.providers.base import AuthProcess except ImportError: raise ImportError("allauth needs to be added to INSTALLED_APPS.") @@ -16,6 +18,21 @@ from rest_framework import serializers from requests.exceptions import HTTPError +class SocialAccountSerializer(serializers.ModelSerializer): + """ + serialize allauth SocialAccounts for use with a REST API + """ + class Meta: + model = SocialAccount + fields = ( + 'id', + 'provider', + 'uid', + 'last_login', + 'date_joined', + ) + + class SocialLoginSerializer(serializers.Serializer): access_token = serializers.CharField(required=False, allow_blank=True) code = serializers.CharField(required=False, allow_blank=True) @@ -105,7 +122,7 @@ class SocialLoginSerializer(serializers.Serializer): login = self.get_social_login(adapter, app, social_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: # We have an account already signed up in a different flow @@ -130,6 +147,22 @@ class SocialLoginSerializer(serializers.Serializer): return attrs +class SocialConnectMixin(object): + def get_social_login(self, *args, **kwargs): + """ + Set the social login process state to connect rather than login + Refer to the implementation of get_social_login in base class and to the + allauth.socialaccount.helpers module complete_social_login function. + """ + social_login = super(SocialConnectMixin, self).get_social_login(*args, **kwargs) + social_login.state['process'] = AuthProcess.CONNECT + return social_login + + +class SocialConnectSerializer(SocialConnectMixin, SocialLoginSerializer): + pass + + class RegisterSerializer(serializers.Serializer): username = serializers.CharField( max_length=get_username_max_length(), @@ -182,44 +215,3 @@ class RegisterSerializer(serializers.Serializer): class VerifyEmailSerializer(serializers.Serializer): key = serializers.CharField() - - -# Import is needed only if we are using social login, in which -# case the allauth.socialaccount will be declared -if 'allauth.socialaccount' in settings.INSTALLED_APPS: - from allauth.socialaccount.helpers import complete_social_login - from allauth.socialaccount.models import SocialAccount - from allauth.socialaccount.providers.base import AuthProcess - - class SocialAccountSerializer(serializers.ModelSerializer): - """ - serialize allauth SocialAccounts for use with a REST API - """ - class Meta: - model = SocialAccount - fields = ( - 'id', - 'provider', - 'uid', - 'last_login', - 'date_joined', - 'extra_data', - ) - - - class SocialConnectMixin(object): - def get_social_login(self, *args, **kwargs): - """ - set the social login process state to connect rather than login - - Refer to the implementation of get_social_login in base class and to the - allauth.socialaccount.helpers module complete_social_login function. - """ - - social_login = super(SocialConnectMixin, self).get_social_login(*args, **kwargs) - social_login.state['process'] = AuthProcess.CONNECT - return social_login - - - class SocialConnectSerializer(SocialConnectMixin, SocialLoginSerializer): - pass diff --git a/rest_auth/registration/views.py b/rest_auth/registration/views.py index 9343e72..0e0ab0d 100644 --- a/rest_auth/registration/views.py +++ b/rest_auth/registration/views.py @@ -7,21 +7,25 @@ from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import (AllowAny, IsAuthenticated) -from rest_framework.decorators import detail_route -from rest_framework.viewsets import GenericViewSet -from rest_framework.generics import CreateAPIView +from rest_framework.generics import CreateAPIView, ListAPIView, GenericAPIView +from rest_framework.exceptions import NotFound from rest_framework import status +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.socialaccount import signals +from allauth.socialaccount.adapter import get_adapter as get_social_adapter +from allauth.socialaccount.models import SocialAccount from rest_auth.app_settings import (TokenSerializer, JWTSerializer, create_token) from rest_auth.models import TokenModel -from rest_auth.registration.serializers import (SocialLoginSerializer, - VerifyEmailSerializer, +from rest_auth.registration.serializers import (VerifyEmailSerializer, + SocialLoginSerializer, + SocialAccountSerializer, SocialConnectSerializer) from rest_auth.utils import jwt_encode from rest_auth.views import LoginView @@ -94,98 +98,89 @@ class VerifyEmailView(APIView, ConfirmEmailView): return Response({'detail': _('ok')}, status=status.HTTP_200_OK) -if 'allauth.socialaccount' in settings.INSTALLED_APPS: - from allauth.socialaccount import signals - from allauth.socialaccount.models import SocialAccount - from allauth.socialaccount.adapter import get_adapter +class SocialLoginView(LoginView): + """ + class used for social authentications + example usage for facebook with access_token + ------------- + from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter - from rest_auth.registration.serializers import SocialAccountSerializer + class FacebookLogin(SocialLoginView): + adapter_class = FacebookOAuth2Adapter + ------------- + + example usage for facebook with code + + ------------- + from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter + from allauth.socialaccount.providers.oauth2.client import OAuth2Client + + class FacebookLogin(SocialLoginView): + adapter_class = FacebookOAuth2Adapter + client_class = OAuth2Client + callback_url = 'localhost:8000' + ------------- + """ + serializer_class = SocialLoginSerializer + + def process_login(self): + get_adapter(self.request).login(self.request, self.user) - class SocialLoginView(LoginView): - """ - class used for social authentications - example usage for facebook with access_token - ------------- - from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter +class SocialConnectView(LoginView): + """ + class used for social account linking - class FacebookLogin(SocialLoginView): - adapter_class = FacebookOAuth2Adapter - ------------- + example usage for facebook with access_token + ------------- + from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter - example usage for facebook with code + class FacebookConnect(SocialConnectView): + adapter_class = FacebookOAuth2Adapter + ------------- + """ + serializer_class = SocialConnectSerializer + permission_classes = (IsAuthenticated,) - ------------- - from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter - from allauth.socialaccount.providers.oauth2.client import OAuth2Client - - class FacebookLogin(SocialLoginView): - adapter_class = FacebookOAuth2Adapter - client_class = OAuth2Client - callback_url = 'localhost:8000' - ------------- - """ - - serializer_class = SocialLoginSerializer - - def process_login(self): - get_adapter(self.request).login(self.request, self.user) + def process_login(self): + get_adapter(self.request).login(self.request, self.user) - class SocialConnectView(SocialLoginView): - """ - class used for social account linking +class SocialAccountListView(ListAPIView): + """ + List SocialAccounts for the currently logged in user + """ + serializer_class = SocialAccountSerializer + permission_classes = (IsAuthenticated,) - example usage for facebook with access_token - ------------- - from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter - - class FacebookConnect(SocialConnectView): - adapter_class = FacebookOAuth2Adapter - ------------- - """ - - serializer_class = SocialConnectSerializer - permission_classes = (IsAuthenticated,) + def get_queryset(self): + return SocialAccount.objects.filter(user=self.request.user) - class SocialAccountViewSet(GenericViewSet): - """ - allauth SocialAccount REST API read and disconnect views for logged in users +class SocialAccountDisconnectView(GenericAPIView): + """ + Disconnect SocialAccount from remote service for + the currently logged in user + """ + serializer_class = SocialConnectSerializer + permission_classes = (IsAuthenticated,) - Refer to the django-allauth package implementation of the models and - account handling logic for more details, this viewset emulates the allauth web UI. - """ + def get_queryset(self): + return SocialAccount.objects.filter(user=self.request.user) - serializer_class = SocialAccountSerializer - permission_classes = (IsAuthenticated,) - queryset = SocialAccount.objects.none() + def post(self, request, *args, **kwargs): + accounts = self.get_queryset() + account = accounts.filter(pk=kwargs['pk']).first() + if not account: + raise NotFound - def get_queryset(self): - return SocialAccount.objects.filter(user=self.request.user) + get_social_adapter(self.request).validate_disconnect(account, accounts) - def list(self, request): - """ - list SocialAccounts for the currently logged in user - """ + account.delete() + signals.social_account_removed.send( + sender=SocialAccount, + request=self.request, + socialaccount=account + ) - return Response(self.get_serializer(self.get_queryset(), many=True).data) - - @detail_route(methods=['POST']) - def disconnect(self, request, pk): - """ - disconnect SocialAccount from remote service for the currently logged in user - """ - - accounts = self.get_queryset() - account = accounts.get(pk=pk) - get_adapter(self.request).validate_disconnect(account, accounts) - - account.delete() - signals.social_account_removed.send( - sender=SocialAccount, - request=self.request, - socialaccount=account - ) - - return Response(self.get_serializer(account).data) + return Response(self.get_serializer(account).data) diff --git a/rest_auth/social_serializers.py b/rest_auth/social_serializers.py index 6161d1a..1621813 100644 --- a/rest_auth/social_serializers.py +++ b/rest_auth/social_serializers.py @@ -1,7 +1,6 @@ from django.conf import settings from django.http import HttpRequest from rest_framework import serializers - # Import is needed only if we are using social login, in which # case the allauth.socialaccount will be declared if 'allauth.socialaccount' in settings.INSTALLED_APPS: @@ -12,71 +11,71 @@ if 'allauth.socialaccount' in settings.INSTALLED_APPS: from rest_auth.registration.serializers import SocialConnectMixin - class TwitterLoginSerializer(serializers.Serializer): - access_token = serializers.CharField() - token_secret = serializers.CharField() +class TwitterLoginSerializer(serializers.Serializer): + access_token = serializers.CharField() + token_secret = serializers.CharField() - def _get_request(self): - request = self.context.get('request') - if not isinstance(request, HttpRequest): - request = request._request - return request + 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 - :returns: 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 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 + :returns: 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() + 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" - ) + 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_class = getattr(view, 'adapter_class', None) + if not adapter_class: + raise serializers.ValidationError("Define adapter_class in view") - adapter = adapter_class(request) - app = adapter.get_provider().get_app(request) + adapter = adapter_class(request) + app = adapter.get_provider().get_app(request) - access_token = attrs.get('access_token') - token_secret = attrs.get('token_secret') + access_token = attrs.get('access_token') + token_secret = attrs.get('token_secret') - 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 + 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 OAuthError as e: - raise serializers.ValidationError(str(e)) + try: + login = self.get_social_login(adapter, app, token, access_token) + complete_social_login(request, login) + except OAuthError as e: + raise serializers.ValidationError(str(e)) - if not login.is_existing: - login.lookup() - login.save(request, connect=True) - attrs['user'] = login.account.user + if not login.is_existing: + login.lookup() + login.save(request, connect=True) + attrs['user'] = login.account.user - return attrs + return attrs - class TwitterConnectSerializer(SocialConnectMixin, TwitterLoginSerializer): - pass +class TwitterConnectSerializer(SocialConnectMixin, TwitterLoginSerializer): + pass