Browse Source

Add new version of Stripe Checkout for donating

merge-requests/85/head
Deimos 5 years ago
parent
commit
ee934105b7
  1. 24
      salt/salt/nginx/tildes.conf.jinja2
  2. 3
      tildes/production.ini.example
  3. 10
      tildes/static/js/behaviors/stripe-checkout.js
  4. 1
      tildes/tildes/lib/ratelimit.py
  5. 4
      tildes/tildes/metrics.py
  6. 4
      tildes/tildes/routes.py
  7. 22
      tildes/tildes/templates/donate_stripe.jinja2
  8. 15
      tildes/tildes/templates/donate_success.jinja2
  9. 68
      tildes/tildes/views/donate.py

24
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; add_header Strict-Transport-Security "max-age={{ pillar['hsts_max_age'] }}; includeSubDomains; preload" always;
{% endif %} {% 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-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always; add_header X-Frame-Options "DENY" always;
add_header X-Xss-Protection "1; mode=block" 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 # add Expires+Cache-Control headers from the mime-type map defined above
expires $expires_type_map; 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 / { 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 # checks for static file, if not found proxy to app
try_files $uri @proxy_to_app; try_files $uri @proxy_to_app;
gzip_static on; gzip_static on;

3
tildes/production.ini.example

@ -34,7 +34,8 @@ webassets.manifest = json
# API keys for external APIs # API keys for external APIs
api_keys.embedly = embedlykeygoeshere 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 api_keys.youtube = youtubekeygoeshere
[server:main] [server:main]

10
tildes/static/js/behaviors/stripe-checkout.js

@ -0,0 +1,10 @@
// Copyright (c) 2019 Tildes contributors <code@tildes.net>
// 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")
});
});

1
tildes/tildes/lib/ratelimit.py

@ -281,7 +281,6 @@ class RateLimitedAction:
# the actual list of actions with rate-limit restrictions # the actual list of actions with rate-limit restrictions
# each action must have a unique name to prevent key collisions # each action must have a unique name to prevent key collisions
_RATE_LIMITED_ACTIONS = ( _RATE_LIMITED_ACTIONS = (
RateLimitedAction("donate", timedelta(hours=1), 5, max_burst=5, by_user=False),
RateLimitedAction("login", timedelta(hours=1), 20), RateLimitedAction("login", timedelta(hours=1), 20),
RateLimitedAction("login_two_factor", timedelta(hours=1), 20), RateLimitedAction("login_two_factor", timedelta(hours=1), 20),
RateLimitedAction("register", timedelta(hours=1), 50), RateLimitedAction("register", timedelta(hours=1), 50),

4
tildes/tildes/metrics.py

@ -21,8 +21,8 @@ _COUNTERS = {
"donations": Counter( "donations": Counter(
"tildes_donations_total", "Donation Attempts", labelnames=["type"] "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( "invite_code_failures": Counter(
"tildes_invite_code_failures_total", "Invite Code Failures" "tildes_invite_code_failures_total", "Invite Code Failures"

4
tildes/tildes/routes.py

@ -106,6 +106,10 @@ def includeme(config: Configurator) -> None:
# Route to expose metrics to Prometheus # Route to expose metrics to Prometheus
config.add_route("metrics", "/metrics") 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 # Add all intercooler routes under the /api/web path
with config.route_prefix_context("/api/web"): with config.route_prefix_context("/api/web"):
add_intercooler_routes(config) add_intercooler_routes(config)

22
tildes/tildes/templates/donate_stripe.jinja2

@ -5,18 +5,14 @@
{% block title %}Stripe donation{% endblock %} {% block title %}Stripe donation{% endblock %}
{% block main_heading %}
{% if payment_successful %}
Thanks for donating to Tildes!
{% else %}
Donation failed
{% endif %}
{% endblock %}
{% block content %} {% block content %}
{% if payment_successful %}
<p>You should receive an email receipt. If you have any questions, please feel free to contact <a href="mailto:donate@tildes.net">donate@tildes.net</a></p>
{% else %}
<p>The Stripe payment failed for some reason (your credit card has not been charged). Please go back to <a href="https://docs.tildes.net/donate-stripe">the donation page</a> and try again. If the payment fails again, please <a href="mailto:donate@tildes.net">send an email</a> so it can be looked into.</p>
{% endif %}
<script src="https://js.stripe.com/v3/"></script>
<p>Redirecting to Stripe...</p>
{# This div will cause the page to redirect to the Stripe Checkout page #}
<div
data-js-stripe-checkout="{{ publishable_key }}"
data-js-stripe-checkout-session="{{ session_id }}"
></div>
{% endblock %} {% endblock %}

15
tildes/tildes/templates/donate_success.jinja2

@ -0,0 +1,15 @@
{# Copyright (c) 2019 Tildes contributors <code@tildes.net> #}
{# SPDX-License-Identifier: AGPL-3.0-or-later #}
{% extends 'base_no_sidebar.jinja2' %}
{% block title %}Thanks for donating!{% endblock %}
{% block content %}
<div class="empty">
<h2 class="empty-title">Thanks for donating to Tildes!</h2>
<p class="empty-subtitle">You should receive an email receipt. If you have any questions, please feel free to contact <a href="mailto:donate@tildes.net">donate@tildes.net</a></p>
<div class="empty-action"><a href="/" class="btn btn-primary">Back to the home page</a></div>
</div>
{% endblock %}

68
tildes/tildes/views/donate.py

@ -0,0 +1,68 @@
# Copyright (c) 2018 Tildes contributors <code@tildes.net>
# 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 {}
Loading…
Cancel
Save