From 4125672a5abbb4cae130b1ad2af292f178d33c23 Mon Sep 17 00:00:00 2001 From: goztrk Date: Fri, 13 Dec 2024 21:06:46 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20social-auth=20auto=20associ?= =?UTF-8?q?ate=20existing=20user?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/constants/defaults.py | 4 +- apps/accounts/social_auth_pipeline.py | 149 +++++++++++++++++++------- 2 files changed, 112 insertions(+), 41 deletions(-) diff --git a/apps/accounts/constants/defaults.py b/apps/accounts/constants/defaults.py index 344b031e..a2f95c54 100644 --- a/apps/accounts/constants/defaults.py +++ b/apps/accounts/constants/defaults.py @@ -7,7 +7,9 @@ HTK_DEFAULT_LOGGED_IN_ACCOUNT_HOME = 'account_index' HTK_ACCOUNTS_CHANGE_PASSWORD_UPDATE_SESSION_AUTH_HASH = True - +# The list of backends that will automatically link user accounts with social +# auth accounts. +HTK_ACCOUNTS_SOCIAL_AUTO_ASSOCIATE_BACKENDS = [] HTK_ACCOUNTS_REGISTER_SET_PRETTY_USERNAME_FROM_EMAIL = False HTK_ACCOUNTS_REGISTER_SOCIAL_LOGIN_URL_NAME = 'account_register_social_login' HTK_ACCOUNTS_REGISTER_SOCIAL_EMAIL_URL_NAME = 'account_register_social_email' diff --git a/apps/accounts/social_auth_pipeline.py b/apps/accounts/social_auth_pipeline.py index 2711169e..05cfdf93 100644 --- a/apps/accounts/social_auth_pipeline.py +++ b/apps/accounts/social_auth_pipeline.py @@ -1,20 +1,32 @@ # Third Party / PIP Imports -# Django Imports -from django.shortcuts import redirect - +# Third Party (PyPI) Imports # Django Extensions Imports +# Third Party (PyPI) Imports from social_core.pipeline.partial import partial +from social_core.pipeline.social_auth import associate_user + +# Django Imports +from django.shortcuts import redirect # HTK Imports from htk.apps.accounts.emails import welcome_email -from htk.apps.accounts.session_keys import * -from htk.apps.accounts.utils import associate_user_email -from htk.apps.accounts.utils import get_incomplete_signup_user_by_email -from htk.apps.accounts.utils import get_user_by_email -from htk.apps.accounts.view_helpers import redirect_to_social_auth_complete +from htk.apps.accounts.session_keys import ( + SOCIAL_AUTH_FLOW_KEYS, + SOCIAL_REGISTRATION_SETTING_AGREED_TO_TERMS, + SOCIAL_REGISTRATION_SETTING_EMAIL, + SOCIAL_REGISTRATION_SETTING_MISSING_EMAIL, +) +from htk.apps.accounts.utils import ( + associate_user_email, + get_incomplete_signup_user_by_email, + get_user_by_email, +) from htk.utils import htk_setting +# isort: off + + # Custom Pipeline Functions # https://django-social-auth.readthedocs.org/en/v0.7.22/pipeline.html # @@ -28,67 +40,111 @@ # 1. If there is no email, have the user enter an email # 2. Check association. If there is an account with that email: -# a. "An account with this email address already exists. Please log in to link your {{ SOCIAL }} account." -# b. "An account with this email address is already linked to {{ SOCIAL }}. Please create a new account using a different email address." +# a. "An account with this email address already exists. Please log in to link your +# {{ SOCIAL }} account." +# b. "An account with this email address is already linked to {{ SOCIAL }}. Please +# create a new account using a different email address." # 3. Create the account with the username and email + def python_social_auth_shim(pipeline_func): """Shim layer decorator for django-social-auth to python-social auth migration pipeline complete wasn't passing the request object, but the strategy object instead """ + def wrapped(strategy, *args, **kwargs): if not kwargs.get('request'): request = strategy.request kwargs['request'] = request return pipeline_func(*args, **kwargs) + return wrapped + def reset_session_keys(strategy, *args, **kwargs): """Reset a bunch of keys used as part of the social auth flow - This is to prevent partially-completed values from a previous flow from affecting a new social auth flow + + This is to prevent partially-completed values from a previous flow from affecting a + new social auth flow """ for key in SOCIAL_AUTH_FLOW_KEYS: if strategy.request.session.get(key): del strategy.request.session[key] return None + @partial -def check_email(strategy, details, user=None, *args, **kwargs): +def check_email(strategy, details, backend, uid, user=None, *args, **kwargs): """Ask the user to enter the email if we don't have one yet - The pipeline process was cut prior to this custom pipeline function, and will resume to this same function after completing + The pipeline process was cut prior to this custom pipeline function, and will + resume to this same function after completing the social auth flow. """ response = None if user is None: - strategy.request.session['backend'] = kwargs.get('current_partial').backend + strategy.request.session['backend'] = kwargs.get( + 'current_partial' + ).backend social_email = details.get('email') - collected_email = strategy.request.session.get(SOCIAL_REGISTRATION_SETTING_EMAIL) + collected_email = strategy.request.session.get( + SOCIAL_REGISTRATION_SETTING_EMAIL + ) if social_email: # email available from social auth user = get_user_by_email(social_email) if user and user.is_active: - # a user is already associated with this email - # TODO: there is an error with linking accounts... - strategy.request.session[SOCIAL_REGISTRATION_SETTING_EMAIL] = social_email - if user.has_usable_password(): - # user should log into the existing account with a password - url_name = htk_setting('HTK_ACCOUNTS_REGISTER_SOCIAL_LOGIN_URL_NAME') + # A user is already associated with this email + auto_associate_backends = htk_setting( + 'HTK_ACCOUNTS_SOCIAL_AUTO_ASSOCIATE_BACKENDS', [] + ) + backend_name = backend.name + if backend_name in auto_associate_backends: + # The backend is one of the auto-associate backends, so we need to + # associate the Django user with the social auth account + # This is a job for `associate_user` pipeline but the association + # must happen in here. + associate_user(backend, uid, user=user) else: - # no password was set, so user must log in with another social auth account - url_name = htk_setting('HTK_ACCOUNTS_REGISTER_SOCIAL_ALREADY_LINKED_URL_NAME') - response = redirect(url_name) + # TODO: There is an error with linking accounts... + strategy.request.session[ + SOCIAL_REGISTRATION_SETTING_EMAIL + ] = social_email + if user.has_usable_password(): + # user should log into the existing account with a password + url_name = htk_setting( + 'HTK_ACCOUNTS_REGISTER_SOCIAL_LOGIN_URL_NAME' + ) + response = redirect(url_name) + else: + # no password was set, so user must log in with another social + # auth account + url_name = htk_setting( + 'HTK_ACCOUNTS_REGISTER_SOCIAL_ALREADY_LINKED_URL_NAME' + ) + response = redirect(url_name) + else: + # no user found with this email + pass + elif collected_email: # email provided by user details['email'] = collected_email - response = { 'details' : details } + response = {'details': details} else: # no email provided from social auth - strategy.request.session[SOCIAL_REGISTRATION_SETTING_MISSING_EMAIL] = True - url_name = htk_setting('HTK_ACCOUNTS_REGISTER_SOCIAL_EMAIL_URL_NAME') + strategy.request.session[ + SOCIAL_REGISTRATION_SETTING_MISSING_EMAIL + ] = True + url_name = htk_setting( + 'HTK_ACCOUNTS_REGISTER_SOCIAL_EMAIL_URL_NAME' + ) response = redirect(url_name) + else: + pass return response + @partial def check_terms_agreement(strategy, details, user=None, *args, **kwargs): """ @@ -96,11 +152,15 @@ def check_terms_agreement(strategy, details, user=None, *args, **kwargs): """ response = None if user is None: - agreed_to_terms = strategy.request.session.get(SOCIAL_REGISTRATION_SETTING_AGREED_TO_TERMS, False) + agreed_to_terms = strategy.request.session.get( + SOCIAL_REGISTRATION_SETTING_AGREED_TO_TERMS, False + ) if not agreed_to_terms: email = details.get('email') strategy.request.session[SOCIAL_REGISTRATION_SETTING_EMAIL] = email - url_name = htk_setting('HTK_ACCOUNTS_REGISTER_SOCIAL_EMAIL_AND_TERMS_URL_NAME') + url_name = htk_setting( + 'HTK_ACCOUNTS_REGISTER_SOCIAL_EMAIL_AND_TERMS_URL_NAME' + ) response = redirect(url_name) else: pass @@ -108,19 +168,20 @@ def check_terms_agreement(strategy, details, user=None, *args, **kwargs): pass return response + def check_incomplete_signup(strategy, details, user=None, *args, **kwargs): - """Checks for an incomplete signup, and sets that User instead - """ + """Checks for an incomplete signup, and sets that User instead""" response = None if user is None: social_email = details.get('email') user = get_incomplete_signup_user_by_email(social_email) response = { - 'user' : user, - 'is_new' : user is None, + 'user': user, + 'is_new': user is None, } return response + def set_username(strategy, details, user, social, *args, **kwargs): """This pipeline function can be used to set UserProfile.has_username_set = True @@ -137,9 +198,9 @@ def set_username(strategy, details, user, social, *args, **kwargs): user_profile.save() return response + def associate_email(strategy, details, user, social, *args, **kwargs): - """Associate email with the user - """ + """Associate email with the user""" if not user or not social: return None @@ -149,19 +210,26 @@ def associate_email(strategy, details, user, social, *args, **kwargs): domain = strategy.request.get_host() # Should confirm if the email was provided by the social auth provider, not the user # i.e. SOCIAL_REGISTRATION_SETTING_MISSING_EMAIL was False - confirmed = not(strategy.request.session.get(SOCIAL_REGISTRATION_SETTING_MISSING_EMAIL, False)) - user_email = associate_user_email(user, email, domain=domain, confirmed=confirmed) + confirmed = not ( + strategy.request.session.get( + SOCIAL_REGISTRATION_SETTING_MISSING_EMAIL, False + ) + ) + user_email = associate_user_email( + user, email, domain=domain, confirmed=confirmed + ) if user_email: - # need to update the User with the activated one, so that it doesn't get overwritten later on + # need to update the User with the activated one, so that it doesn't get + # overwritten later on response = { 'user': user_email.user, } return response + def handle_new_user(user, is_new, *args, **kwargs): - """Do stuff if the account was newly created - """ + """Do stuff if the account was newly created""" if not user: return None @@ -169,6 +237,7 @@ def handle_new_user(user, is_new, *args, **kwargs): # send a welcome email to the user, regardless of email confirmation status welcome_email(user) + def post_connect(user, social, *args, **kwargs): response = None return response