def on_user_signed_up(sender, request, user, **kwargs): """ Signal handler to be called when a given user has signed up. """ sociallogin = kwargs.get("sociallogin") if sociallogin: # If the user did the "social_auth_add" they already logged in and # all we needed to do was to "combine" their github social account # with their google social account. Or vice versa. if getattr(request, "social_auth_added", False): return track_event(CATEGORY_SIGNUP_FLOW, ACTION_PROFILE_CREATED, sociallogin.account.provider) # This puts a hint to the 'user_logged_in' signal, which'll happen # next, that the user needed to create a profile. request.signed_up = True track_event( CATEGORY_SIGNUP_FLOW, ACTION_FREE_NEWSLETTER, "opt-in" if user.is_newsletter_subscribed else "opt-out", ) if switch_is_active("welcome_email"): # only send if the user has already verified # at least one email address if user.emailaddress_set.filter(verified=True).exists(): transaction.on_commit(lambda: send_welcome_email.delay( user.pk, request.LANGUAGE_CODE))
def form_invalid(self, form): """ This is called on POST but only when the form is invalid. We're overriding this method simply to send GA events when we find an error in the username field. """ if form.errors.get("username") is not None: track_event( CATEGORY_SIGNUP_FLOW, ACTION_PROFILE_EDIT_ERROR, "username", ) is_yari_signup = self.request.session.get("yari_signup", False) if is_yari_signup: # Have to redirect instead of rendering HTML. next_url = self.request.session.get("sociallogin_next_url") next_url_prefix = self.get_next_url_prefix(self.request) params = { "next": next_url, "errors": form.errors.as_json(), } yari_signup_url = f"{next_url_prefix}/{self.request.LANGUAGE_CODE}/signup" return redirect(yari_signup_url + "?" + urlencode(params)) return super().form_invalid(form)
def dispatch(self, request): # TODO: Figure out a way to NOT trigger the "ACTION_AUTH_STARTED" when # simply following the link. We've seen far too many submissions when # curl or some browser extensions follow the link but not actually being # users who proceed "earnestly". # For now, to make a simple distinction between uses of `curl` and normal # browser clicks we check that a HTTP_REFERER is actually set and comes # from the same host as the request. # Note! This is the same in kuma.users.providers.google.KumaOAuth2LoginView # See https://github.com/mdn/kuma/issues/6759 http_referer = request.META.get("HTTP_REFERER") if http_referer: if urlparse(http_referer).netloc == request.get_host(): track_event(CATEGORY_SIGNUP_FLOW, ACTION_AUTH_STARTED, "github") next_url = get_next_redirect_url(request) # This is a temporary solution whilst Kuma needs to work for the prod # old Kuma front-end and at the same time the new redirects-based Yari. # If `allauth` decides to render the `signup` view rather than redirect # back based on the `?next=`, then that view will know this is Yari. # We can remove this hack once Kuma does 0% HTML responses and just # does redirects + JSON to back up Yari. if request.GET.get("yarisignup"): request.session["yari_signup"] = True if next_url: request.session["sociallogin_next_url"] = next_url request.session.modified = True return super(KumaOAuth2LoginView, self).dispatch(request)
def dispatch(self, request): track_event(CATEGORY_SIGNUP_FLOW, ACTION_AUTH_STARTED, "github") next_url = get_next_redirect_url(request) or reverse( "users.my_edit_page") request.session["sociallogin_next_url"] = next_url request.session.modified = True return super(KumaOAuth2LoginView, self).dispatch(request)
def save_user(self, request, sociallogin, form=None): """ Checks for an existing user (via verified email addresses within the social login object) and, if one is found, associates the incoming social account with that existing user instead of a new user. It also removes the "socialaccount_sociallogin" key from the session. If the "socialaccount_sociallogin" key remains in the session, then the user will be unable to connect a second account unless they log out and log in again. (TODO: Check if this part of the method is still needed/used. I suspect not.) """ # We have to call get_existing_user() again. The result of the earlier # call (within the is_auto_signup_allowed() method), can't be cached as # an attribute on the instance because a different instance of this # class is used when calling this method from the one used when calling # is_auto_signup_allowed(). user = get_existing_user(sociallogin) if user: # We can re-use an existing user instead of creating a new one. # Let's guarantee this user has an unusable password, just in case # we're recovering an old user that has never had this done before. user.set_unusable_password() # This associates this new social account with the existing user. sociallogin.connect(request, user) # Since the "connect" call above does not add any email addresses # from the social login that are missing from the user's current # associated set, let's add them here. add_user_email(request, user, sociallogin.email_addresses) # Now that we've successfully associated a GitHub/Google social # account with this existing user, let's delete all of the user's # associated Persona social accounts (if any). Users may have # multiple associated Persona social accounts (each identified # by a unique email address). user.socialaccount_set.filter(provider="persona").delete() # Send an event to Google Analytics that the authentication # from one provider could be added to an account created by a # *different* provider (originally). track_event( CATEGORY_SIGNUP_FLOW, ACTION_SOCIAL_AUTH_ADD, f"{sociallogin.account.provider}-added", ) # This is to help the 'users.user_signed_up' signal, which will # later kick in, to remember that we've already sent a tracking # event about the the "social auth add". request.social_auth_added = True else: user = super().save_user(request, sociallogin, form) try: del request.session["socialaccount_sociallogin"] except KeyError: # pragma: no cover pass return user
def get(self, request, *args, **kwargs): """This exists so we can squeeze in a tracking event exclusively about viewing the profile creation page. If we did it to all dispatch() it would trigger on things like submitting form, which might trigger repeatedly if the form submission has validation errors that the user has to address. """ if request.session.get("sociallogin_provider"): track_event( CATEGORY_SIGNUP_FLOW, ACTION_PROFILE_AUDIT, request.session["sociallogin_provider"], ) # This view is meant to work for both Yari and for the old Kuma- # front-end way of doing things. ...at the same time. For a slow # and gentle rollout. # Once Kuma is divorced of ever returning HTML in any form, we can # refactor this whole view function to never have to depend # on `request.session.get("yari_signup")`. is_yari_signup = request.session.get("yari_signup", False) # If this is the case, always redirect. if is_yari_signup: next_url = request.session.get("sociallogin_next_url") next_url_prefix = self.get_next_url_prefix(request) socialaccount_sociallogin = request.session.get("socialaccount_sociallogin") if not socialaccount_sociallogin: # This means first used Yari to attempt to sign in but arrived # ignored the outcomes and manually went to the Kuma signup URL. # We have to kick you out and ask you to start over. But where to? yari_signin_url = f"{next_url_prefix}/{request.LANGUAGE_CODE}/signin" return redirect(yari_signin_url) # Things that are NOT PII. safe_user_details = {} account = socialaccount_sociallogin["account"] extra_data = account["extra_data"] if extra_data.get("name"): safe_user_details["name"] = extra_data["name"] if extra_data.get("picture"): # Google OAuth2 safe_user_details["avatar_url"] = extra_data["picture"] elif extra_data.get("avatar_url"): # GitHub OAuth2 safe_user_details["avatar_url"] = extra_data["avatar_url"] params = { "next": next_url, "user_details": json.dumps(safe_user_details), "csrfmiddlewaretoken": request.META.get("CSRF_COOKIE"), "provider": account.get("provider"), } yari_signup_url = f"{next_url_prefix}/{request.LANGUAGE_CODE}/signup" return redirect(yari_signup_url + "?" + urlencode(params)) return super().get(request, *args, **kwargs)
def form_invalid(self, form): """ This is called on POST but only when the form is invalid. We're overriding this method simply to send GA events when we find an error in the username field. """ if form.errors.get("username") is not None: track_event( CATEGORY_SIGNUP_FLOW, ACTION_PROFILE_EDIT_ERROR, "username", ) return super().form_invalid(form)
def on_pre_social_login(sender, request, **kwargs): """ Signal handler to be called when a given user has at least successfully authenticated with a provider but not necessarily fully logged in on our site. For example, if the user hasn't created a profile (e.g. agreeing to terms and conditions) the 'user_logged_in' won't fire until then. """ sociallogin = kwargs.get("sociallogin") if sociallogin: track_event(CATEGORY_SIGNUP_FLOW, ACTION_AUTH_SUCCESSFUL, sociallogin.account.provider)
def get(self, request, *args, **kwargs): """This exists so we can squeeze in a tracking event exclusively about viewing the profile creation page. If we did it to all dispatch() it would trigger on things like submitting form, which might trigger repeatedly if the form submission has validation errors that the user has to address. """ if request.session.get("sociallogin_provider"): track_event( CATEGORY_SIGNUP_FLOW, ACTION_PROFILE_AUDIT, request.session["sociallogin_provider"], ) return super().get(request, *args, **kwargs)
def send_subscriptions_feedback(request): """ Sends feedback to Google Analytics. This is done on the backend to ensure that all feedback is collected, even from users with DNT or where GA is disabled. """ data = json.loads(request.body) feedback = (data.get("feedback") or "").strip() if not feedback: return HttpResponseBadRequest("no feedback") track_event(CATEGORY_MONTHLY_PAYMENTS, ACTION_SUBSCRIPTION_FEEDBACK, data["feedback"]) return HttpResponse(status=204)
def dispatch(self, request): # TODO: Figure out a way to NOT trigger the "ACTION_AUTH_STARTED" when # simply following the link. We've seen far too many submissions when # curl or some browser extensions follow the link but not actually being # users who proceed "earnestly". # For now, to make a simple distinction between uses of `curl` and normal # browser clicks we check that a HTTP_REFERER is actually set and comes # from the same host as the request. # Note! This is the same in kuma.users.providers.github.KumaOAuth2LoginView # See https://github.com/mdn/kuma/issues/6759 http_referer = request.META.get("HTTP_REFERER") if http_referer: if urlparse(http_referer).netloc == request.get_host(): track_event(CATEGORY_SIGNUP_FLOW, ACTION_AUTH_STARTED, "google") return super().dispatch(request)
def form_valid(self, form): """ We use the selected email here and reset the social logging list of email addresses before they get created. We send our welcome email via celery during complete_signup. So, we need to manually commit the user to the db for it. """ selected_email = form.cleaned_data["email"] if selected_email in self.email_addresses: data = self.email_addresses[selected_email] elif selected_email == self.default_email: data = { "email": selected_email, "verified": True, "primary": True, } else: return HttpResponseBadRequest("email not a valid choice") primary_email_address = EmailAddress( email=data["email"], verified=data["verified"], primary=True ) form.sociallogin.email_addresses = self.sociallogin.email_addresses = [ primary_email_address ] if data["verified"]: # we have to stash the selected email address here # so that no email verification is sent again # this is done by adding the email address to the session get_adapter().stash_verified_email(self.request, data["email"]) with transaction.atomic(): saved_user = form.save(self.request) if saved_user.username != form.initial["username"]: track_event( CATEGORY_SIGNUP_FLOW, ACTION_PROFILE_EDIT, "username edit", ) # This won't be needed once this view is entirely catering to Yari. self.request.session.pop("yari_signup", None) return helpers.complete_social_signup(self.request, self.sociallogin)
def form_invalid(self, form): """ This is called on POST but only when the form is invalid. We're overriding this method simply to send GA events when we find an error in the username field. """ if form.errors.get("username") is not None: track_event( CATEGORY_SIGNUP_FLOW, ACTION_PROFILE_EDIT_ERROR, "username", ) is_yari_signup = self.request.session.get("yari_signup", False) if is_yari_signup: return JsonResponse({"errors": form.errors.get_json_data()}, status=400) return super().form_invalid(form)
def on_user_logged_in(sender, request, user, **kwargs): # We've already recorded that they have signed up. No point sending one # about them logged in too. if getattr(request, "signed_up", False): return # They've already logged in through the effect of matching to an existing # profile. if getattr(request, "social_auth_added", False): return sociallogin = kwargs.get("sociallogin") if sociallogin: track_event( CATEGORY_SIGNUP_FLOW, ACTION_RETURNING_USER_SIGNIN, sociallogin.account.provider, )
def stripe_hooks(request): try: payload = json.loads(request.body) except ValueError: return HttpResponseBadRequest("Invalid JSON payload") try: event = stripe.Event.construct_from(payload, stripe.api_key) except stripe.error.StripeError: raven_client.captureException() return HttpResponseBadRequest() # Generally, for this list of if-statements, see the create_missing_stripe_webhook # function. # The list of events there ought to at least minimally match what we're prepared # to deal with here. if event.type == "invoice.payment_succeeded": payment_intent = event.data.object send_payment_received_email.delay( payment_intent.customer, request.LANGUAGE_CODE, payment_intent.created, payment_intent.invoice_pdf, ) track_event( CATEGORY_MONTHLY_PAYMENTS, ACTION_SUBSCRIPTION_CREATED, f"{settings.CONTRIBUTION_AMOUNT_USD:.2f}", ) elif event.type == "customer.subscription.deleted": obj = event.data.object for user in User.objects.filter(stripe_customer_id=obj.customer): UserSubscription.set_canceled(user, obj.id) track_event(CATEGORY_MONTHLY_PAYMENTS, ACTION_SUBSCRIPTION_CANCELED, "webhook") else: return HttpResponseBadRequest( f"We did not expect a Stripe webhook of type {event.type!r}") return HttpResponse()
def dispatch(self, request): track_event(CATEGORY_SIGNUP_FLOW, ACTION_AUTH_STARTED, "google") return super().dispatch(request)