From d003e759febb26e8364aa99fa7dde2dda7b328de Mon Sep 17 00:00:00 2001
From: Chris Li <c58li@uwaterloo.ca>
Date: Tue, 7 Feb 2023 13:22:00 -0500
Subject: [PATCH] save intermeidate things.

---
 core/api/auth.py                              | 33 ++++++-
 .../serializers/appleSocialLoginSerializer.py | 98 +++++++++++++++++++
 core/urls.py                                  |  5 +-
 ece651_backend/settings.py                    | 51 +++++++++-
 ece651_backend/urls.py                        |  2 +
 env.yml                                       | 12 +++
 environment.yml                               |  3 +
 7 files changed, 201 insertions(+), 3 deletions(-)
 create mode 100644 core/serializers/appleSocialLoginSerializer.py
 create mode 100644 env.yml

diff --git a/core/api/auth.py b/core/api/auth.py
index 56120f1..bb1eb83 100644
--- a/core/api/auth.py
+++ b/core/api/auth.py
@@ -1,10 +1,18 @@
 from django.shortcuts import render
+from django.urls import reverse
 from rest_framework import generics
 from rest_framework.response import Response
+from dj_rest_auth.registration.views import SocialConnectView, SocialLoginView
+from dj_rest_auth.social_serializers import TwitterLoginSerializer
 from knox.models import AuthToken
-from django.contrib.auth.backends import AllowAllUsersModelBackend
+from allauth.socialaccount.providers.apple.client import AppleOAuth2Client
+from allauth.socialaccount.providers.apple.views import AppleOAuth2Adapter
+from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
+from allauth.socialaccount.providers.twitter.views import TwitterOAuthAdapter
+from allauth.socialaccount.providers.oauth2.client import OAuth2Client
 
 from core.serializers.login import LoginSerializer
+from core.serializers.appleSocialLoginSerializer import AppleSocialLoginSerializer
 from core.serializers.register import RegisterSerializer
 from core.serializers.user import UserSerializer
 
@@ -39,6 +47,29 @@ class LoginAPI(generics.GenericAPIView):
         })
 
 
+class AppleLogin(SocialLoginView):
+    adapter_class = AppleOAuth2Adapter
+    client_class = AppleOAuth2Client
+
+    @property
+    def callback_url(self):
+        return self.request.build_absolute_uri(reverse('apple_callback'))
+
+
+class GoogleLogin(SocialLoginView): # if you want to use Authorization Code Grant, use this
+    adapter_class = GoogleOAuth2Adapter
+    client_class = OAuth2Client
+
+    @property
+    def callback_url(self):
+        return self.request.build_absolute_uri(reverse('google_callback'))
+
+
+class TwitterLogin(SocialLoginView):
+    serializer_class = TwitterLoginSerializer
+    adapter_class = TwitterOAuthAdapter
+
+
 def verify_user_and_activate(request, token):
     try:
         auth = AuthToken.objects.filter(digest=token).first()
diff --git a/core/serializers/appleSocialLoginSerializer.py b/core/serializers/appleSocialLoginSerializer.py
new file mode 100644
index 0000000..664b983
--- /dev/null
+++ b/core/serializers/appleSocialLoginSerializer.py
@@ -0,0 +1,98 @@
+from dj_rest_auth.registration.serializers import SocialLoginSerializer
+from django.contrib.auth import get_user_model
+from django.utils.translation import gettext_lazy as _
+from rest_framework import serializers
+from requests.exceptions import HTTPError
+
+try:
+    from allauth.account import app_settings as allauth_settings
+    from allauth.socialaccount.helpers import complete_social_login
+except ImportError:
+    raise ImportError('allauth needs to be added to INSTALLED_APPS.')
+
+
+class AppleSocialLoginSerializer(SocialLoginSerializer):
+    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(request)
+        app = adapter.get_provider().get_app(request)
+
+        # More info on code vs access_token
+        # http://stackoverflow.com/questions/8666316/facebook-oauth-2-0-code-and-token
+
+        # Case 1: We received the access_token
+        if attrs.get('access_token'):
+            access_token = attrs.get('access_token')
+            token = {'access_token': access_token}
+
+        # Case 2: We received the authorization code
+        elif attrs.get('code'):
+            self.callback_url = getattr(view, 'callback_url', None)
+            self.client_class = getattr(view, 'client_class', None)
+
+            if not self.callback_url:
+                raise serializers.ValidationError(
+                    _('Define callback_url in view')
+                )
+            if not self.client_class:
+                raise serializers.ValidationError(
+                    _('Define client_class in view')
+                )
+
+            code = attrs.get('code')
+
+            provider = adapter.get_provider()
+            scope = provider.get_scope(request)
+            client = self.client_class(
+                request,
+                app.client_id,
+                app.secret,
+                adapter.access_token_method,
+                adapter.access_token_url,
+                self.callback_url,
+                scope,
+                key=app.key,
+                cert=app.cert,
+            )
+            token = client.get_access_token(code)
+            access_token = token['access_token']
+
+        else:
+            raise serializers.ValidationError(
+                _('Incorrect input. access_token or code is required.'))
+
+        social_token = adapter.parse_token(token)  # The important change is here.
+        social_token.app = app
+
+        try:
+            login = self.get_social_login(adapter, app, social_token, access_token)
+            complete_social_login(request, login)
+        except HTTPError:
+            raise serializers.ValidationError(_('Incorrect value'))
+
+        if not login.is_existing:
+            # We have an account already signed up in a different flow
+            # with the same email address: raise an exception.
+            # This needs to be handled in the frontend. We can not just
+            # link up the accounts due to security constraints
+            if allauth_settings.UNIQUE_EMAIL:
+                # Do we have an account already with this email address?
+                if get_user_model().objects.filter(email=login.user.email).exists():
+                    raise serializers.ValidationError(_('E-mail already registered using different signup method.'))
+
+            login.lookup()
+            login.save(request, connect=True)
+
+        attrs['user'] = login.account.user
+        return attrs
diff --git a/core/urls.py b/core/urls.py
index 6258c45..a60ca09 100644
--- a/core/urls.py
+++ b/core/urls.py
@@ -2,7 +2,7 @@ from django.urls import path, include
 from knox import views as knox_views
 from rest_framework import routers
 
-from core.api.auth import RegisterAPI, LoginAPI, verify_user_and_activate
+from core.api.auth import RegisterAPI, LoginAPI, AppleLogin, GoogleLogin, TwitterLogin, verify_user_and_activate
 from core.api.password import ChangePasswordView
 from core.api.profile import ProfileViewSet
 
@@ -16,6 +16,9 @@ urlpatterns += [
     path('api/auth/activate/<token>', verify_user_and_activate, name='activate'),
     path('api/auth/login', LoginAPI.as_view(), name='login'),
     path('api/auth/logout', knox_views.LogoutView.as_view(), name='logout'),
+    path('api/auth/apple', AppleLogin.as_view(), name='apple_login'),
+    path('api/auth/google', GoogleLogin.as_view(), name='google_login'),
+    path('api/auth/twitter', TwitterLogin.as_view(), name='twitter_login'),
     # passwd
     path('api/change-password', ChangePasswordView.as_view(), name='change-password'),
     path('api/password_reset/', include('django_rest_passwordreset.urls', namespace='password_reset')),
diff --git a/ece651_backend/settings.py b/ece651_backend/settings.py
index a8ddd3e..48b555b 100644
--- a/ece651_backend/settings.py
+++ b/ece651_backend/settings.py
@@ -36,19 +36,31 @@ INSTALLED_APPS = [
     'django.contrib.auth',
     'django.contrib.contenttypes',
     'django.contrib.sessions',
+    'django.contrib.sites',
     'django.contrib.messages',
     'django.contrib.staticfiles',
     'django_extensions',
     'rest_framework',
     'django_rest_passwordreset',
     'knox',
+    'dj_rest_auth',
+    'dj_rest_auth.registration',
+    'allauth',
+    'allauth.account',
+    'allauth.socialaccount',
+    'allauth.socialaccount.providers.apple',
+    'allauth.socialaccount.providers.google',
+    'allauth.socialaccount.providers.twitter',
     'core',
 ]
 
+SITE_ID = 1
+
 REST_FRAMEWORK = {
     'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
     'DEFAULT_AUTHENTICATION_CLASSES': [
         'knox.auth.TokenAuthentication',
+        'dj_rest_auth.jwt_auth.JWTCookieAuthentication',
     ],
 }
 
@@ -57,6 +69,8 @@ REST_KNOX = {
   'AUTO_REFRESH': True,
 }
 
+REST_USE_JWT = True
+
 MIDDLEWARE = [
     'django.middleware.security.SecurityMiddleware',
     'django.contrib.sessions.middleware.SessionMiddleware',
@@ -129,7 +143,7 @@ TIME_ZONE = 'America/Toronto'
 
 USE_I18N = True
 
-USE_TZ = True
+USE_TZ = False
 
 
 # Static files (CSS, JavaScript, Images)
@@ -148,3 +162,38 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
 # Media Settings
 MEDIA_URL = 'media/'
 MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
+
+SOCIALACCOUNT_PROVIDERS = {
+    "apple": {
+        "APP": {
+            # Your service identifier.
+            "client_id": "your.service.id",
+
+            # The Key ID (visible in the "View Key Details" page).
+            "secret": "KEYID",
+
+             # Member ID/App ID Prefix -- you can find it below your name
+             # at the top right corner of the page, or it’s your App ID
+             # Prefix in your App ID.
+            "key": "MEMAPPIDPREFIX",
+
+            # The certificate you downloaded when generating the key.
+            "certificate_key": """-----BEGIN PRIVATE KEY-----
+s3cr3ts3cr3ts3cr3ts3cr3ts3cr3ts3cr3ts3cr3ts3cr3ts3cr3ts3cr3ts3cr
+3ts3cr3ts3cr3ts3cr3ts3cr3ts3cr3ts3cr3ts3cr3ts3cr3ts3cr3ts3cr3ts3
+c3ts3cr3t
+-----END PRIVATE KEY-----
+"""
+        }
+    },
+    'google': {
+        'SCOPE': [
+            'profile',
+            'email',
+        ],
+        'AUTH_PARAMS': {
+            'access_type': 'online',
+        },
+        'OAUTH_PKCE_ENABLED': True,
+    }
+}
diff --git a/ece651_backend/urls.py b/ece651_backend/urls.py
index bbc9de7..bae7a3a 100644
--- a/ece651_backend/urls.py
+++ b/ece651_backend/urls.py
@@ -11,6 +11,8 @@ urlpatterns = router.urls
 
 urlpatterns += [
     path('', include('core.urls')),
+    # path('dj-rest-auth/', include('dj_rest_auth.urls')),
+    # path('dj-rest-auth/registration/', include('dj_rest_auth.registration.urls')),
     path('admin/', admin.site.urls),
     path('docs/', include_docs_urls(title='ECE651 Backend API Document', description='This API document includes all ednpoints that has been implemented.')),
 ]
diff --git a/env.yml b/env.yml
new file mode 100644
index 0000000..0250500
--- /dev/null
+++ b/env.yml
@@ -0,0 +1,12 @@
+name: django
+channels:
+  - defaults
+dependencies:
+  - python=3.9
+  - django
+  - pyjwt
+  - cryptography
+  - djangorestframework
+  - django-extensions
+  - pillow
+prefix: /Users/lichenyang/opt/anaconda3/envs/django
diff --git a/environment.yml b/environment.yml
index fbcd5ab..1974a01 100644
--- a/environment.yml
+++ b/environment.yml
@@ -38,3 +38,6 @@ dependencies:
     - pygments
     - django-filter
     - django-guardian
+    - dj-rest-auth
+    - django-allauth
+    - djangorestframework-simplejwt
-- 
GitLab