Refactor social connect views and serializers

# 347
This commit is contained in:
Maxim Kukhtenkov 2018-01-18 21:08:41 -05:00
parent 41ae498be0
commit fed6b9840c
3 changed files with 169 additions and 183 deletions

View File

@ -1,5 +1,4 @@
from django.http import HttpRequest from django.http import HttpRequest
from django.conf import settings
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
@ -9,6 +8,9 @@ try:
get_username_max_length) get_username_max_length)
from allauth.account.adapter import get_adapter from allauth.account.adapter import get_adapter
from allauth.account.utils import setup_user_email 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: except ImportError:
raise ImportError("allauth needs to be added to INSTALLED_APPS.") raise ImportError("allauth needs to be added to INSTALLED_APPS.")
@ -16,6 +18,21 @@ from rest_framework import serializers
from requests.exceptions import HTTPError 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): class SocialLoginSerializer(serializers.Serializer):
access_token = serializers.CharField(required=False, allow_blank=True) access_token = serializers.CharField(required=False, allow_blank=True)
code = 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) login = self.get_social_login(adapter, app, social_token, access_token)
complete_social_login(request, login) complete_social_login(request, login)
except HTTPError: except HTTPError:
raise serializers.ValidationError(_('Incorrect value')) raise serializers.ValidationError(_("Incorrect value"))
if not login.is_existing: if not login.is_existing:
# We have an account already signed up in a different flow # We have an account already signed up in a different flow
@ -130,6 +147,22 @@ class SocialLoginSerializer(serializers.Serializer):
return attrs 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): class RegisterSerializer(serializers.Serializer):
username = serializers.CharField( username = serializers.CharField(
max_length=get_username_max_length(), max_length=get_username_max_length(),
@ -182,44 +215,3 @@ class RegisterSerializer(serializers.Serializer):
class VerifyEmailSerializer(serializers.Serializer): class VerifyEmailSerializer(serializers.Serializer):
key = serializers.CharField() 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

View File

@ -7,21 +7,25 @@ from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.permissions import (AllowAny, from rest_framework.permissions import (AllowAny,
IsAuthenticated) IsAuthenticated)
from rest_framework.decorators import detail_route from rest_framework.generics import CreateAPIView, ListAPIView, GenericAPIView
from rest_framework.viewsets import GenericViewSet from rest_framework.exceptions import NotFound
from rest_framework.generics import CreateAPIView
from rest_framework import status from rest_framework import status
from allauth.account.adapter import get_adapter
from allauth.account.views import ConfirmEmailView from allauth.account.views import ConfirmEmailView
from allauth.account.utils import complete_signup from allauth.account.utils import complete_signup
from allauth.account import app_settings as allauth_settings 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, from rest_auth.app_settings import (TokenSerializer,
JWTSerializer, JWTSerializer,
create_token) create_token)
from rest_auth.models import TokenModel from rest_auth.models import TokenModel
from rest_auth.registration.serializers import (SocialLoginSerializer, from rest_auth.registration.serializers import (VerifyEmailSerializer,
VerifyEmailSerializer, SocialLoginSerializer,
SocialAccountSerializer,
SocialConnectSerializer) SocialConnectSerializer)
from rest_auth.utils import jwt_encode from rest_auth.utils import jwt_encode
from rest_auth.views import LoginView from rest_auth.views import LoginView
@ -94,98 +98,89 @@ class VerifyEmailView(APIView, ConfirmEmailView):
return Response({'detail': _('ok')}, status=status.HTTP_200_OK) return Response({'detail': _('ok')}, status=status.HTTP_200_OK)
if 'allauth.socialaccount' in settings.INSTALLED_APPS: class SocialLoginView(LoginView):
from allauth.socialaccount import signals """
from allauth.socialaccount.models import SocialAccount class used for social authentications
from allauth.socialaccount.adapter import get_adapter 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 SocialConnectView(LoginView):
""" """
class used for social authentications class used for social account linking
example usage for facebook with access_token
-------------
from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter
class FacebookLogin(SocialLoginView): example usage for facebook with access_token
adapter_class = FacebookOAuth2Adapter -------------
------------- 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,)
------------- def process_login(self):
from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter get_adapter(self.request).login(self.request, self.user)
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 SocialConnectView(SocialLoginView): class SocialAccountListView(ListAPIView):
""" """
class used for social account linking List SocialAccounts for the currently logged in user
"""
serializer_class = SocialAccountSerializer
permission_classes = (IsAuthenticated,)
example usage for facebook with access_token def get_queryset(self):
------------- return SocialAccount.objects.filter(user=self.request.user)
from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter
class FacebookConnect(SocialConnectView):
adapter_class = FacebookOAuth2Adapter
-------------
"""
serializer_class = SocialConnectSerializer
permission_classes = (IsAuthenticated,)
class SocialAccountViewSet(GenericViewSet): class SocialAccountDisconnectView(GenericAPIView):
""" """
allauth SocialAccount REST API read and disconnect views for logged in users 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 def get_queryset(self):
account handling logic for more details, this viewset emulates the allauth web UI. return SocialAccount.objects.filter(user=self.request.user)
"""
serializer_class = SocialAccountSerializer def post(self, request, *args, **kwargs):
permission_classes = (IsAuthenticated,) accounts = self.get_queryset()
queryset = SocialAccount.objects.none() account = accounts.filter(pk=kwargs['pk']).first()
if not account:
raise NotFound
def get_queryset(self): get_social_adapter(self.request).validate_disconnect(account, accounts)
return SocialAccount.objects.filter(user=self.request.user)
def list(self, request): account.delete()
""" signals.social_account_removed.send(
list SocialAccounts for the currently logged in user sender=SocialAccount,
""" request=self.request,
socialaccount=account
)
return Response(self.get_serializer(self.get_queryset(), many=True).data) return Response(self.get_serializer(account).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)

View File

@ -1,7 +1,6 @@
from django.conf import settings from django.conf import settings
from django.http import HttpRequest from django.http import HttpRequest
from rest_framework import serializers from rest_framework import serializers
# Import is needed only if we are using social login, in which # Import is needed only if we are using social login, in which
# case the allauth.socialaccount will be declared # case the allauth.socialaccount will be declared
if 'allauth.socialaccount' in settings.INSTALLED_APPS: 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 from rest_auth.registration.serializers import SocialConnectMixin
class TwitterLoginSerializer(serializers.Serializer): class TwitterLoginSerializer(serializers.Serializer):
access_token = serializers.CharField() access_token = serializers.CharField()
token_secret = serializers.CharField() token_secret = serializers.CharField()
def _get_request(self): def _get_request(self):
request = self.context.get('request') request = self.context.get('request')
if not isinstance(request, HttpRequest): if not isinstance(request, HttpRequest):
request = request._request request = request._request
return request return request
def get_social_login(self, adapter, app, token, response): def get_social_login(self, adapter, app, token, response):
""" """
:param adapter: allauth.socialaccount Adapter subclass. :param adapter: allauth.socialaccount Adapter subclass.
Usually OAuthAdapter or Auth2Adapter Usually OAuthAdapter or Auth2Adapter
:param app: `allauth.socialaccount.SocialApp` instance :param app: `allauth.socialaccount.SocialApp` instance
:param token: `allauth.socialaccount.SocialToken` instance :param token: `allauth.socialaccount.SocialToken` instance
:param response: Provider's response for OAuth1. Not used in the :param response: Provider's response for OAuth1. Not used in the
:returns: A populated instance of the :returns: A populated instance of the
`allauth.socialaccount.SocialLoginView` instance `allauth.socialaccount.SocialLoginView` instance
""" """
request = self._get_request() request = self._get_request()
social_login = adapter.complete_login(request, app, token, social_login = adapter.complete_login(request, app, token,
response=response) response=response)
social_login.token = token social_login.token = token
return social_login return social_login
def validate(self, attrs): def validate(self, attrs):
view = self.context.get('view') view = self.context.get('view')
request = self._get_request() request = self._get_request()
if not view: if not view:
raise serializers.ValidationError( 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) adapter_class = getattr(view, 'adapter_class', None)
if not adapter_class: if not adapter_class:
raise serializers.ValidationError("Define adapter_class in view") raise serializers.ValidationError("Define adapter_class in view")
adapter = adapter_class(request) adapter = adapter_class(request)
app = adapter.get_provider().get_app(request) app = adapter.get_provider().get_app(request)
access_token = attrs.get('access_token') access_token = attrs.get('access_token')
token_secret = attrs.get('token_secret') token_secret = attrs.get('token_secret')
request.session['oauth_api.twitter.com_access_token'] = { request.session['oauth_api.twitter.com_access_token'] = {
'oauth_token': access_token, 'oauth_token': access_token,
'oauth_token_secret': token_secret, 'oauth_token_secret': token_secret,
} }
token = SocialToken(token=access_token, token_secret=token_secret) token = SocialToken(token=access_token, token_secret=token_secret)
token.app = app token.app = app
try: try:
login = self.get_social_login(adapter, app, token, access_token) login = self.get_social_login(adapter, app, token, access_token)
complete_social_login(request, login) complete_social_login(request, login)
except OAuthError as e: except OAuthError as e:
raise serializers.ValidationError(str(e)) raise serializers.ValidationError(str(e))
if not login.is_existing: if not login.is_existing:
login.lookup() login.lookup()
login.save(request, connect=True) login.save(request, connect=True)
attrs['user'] = login.account.user attrs['user'] = login.account.user
return attrs return attrs
class TwitterConnectSerializer(SocialConnectMixin, TwitterLoginSerializer): class TwitterConnectSerializer(SocialConnectMixin, TwitterLoginSerializer):
pass pass