diff --git a/.gitignore b/.gitignore index ea8e861..0cc78b3 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ reload reset __krud/ celerybeat-schedule +private/ \ No newline at end of file diff --git a/api/auth.py b/api/auth.py index b834b6d..743d117 100644 --- a/api/auth.py +++ b/api/auth.py @@ -1,116 +1,165 @@ -from __future__ import unicode_literals - import datetime +import json from calendar import timegm from urllib.parse import parse_qsl import requests +from allauth.socialaccount import models as aamodels +from requests_oauthlib import OAuth1 +from rest_framework import parsers, renderers from rest_framework import status from rest_framework.authtoken.models import Token from rest_framework.authtoken.serializers import AuthTokenSerializer -from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.views import APIView -from rest_framework import parsers, renderers from rest_framework_jwt.settings import api_settings from rest_framework_jwt.utils import jwt_payload_handler, jwt_encode_handler -from social.apps.django_app.utils import psa from dss import settings +from spa.models import UserProfile +from spa.models.socialaccountlink import SocialAccountLink -@psa() -def auth_by_token(request, backend, auth_token): - """Decorator that creates/authenticates a user with an access_token""" - - user = request.backend.do_auth( - access_token=auth_token - ) - if user: - return user - else: - return None - - -def get_access_token(request, backend): +def _temp_reverse_user(uid, provider, access_token, access_token_secret, payload): """ - Tries to get the access token from an OAuth Provider - :param request: - :param backend: - :return: + Do some magic here to find user account and deprecate psa + 1. Look for account in """ - access_token_url = '' - secret = '' - - if backend == 'facebook': - access_token_url = 'https://graph.facebook.com/oauth/access_token' - secret = settings.SOCIAL_AUTH_FACEBOOK_SECRET - if backend == 'twitter': - access_token_url = 'https://api.twitter.com/oauth/request_token' - secret = settings.SOCIAL_AUTH_TWITTER_SECRET - - params = { - 'client_id': request.data.get('clientId'), - 'redirect_uri': request.data.get('redirectUri'), - 'client_secret': secret, - 'code': request.data.get('code') - } - - # Step 1. Exchange authorization code for access token. - r = requests.get(access_token_url, params=params) + user = None try: - access_token = dict(parse_qsl(r.text))['access_token'] - except KeyError: - access_token = 'FAILED' - return access_token + sa = SocialAccountLink.objects.get(social_id=uid) + sa.type = provider + sa.social_id = uid + sa.access_token = access_token + sa.access_token_secret = access_token_secret + sa.provider_data = payload + sa.save() + user = UserProfile.objects.get(id=sa.user.id) + except SocialAccountLink.DoesNotExist: + # try allauth + try: + aa = aamodels.SocialAccount.objects.get(uid=uid) + try: + user = UserProfile.objects.get(user__id=aa.user_id) + except UserProfile.DoesNotExist: + print('Need to create UserProfile') + # we got an allauth, create the SocialAccountLink + sa = SocialAccountLink() + sa.user = user + sa.social_id = aa.uid + sa.type = aa.provider + sa.access_token = access_token + sa.access_token_secret = access_token_secret + sa.provider_data = payload + sa.save() + except aamodels.SocialAccount.DoesNotExist: + print('Need to create social model') + + return user if user else None class SocialLoginHandler(APIView): """View to authenticate users through social media.""" permission_classes = (AllowAny,) - def get(self, request, format=None): - pass + def post(self, request): + uid = None + backend = request.query_params.get('backend') + user = None + if backend in ['twitter']: + request_token_url = 'https://api.twitter.com/oauth/request_token' + access_token_url = 'https://api.twitter.com/oauth/access_token' + access_token = "" + access_token_secret = "" + if request.data.get('oauth_token') and request.data.get('oauth_verifier'): + auth = OAuth1(settings.SOCIAL_AUTH_TWITTER_KEY, + client_secret=settings.SOCIAL_AUTH_TWITTER_SECRET, + resource_owner_key=request.data.get('oauth_token'), + verifier=request.data.get('oauth_verifier')) + r = requests.post(access_token_url, auth=auth) + profile = dict(parse_qsl(r.text)) + payload = json.dumps(profile) + uid = profile.get('user_id') + access_token = profile.get('oauth_token') + access_token_secret = profile.get('oauth_token_secret') + user = _temp_reverse_user(uid, 'twitter', access_token, access_token_secret, payload) + else: + oauth = OAuth1(settings.SOCIAL_AUTH_TWITTER_KEY, + client_secret=settings.SOCIAL_AUTH_TWITTER_SECRET, + callback_uri=settings.TWITTER_CALLBACK_URL) + r = requests.post(request_token_url, auth=oauth) + access_token = dict(parse_qsl(r.text)) + return Response(access_token) - def post(self, request, format=None): - backend = request.query_params.get(u'backend', None) - auth_token = get_access_token(request, backend) - if auth_token and backend: - try: - # Try to authenticate the user using python-social-auth - user = auth_by_token(request, backend, auth_token) - except Exception: - return Response({'status': 'Bad request', - 'message': 'Could not authenticate with the provided token.'}, - status=status.HTTP_400_BAD_REQUEST) - if user: - if not user.is_active: - return Response({'status': 'Unauthorized', - 'message': 'The user account is disabled.'}, status=status.HTTP_401_UNAUTHORIZED) + elif backend in ['facebook']: + access_token_url = 'https://graph.facebook.com/v2.3/oauth/access_token' + graph_api_url = 'https://graph.facebook.com/v2.3/me' + access_token = "" + access_token_secret = "" + params = { + 'client_id': request.data.get('clientId'), + 'redirect_uri': request.data.get('redirectUri'), + 'client_secret': settings.SOCIAL_AUTH_FACEBOOK_SECRET, + 'code': request.data.get('code') + } - # This is the part that differs from the normal python-social-auth implementation. - # Return the JWT instead. + # Step 1. Exchange authorization code for access token. + r = requests.get(access_token_url, params=params) + token = json.loads(r.text) - # Get the JWT payload for the user. - payload = jwt_payload_handler(user) + # Step 2. Retrieve information about the current user. + r = requests.get(graph_api_url, params=token) + profile = json.loads(r.text) + access_token = token.get('access_token') + uid = profile.get('id') + user = _temp_reverse_user(uid, 'facebook', access_token, access_token_secret, r.text) + elif backend in ['google']: + access_token_url = 'https://accounts.google.com/o/oauth2/token' + people_api_url = 'https://www.googleapis.com/plus/v1/people/me/openIdConnect' + access_token = "" + access_token_secret = "" + payload = dict(client_id=request.data.get('clientId'), + redirect_uri=request.data.get('redirectUri'), + client_secret=settings.SOCIAL_AUTH_GOOGLE_OAUTH_SECRET, + code=request.data.get('code'), + grant_type='authorization_code') - # Include original issued at time for a brand new token, - # to allow token refresh - if api_settings.JWT_ALLOW_REFRESH: - payload['orig_iat'] = timegm( - datetime.datetime.utcnow().utctimetuple() - ) + # Step 1. Exchange authorization code for access token. + r = requests.post(access_token_url, data=payload) + token = json.loads(r.text) + headers = {'Authorization': 'Bearer {0}'.format(token['access_token'])} - # Create the response object with the JWT payload. - response_data = { - 'token': jwt_encode_handler(payload) - } + # Step 2. Retrieve information about the current user. + r = requests.get(people_api_url, headers=headers) + profile = json.loads(r.text) + uid = profile.get('sub') + user = _temp_reverse_user(uid, 'google', access_token, access_token_secret, r.text) - return Response(response_data) - else: - return Response({'status': 'Bad request', - 'message': 'Authentication could not be performed with received data.'}, - status=status.HTTP_400_BAD_REQUEST) + if uid is not None and user is not None: + if not user.user.is_active: + return Response({ + 'status': 'Unauthorized', + 'message': 'User account disabled' + }, status=status.HTTP_401_UNAUTHORIZED) + + payload = jwt_payload_handler(user.user) + if api_settings.JWT_ALLOW_REFRESH: + payload['orig_iat'] = timegm( + datetime.datetime.utcnow().utctimetuple() + ) + + response_data = { + 'token': jwt_encode_handler(payload), + 'session': user.get_session_id() + } + + return Response(response_data) + + return Response({ + 'status': 'Bad request', + 'message': 'Authentication could not be performed with received data.' + }, status=status.HTTP_400_BAD_REQUEST) class ObtainUser(APIView): @@ -127,11 +176,11 @@ class ObtainUser(APIView): def get(self, request): if request.user.is_authenticated(): return Response( - status=status.HTTP_200_OK, data={ - 'id': request.user.id, - 'name': request.user.username, - 'slug': request.user.userprofile.slug, - 'userRole': 'user' - }) + status=status.HTTP_200_OK, data={ + 'id': request.user.id, + 'name': request.user.username, + 'slug': request.user.userprofile.slug, + 'userRole': 'user' + }) else: return Response(status=status.HTTP_401_UNAUTHORIZED) diff --git a/api/urls.py b/api/urls.py index 11a8a69..a9c3a9d 100755 --- a/api/urls.py +++ b/api/urls.py @@ -59,8 +59,8 @@ urlpatterns = patterns( url(r'_search/$', views.SearchResultsView.as_view()), url(r'^', include(router.urls)), - url(r'^_login', SocialLoginHandler.as_view()), - url(r'^_a', SocialLoginHandler.as_view()), + url(r'^_login/?$', SocialLoginHandler.as_view()), + url(r'^_a?$', SocialLoginHandler.as_view()), url(r'^token-refresh/', 'rest_framework_jwt.views.refresh_jwt_token'), url(r'^__u/checkslug', helpers.UserSlugCheckHelper.as_view()), diff --git a/dss/localsettings.py b/dss/localsettings.py index f52a26b..ec4fc83 100644 --- a/dss/localsettings.py +++ b/dss/localsettings.py @@ -1,8 +1,6 @@ import os import ast -print("Importing local settings") - DEBUG = ast.literal_eval(os.environ.get('IS_DEBUG', 'True')) DSS_TEMP_PATH = os.environ.get('DSS_TEMP_PATH', '/tmp/') @@ -40,11 +38,16 @@ RADIO_PORT = os.environ.get('RADIO_PORT', 8888) MANDRILL_API_KEY = os.environ.get('MANDRILL_API_KEY', '') +FACEBOOK_API_VERSION = os.environ.get('FACEBOOK_API_VERSION', '2.5') +GOOGLE_CREDENTIALS = os.environ.get('GOOGLE_CREDENTIALS', + '/home/fergalm/Dropbox/development/deepsouthsounds.com/dss.api/googleapikey.json') SOCIAL_AUTH_FACEBOOK_KEY = os.environ.get('SOCIAL_AUTH_FACEBOOK_KEY', '') SOCIAL_AUTH_FACEBOOK_SECRET = os.environ.get('SOCIAL_AUTH_FACEBOOK_SECRET', '') SOCIAL_AUTH_TWITTER_KEY = os.environ.get('SOCIAL_AUTH_TWITTER_KEY', '') SOCIAL_AUTH_TWITTER_SECRET = os.environ.get('SOCIAL_AUTH_TWITTER_SECRET', '') +TWITTER_CALLBACK_URL = os.environ.get('TWITTER_CALLBACK_URL', + 'http://ext-test.deepsouthsounds.com/_login/?backend=twitter') SOCIAL_AUTH_GOOGLE_OAUTH_KEY = os.environ.get('SOCIAL_AUTH_GOOGLE_OAUTH_KEY', '') SOCIAL_AUTH_GOOGLE_OAUTH_SECRET = os.environ.get('SOCIAL_AUTH_GOOGLE_OAUTH_SECRET', '') diff --git a/dss/settings.py b/dss/settings.py index 0c7b3ed..2e41ca4 100755 --- a/dss/settings.py +++ b/dss/settings.py @@ -6,18 +6,21 @@ from django.core.urlresolvers import reverse_lazy from django.conf import global_settings from dss import storagesettings from utils import here -from dss.localsettings import * + from dss.storagesettings import * from dss.paymentsettings import * from dss.logsettings import * from dss.pipelinesettings import * from dss.psa import * from dss.celerysettings import * +from dss.localsettings import * DEVELOPMENT = DEBUG +# AUTH_USER_MODEL = 'spa.UserProfile' + TEMPLATE_DEBUG = DEBUG -VERSION = '2.13.04' +VERSION = '3.0.1' ADMINS = ( ('Fergal Moran', 'fergal.moran@gmail.com'), @@ -83,6 +86,7 @@ MIDDLEWARE_CLASSES = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', + # 'spa.middleware.auth.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'corsheaders.middleware.CorsMiddleware', diff --git a/dss/storagesettings.py b/dss/storagesettings.py index 6d59c4c..41084d2 100644 --- a/dss/storagesettings.py +++ b/dss/storagesettings.py @@ -1,8 +1,6 @@ from dss import localsettings import os -print("Importing storage settings") - AZURE_ACCOUNT_NAME = os.environ.get('CDN_NAME', 'dsscdn2') AZURE_CONTAINER = 'media' AZURE_ACCOUNT_KEY = localsettings.AZURE_ACCOUNT_KEY diff --git a/requirements.txt b/requirements.txt index 93fe9c7..20fbab7 100755 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,10 @@ django-grappelli==2.5.7 django-model_utils redis git+git://github.com/llazzaro/django-scheduler.git#django-scheduler + +git+git://github.com/pythonforfacebook/facebook-sdk.git#facebook-sdk +google-api-python-client + django-celery django-scheduler django-recurrence @@ -24,7 +28,7 @@ sorl-thumbnail git+git://github.com/disqus/django-bitfield.git#django-bitfield git+git://github.com/tschellenbach/Django-facebook.git#django-facebook -git+git://github.com/omab/python-social-auth.git#egg=python-social-auth +git+git://github.com/omab/python-social-auth.git#python-social-auth django-allauth apache-libcloud mandrill @@ -46,4 +50,6 @@ ipython ipdb beautifulsoup4 django-pipeline -django-pipeline-forgiving \ No newline at end of file +django-pipeline-forgiving + +requests-oauthlib \ No newline at end of file diff --git a/spa/middleware/auth.py b/spa/middleware/auth.py new file mode 100644 index 0000000..19ba23c --- /dev/null +++ b/spa/middleware/auth.py @@ -0,0 +1,13 @@ +from django.contrib import auth + + +class AuthenticationMiddleware(object): + def process_request(self, request): + user_cookie_name = "session_key" + if user_cookie_name not in request.COOKIES: + # log user out if you want + return + id = request.COOKIES.get(user_cookie_name) + # this will find the right backend + user = auth.authenticate(id) + request.user = user diff --git a/spa/migrations/0021_auto_20151112_2017.py b/spa/migrations/0021_auto_20151112_2017.py index f2399b7..46f8b5b 100644 --- a/spa/migrations/0021_auto_20151112_2017.py +++ b/spa/migrations/0021_auto_20151112_2017.py @@ -23,6 +23,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='mix', name='is_private', - field=models.BooleanField(default=True), + field=models.BooleanField(default=False), ), ] diff --git a/spa/migrations/0023_socialaccountlink.py b/spa/migrations/0023_socialaccountlink.py new file mode 100644 index 0000000..be51623 --- /dev/null +++ b/spa/migrations/0023_socialaccountlink.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + dependencies = [ + ('spa', '0022_auto_20151112_2017'), + ] + + operations = [ + migrations.CreateModel( + name='SocialAccountLink', + fields=[ + ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)), + ('object_created', models.DateTimeField(auto_now_add=True)), + ('object_updated', models.DateTimeField(db_index=True, auto_now=True)), + ('type', models.CharField(max_length=30, choices=[('twitter', 'Twitter'), ('facebook', 'Facebook'), + ('google', 'Google')])), + ('social_id', models.CharField(max_length=150)), + ('user', models.ForeignKey(to='spa.UserProfile', related_name='social_accounts')), + ], + options={ + 'abstract': False, + }, + ) + ] diff --git a/spa/migrations/0024_auto_20160121_2029.py b/spa/migrations/0024_auto_20160121_2029.py new file mode 100644 index 0000000..2fabb10 --- /dev/null +++ b/spa/migrations/0024_auto_20160121_2029.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('spa', '0023_socialaccountlink'), + ] + + operations = [ + migrations.AddField( + model_name='socialaccountlink', + name='access_token', + field=models.CharField(max_length=500, null=True, blank=True), + ), + migrations.AddField( + model_name='socialaccountlink', + name='access_token_secret', + field=models.CharField(max_length=500, null=True, blank=True), + ), + ] diff --git a/spa/migrations/0025_socialaccountlink_provider_data.py b/spa/migrations/0025_socialaccountlink_provider_data.py new file mode 100644 index 0000000..c793c81 --- /dev/null +++ b/spa/migrations/0025_socialaccountlink_provider_data.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('spa', '0024_auto_20160121_2029'), + ] + + operations = [ + migrations.AddField( + model_name='socialaccountlink', + name='provider_data', + field=models.CharField(blank=True, null=True, max_length=2000), + ), + ] diff --git a/spa/models/__init__.py b/spa/models/__init__.py index 221098c..4378458 100755 --- a/spa/models/__init__.py +++ b/spa/models/__init__.py @@ -1,5 +1,6 @@ from .basemodel import BaseModel from .userprofile import UserProfile +from .socialaccountlink import SocialAccountLink from .chatmessage import ChatMessage from .comment import Comment from .venue import Venue diff --git a/spa/models/socialaccountlink.py b/spa/models/socialaccountlink.py new file mode 100644 index 0000000..518c620 --- /dev/null +++ b/spa/models/socialaccountlink.py @@ -0,0 +1,60 @@ +import json +import logging +import urllib.error +import urllib.parse +import urllib.request + +import facebook +from django.core.files import File +from django.core.files.temp import NamedTemporaryFile +from django.db import models +from requests_oauthlib import OAuth1Session + +from dss import settings +from spa.models import BaseModel +from spa.models.userprofile import UserProfile + +logger = logging.getLogger(__name__) + +class SocialAccountLink(BaseModel): + ACCOUNT_TYPE = ( + ('twitter', 'Twitter'), + ('facebook', 'Facebook'), + ('google', 'Google') + ) + type = models.CharField(max_length=30, choices=ACCOUNT_TYPE) + social_id = models.CharField(max_length=150) + user = models.ForeignKey(UserProfile, related_name='social_accounts') + access_token = models.CharField(max_length=500, null=True, blank=True) + access_token_secret = models.CharField(max_length=500, null=True, blank=True) + provider_data = models.CharField(max_length=2000, null=True, blank=True) + + def _save_image(self, url): + + img = NamedTemporaryFile(delete=True) + img.write(urllib.request.urlopen(url).read()) + + img.flush() + self.user.avatar_image.save(str(self.user.id), File(img)) + + def update_image_url(self): + try: + if self.type in ['twitter']: + twitter = OAuth1Session( + settings.SOCIAL_AUTH_TWITTER_KEY, + client_secret=settings.SOCIAL_AUTH_TWITTER_SECRET, + resource_owner_key=self.access_token, + resource_owner_secret=self.access_token_secret) + url = 'https://api.twitter.com/1.1/users/show.json?user_id={0}'.format(self.social_id) + response = twitter.get(url) + r = json.loads(response.text) + self._save_image(r.get('profile_image_url')) + elif self.type in ['facebook']: + graph = facebook.GraphAPI(access_token=self.access_token, version=settings.FACEBOOK_API_VERSION) + image_url = graph.get_object("me/picture?type=large") + self._save_image(image_url.get('url')) + elif self.type in ['google']: + data = json.loads(self.provider_data) + self._save_image(data.get('picture')) + except Exception as ex: + logger.exception(ex) diff --git a/spa/signals.py b/spa/signals.py index 8952a15..2756d6c 100755 --- a/spa/signals.py +++ b/spa/signals.py @@ -7,11 +7,11 @@ from django.contrib.auth.models import User from core.realtime import activity from core.utils.audio.mp3 import mp3_length +from spa.models import SocialAccountLink from spa.models.activity import ActivityFollow from spa.models.userprofile import UserProfile from spa.models.mix import Mix - waveform_generated_signal = Signal() @@ -136,3 +136,8 @@ def user_followers_changed(sender, **kwargs): ActivityFollow(user=source_user, to_user=target_user).save() except Exception as ex: print("Error sending new follower: %s" % ex) + +@receiver(post_save, sender=SocialAccountLink, dispatch_uid='socialaccountlink_pre_save') +def socialaccountlink_pre_save(sender, **kwargs): + # update image url + kwargs['instance'].update_image_url()