Пример #1
0
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))
Пример #2
0
    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)
Пример #3
0
    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)
Пример #4
0
 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)
Пример #5
0
    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
Пример #6
0
    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)
Пример #7
0
 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)
Пример #8
0
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)
Пример #9
0
 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)
Пример #10
0
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)
Пример #11
0
 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)
Пример #12
0
    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)
Пример #13
0
    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)
Пример #14
0
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,
        )
Пример #15
0
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()
Пример #16
0
 def dispatch(self, request):
     track_event(CATEGORY_SIGNUP_FLOW, ACTION_AUTH_STARTED, "google")
     return super().dispatch(request)