From ee934105b783bc83f96195ea850d5986113340f5 Mon Sep 17 00:00:00 2001 From: Deimos Date: Fri, 20 Sep 2019 12:22:52 -0600 Subject: [PATCH] Add new version of Stripe Checkout for donating --- salt/salt/nginx/tildes.conf.jinja2 | 24 ++++--- tildes/production.ini.example | 3 +- tildes/static/js/behaviors/stripe-checkout.js | 10 +++ tildes/tildes/lib/ratelimit.py | 1 - tildes/tildes/metrics.py | 4 +- tildes/tildes/routes.py | 4 ++ tildes/tildes/templates/donate_stripe.jinja2 | 22 +++--- tildes/tildes/templates/donate_success.jinja2 | 15 ++++ tildes/tildes/views/donate.py | 68 +++++++++++++++++++ 9 files changed, 124 insertions(+), 27 deletions(-) create mode 100644 tildes/static/js/behaviors/stripe-checkout.js create mode 100644 tildes/tildes/templates/donate_success.jinja2 create mode 100644 tildes/tildes/views/donate.py diff --git a/salt/salt/nginx/tildes.conf.jinja2 b/salt/salt/nginx/tildes.conf.jinja2 index 29ae9ab..9b873b5 100644 --- a/salt/salt/nginx/tildes.conf.jinja2 +++ b/salt/salt/nginx/tildes.conf.jinja2 @@ -31,16 +31,6 @@ server { add_header Strict-Transport-Security "max-age={{ pillar['hsts_max_age'] }}; includeSubDomains; preload" always; {% endif %} - # Content Security Policy: - # - "img-src data:" is needed for Spectre.css icons - set $csp_value "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; manifest-src 'self'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'"; - - {% if grains['id'] == 'dev' %} - add_header Content-Security-Policy-Report-Only $csp_value always; - {% else %} - add_header Content-Security-Policy $csp_value always; - {% endif %} - add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; add_header X-Xss-Protection "1; mode=block" always; @@ -66,7 +56,21 @@ server { # add Expires+Cache-Control headers from the mime-type map defined above expires $expires_type_map; + # Use a different Content-Security-Policy header for the donation page, to allow + # the Stripe javascript file to be loaded from their domain + location = /donate_stripe { + add_header Content-Security-Policy "default-src 'none'; script-src 'self' https://js.stripe.com; style-src 'self'; img-src 'self' data:; connect-src 'self'; manifest-src 'self'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'" always; + + try_files $uri @proxy_to_app; + gzip_static on; + } + location / { + {% if grains['id'] == 'prod' %} + # Content Security Policy - "img-src data:" is needed for Spectre.css icons + add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; manifest-src 'self'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'" always; + {% endif %} + # checks for static file, if not found proxy to app try_files $uri @proxy_to_app; gzip_static on; diff --git a/tildes/production.ini.example b/tildes/production.ini.example index 2988f99..e81fd6c 100644 --- a/tildes/production.ini.example +++ b/tildes/production.ini.example @@ -34,7 +34,8 @@ webassets.manifest = json # API keys for external APIs api_keys.embedly = embedlykeygoeshere -api_keys.stripe = sk_live_ActualKeyShouldGoHere +api_keys.stripe.publishable = pk_live_ActualKeyShouldGoHere +api_keys.stripe.secret = sk_live_ActualKeyShouldGoHere api_keys.youtube = youtubekeygoeshere [server:main] diff --git a/tildes/static/js/behaviors/stripe-checkout.js b/tildes/static/js/behaviors/stripe-checkout.js new file mode 100644 index 0000000..1e9c2a2 --- /dev/null +++ b/tildes/static/js/behaviors/stripe-checkout.js @@ -0,0 +1,10 @@ +// Copyright (c) 2019 Tildes contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +$.onmount("[data-js-stripe-checkout]", function() { + /* eslint-disable-next-line no-undef */ + var stripe = Stripe($(this).attr("data-js-stripe-checkout")); + stripe.redirectToCheckout({ + sessionId: $(this).attr("data-js-stripe-checkout-session") + }); +}); diff --git a/tildes/tildes/lib/ratelimit.py b/tildes/tildes/lib/ratelimit.py index e781027..f5572f6 100644 --- a/tildes/tildes/lib/ratelimit.py +++ b/tildes/tildes/lib/ratelimit.py @@ -281,7 +281,6 @@ class RateLimitedAction: # the actual list of actions with rate-limit restrictions # each action must have a unique name to prevent key collisions _RATE_LIMITED_ACTIONS = ( - RateLimitedAction("donate", timedelta(hours=1), 5, max_burst=5, by_user=False), RateLimitedAction("login", timedelta(hours=1), 20), RateLimitedAction("login_two_factor", timedelta(hours=1), 20), RateLimitedAction("register", timedelta(hours=1), 50), diff --git a/tildes/tildes/metrics.py b/tildes/tildes/metrics.py index 71adf54..835542b 100644 --- a/tildes/tildes/metrics.py +++ b/tildes/tildes/metrics.py @@ -21,8 +21,8 @@ _COUNTERS = { "donations": Counter( "tildes_donations_total", "Donation Attempts", labelnames=["type"] ), - "donation_failures": Counter( - "tildes_donation_failures_total", "Donation Failures", labelnames=["type"] + "donation_initiations": Counter( + "tildes_donation_initiations_total", "Donation Initiations", labelnames=["type"] ), "invite_code_failures": Counter( "tildes_invite_code_failures_total", "Invite Code Failures" diff --git a/tildes/tildes/routes.py b/tildes/tildes/routes.py index 8e5ba81..1fe056b 100644 --- a/tildes/tildes/routes.py +++ b/tildes/tildes/routes.py @@ -106,6 +106,10 @@ def includeme(config: Configurator) -> None: # Route to expose metrics to Prometheus config.add_route("metrics", "/metrics") + # Route for Stripe donation processing page (POSTed to from docs site) + config.add_route("donate_stripe", "/donate_stripe") + config.add_route("donate_success", "/donate_success") + # Add all intercooler routes under the /api/web path with config.route_prefix_context("/api/web"): add_intercooler_routes(config) diff --git a/tildes/tildes/templates/donate_stripe.jinja2 b/tildes/tildes/templates/donate_stripe.jinja2 index f9c59ec..4a0f7bf 100644 --- a/tildes/tildes/templates/donate_stripe.jinja2 +++ b/tildes/tildes/templates/donate_stripe.jinja2 @@ -5,18 +5,14 @@ {% block title %}Stripe donation{% endblock %} -{% block main_heading %} - {% if payment_successful %} - Thanks for donating to Tildes! - {% else %} - Donation failed - {% endif %} -{% endblock %} - {% block content %} - {% if payment_successful %} -

You should receive an email receipt. If you have any questions, please feel free to contact donate@tildes.net

- {% else %} -

The Stripe payment failed for some reason (your credit card has not been charged). Please go back to the donation page and try again. If the payment fails again, please send an email so it can be looked into.

- {% endif %} + + +

Redirecting to Stripe...

+ + {# This div will cause the page to redirect to the Stripe Checkout page #} +
{% endblock %} diff --git a/tildes/tildes/templates/donate_success.jinja2 b/tildes/tildes/templates/donate_success.jinja2 new file mode 100644 index 0000000..3c0f550 --- /dev/null +++ b/tildes/tildes/templates/donate_success.jinja2 @@ -0,0 +1,15 @@ +{# Copyright (c) 2019 Tildes contributors #} +{# SPDX-License-Identifier: AGPL-3.0-or-later #} + +{% extends 'base_no_sidebar.jinja2' %} + +{% block title %}Thanks for donating!{% endblock %} + +{% block content %} +
+

Thanks for donating to Tildes!

+

You should receive an email receipt. If you have any questions, please feel free to contact donate@tildes.net

+ + +
+{% endblock %} diff --git a/tildes/tildes/views/donate.py b/tildes/tildes/views/donate.py new file mode 100644 index 0000000..a35f426 --- /dev/null +++ b/tildes/tildes/views/donate.py @@ -0,0 +1,68 @@ +# Copyright (c) 2018 Tildes contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""The view for donating via Stripe.""" + +import stripe +from marshmallow.fields import Float, String +from marshmallow.validate import OneOf, Range +from pyramid.httpexceptions import HTTPInternalServerError +from pyramid.request import Request +from pyramid.security import NO_PERMISSION_REQUIRED +from pyramid.view import view_config +from webargs.pyramidparser import use_kwargs + +from tildes.metrics import incr_counter + + +@view_config( + route_name="donate_stripe", + request_method="POST", + renderer="donate_stripe.jinja2", + permission=NO_PERMISSION_REQUIRED, + require_csrf=False, +) +@use_kwargs( + { + "amount": Float(required=True, validate=Range(min=1.0)), + "currency": String(required=True, validate=OneOf(("CAD", "USD"))), + } +) +def post_donate_stripe(request: Request, amount: int, currency: str) -> dict: + """Process a Stripe donation.""" + try: + stripe.api_key = request.registry.settings["api_keys.stripe.secret"] + publishable_key = request.registry.settings["api_keys.stripe.publishable"] + except KeyError: + raise HTTPInternalServerError + + incr_counter("donation_initiations", type="stripe") + + session = stripe.checkout.Session.create( + payment_method_types=["card"], + line_items=[ + { + "name": "One-time donation - tildes.net", + "amount": int(amount * 100), + "currency": currency, + "quantity": 1, + } + ], + submit_type="donate", + success_url="https://tildes.net/donate_success", + cancel_url="https://docs.tildes.net/donate-stripe", + ) + + return {"publishable_key": publishable_key, "session_id": session.id} + + +@view_config(route_name="donate_success", renderer="donate_success.jinja2") +def get_donate_success(request: Request) -> dict: + """Display a message after a successful donation.""" + # pylint: disable=unused-argument + + # incrementing this metric on page-load and hard-coding Stripe isn't ideal, but it + # should do the job for now + incr_counter("donations", type="stripe") + + return {}