From 08e23cf0d1aeda989e833b90a6e38965cfd75fbd Mon Sep 17 00:00:00 2001 From: forest Date: Tue, 12 May 2020 12:38:36 -0500 Subject: [PATCH] stripe payment processor --- Pipfile | 1 + Pipfile.lock | 46 ++++++- capsulflask/__init__.py | 10 +- capsulflask/console.py | 2 - capsulflask/db_model.py | 33 ++++- .../02_up_accounts_vms_etc.sql | 7 ++ capsulflask/static/capsul-product-image.png | Bin 0 -> 18960 bytes capsulflask/static/style.css | 15 ++- capsulflask/stripe.py | 117 +++++++++++++++++- capsulflask/templates/account-balance.html | 4 +- capsulflask/templates/base.html | 1 + capsulflask/templates/stripe.html | 39 ++++++ 12 files changed, 264 insertions(+), 11 deletions(-) create mode 100644 capsulflask/static/capsul-product-image.png create mode 100644 capsulflask/templates/stripe.html diff --git a/Pipfile b/Pipfile index b1df5d8..2d0e3f2 100644 --- a/Pipfile +++ b/Pipfile @@ -24,6 +24,7 @@ toml = "==0.10.0" typed-ast = "==1.4.1" Werkzeug = "==1.0.1" wrapt = "==1.12.1" +stripe = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 1c18ada..062d3fe 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "9b88525881f174f421ccb5b49fc34948053fb5d9d9eafa90bdec8ef0bfbc30ea" + "sha256": "8355b0bc9024432220ab4f05b2997f827af534691520249c6ff8bb2db9014dc8" }, "pipfile-spec": 6, "requires": { @@ -31,6 +31,20 @@ "index": "pypi", "version": "==1.4" }, + "certifi": { + "hashes": [ + "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", + "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" + ], + "version": "==2020.4.5.1" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, "click": { "hashes": [ "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", @@ -62,6 +76,13 @@ "index": "pypi", "version": "==20.0.4" }, + "idna": { + "hashes": [ + "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", + "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" + ], + "version": "==2.9" + }, "isort": { "hashes": [ "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", @@ -195,6 +216,14 @@ "index": "pypi", "version": "==2.5.2" }, + "requests": { + "hashes": [ + "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", + "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" + ], + "markers": "python_version >= '3.0'", + "version": "==2.23.0" + }, "six": { "hashes": [ "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", @@ -203,6 +232,14 @@ "index": "pypi", "version": "==1.14.0" }, + "stripe": { + "hashes": [ + "sha256:515fe2cc915e639468f30150a39c162fc0fb090256ae9d6a04e5022925d136f1", + "sha256:bdbbea632b8faa983c670db61debbe0bdb5802ef98fd0613a03aa466e56cdade" + ], + "index": "pypi", + "version": "==2.48.0" + }, "toml": { "hashes": [ "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", @@ -240,6 +277,13 @@ "markers": "implementation_name == 'cpython' and python_version < '3.8'", "version": "==1.4.1" }, + "urllib3": { + "hashes": [ + "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", + "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" + ], + "version": "==1.25.9" + }, "werkzeug": { "hashes": [ "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", diff --git a/capsulflask/__init__.py b/capsulflask/__init__.py index e04396e..1a8be88 100644 --- a/capsulflask/__init__.py +++ b/capsulflask/__init__.py @@ -1,3 +1,4 @@ +import stripe import os from dotenv import load_dotenv, find_dotenv @@ -23,11 +24,15 @@ app.config.from_mapping( MAIL_PASSWORD=os.environ.get("MAIL_PASSWORD", default=""), MAIL_DEFAULT_SENDER=os.environ.get("MAIL_DEFAULT_SENDER", default="forest@nullhex.com"), - STRIPE_API_VERSION=os.environ.get("STRIPE_API_VERSION", default=""), + STRIPE_API_VERSION=os.environ.get("STRIPE_API_VERSION", default="2020-03-02"), STRIPE_SECRET_KEY=os.environ.get("STRIPE_SECRET_KEY", default=""), STRIPE_PUBLISHABLE_KEY=os.environ.get("STRIPE_PUBLISHABLE_KEY", default="") + #STRIPE_WEBHOOK_SECRET=os.environ.get("STRIPE_WEBHOOK_SECRET", default="") ) +stripe.api_key = app.config['STRIPE_SECRET_KEY'] +stripe.api_version = app.config['STRIPE_API_VERSION'] + app.config['FLASK_MAIL_INSTANCE'] = Mail(app) app.config['VIRTUALIZATION_MODEL'] = virt_model.MockVirtualization() @@ -35,11 +40,12 @@ from capsulflask import db db.init_app(app) -from capsulflask import auth, landing, console +from capsulflask import auth, landing, console, stripe app.register_blueprint(landing.bp) app.register_blueprint(auth.bp) app.register_blueprint(console.bp) +app.register_blueprint(stripe.bp) app.add_url_rule("/", endpoint="index") diff --git a/capsulflask/console.py b/capsulflask/console.py index 322b1e4..cf46c5a 100644 --- a/capsulflask/console.py +++ b/capsulflask/console.py @@ -5,8 +5,6 @@ from flask import Blueprint from flask import flash from flask import current_app from flask import g -from flask import redirect -from flask import url_for from flask import request from flask import session from flask import render_template diff --git a/capsulflask/db_model.py b/capsulflask/db_model.py index 3c8c6af..86c187f 100644 --- a/capsulflask/db_model.py +++ b/capsulflask/db_model.py @@ -23,7 +23,7 @@ class DBModel: return token def consume_token(self, token): - self.cursor.execute("SELECT email FROM login_tokens WHERE token = %s", (token, )) + self.cursor.execute("SELECT email FROM login_tokens WHERE token = %s and created > (NOW() - INTERVAL '20 min')", (token, )) rows = self.cursor.fetchall() if len(rows) > 0: email = rows[0][0] @@ -150,4 +150,35 @@ class DBModel: lambda x: dict(dollars=x[0], created=x[1]), self.cursor.fetchall() )) + + def create_stripe_checkout_session(self, id, email, dollars): + self.cursor.execute(""" + INSERT INTO stripe_checkout_sessions (id, email, dollars) + VALUES (%s, %s, %d) + """, + (id, email, dollars) + ) + self.connection.commit() + + def consume_stripe_checkout_session(self, id, dollars): + self.cursor.execute("SELECT email, dollars FROM stripe_checkout_sessions WHERE id = %s", (id,)) + rows = self.cursor.fetchall() + if len(rows) > 0: + if int(rows[0][1]) != int(dollars): + print(f""" + Stripe sent us a completed checkout session with a different dollar amount than what we had recorded!! + stripe_checkout_session_id: {id} + account: {rows[0][0]} + Recorded amount: {int(rows[0][1])} + Stripe sent: {int(dollars)} + """) + # not sure what to do here. For now just log and do nothing. + self.cursor.execute( "DELETE FROM stripe_checkout_sessions WHERE id = %s", (id,) ) + self.cursor.execute( "INSERT INTO payments (email, dollars) VALUES (%s, %d)", (rows[0][0], rows[0][1]) ) + self.connection.commit() + return rows[0][0] + else: + return None + + diff --git a/capsulflask/schema_migrations/02_up_accounts_vms_etc.sql b/capsulflask/schema_migrations/02_up_accounts_vms_etc.sql index 8c344c1..00f1ba2 100644 --- a/capsulflask/schema_migrations/02_up_accounts_vms_etc.sql +++ b/capsulflask/schema_migrations/02_up_accounts_vms_etc.sql @@ -62,6 +62,13 @@ CREATE TABLE login_tokens ( PRIMARY KEY (email, created) ); +CREATE TABLE stripe_checkout_sessions ( + id TEXT PRIMARY KEY, + email TEXT REFERENCES accounts(email) ON DELETE RESTRICT, + created TIMESTAMP NOT NULL DEFAULT NOW(), + dollars NUMERIC(8, 2) NOT NULL +); + INSERT INTO os_images (id, template_image_file_name, description) VALUES ('alpine311', 'alpine-cloud-2020-04-18.qcow2', 'Alpine Linux 3.11'), ('ubuntu18', 'ubuntu-18.04-minimal-cloudimg-amd64.img', 'Ubuntu 18.04 LTS (Bionic Beaver)'), diff --git a/capsulflask/static/capsul-product-image.png b/capsulflask/static/capsul-product-image.png new file mode 100644 index 0000000000000000000000000000000000000000..fd6dc19b8ba9c2e6c9d50cba5a388c6400689b30 GIT binary patch literal 18960 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 literal 0 HcmV?d00001 diff --git a/capsulflask/static/style.css b/capsulflask/static/style.css index c07779e..73a64c9 100644 --- a/capsulflask/static/style.css +++ b/capsulflask/static/style.css @@ -122,7 +122,7 @@ select { padding-right: 2em; } -input[type=text], textarea { +input[type=text], input[type=number], textarea { font: calc(0.40rem + 1vmin) monospace; border: 1px solid #777e73; outline: 0; @@ -152,6 +152,17 @@ input[type=submit], select { cursor: pointer; } +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +/* Firefox */ +input[type=number] { + -moz-appearance: textfield; +} + h1, h2, h3, h4, h5 { font-size:calc(0.40rem + 1vmin); margin: initial; @@ -195,7 +206,7 @@ td { .code { display: inline-block; - padding: 0.5em 2em; + padding: 0.5em 1.2em; border-radius: 0.5em; border: 1px solid #777e73; background: #bdc7b810; diff --git a/capsulflask/stripe.py b/capsulflask/stripe.py index 4d60ad0..84e4c70 100644 --- a/capsulflask/stripe.py +++ b/capsulflask/stripe.py @@ -1 +1,116 @@ -import stripe \ No newline at end of file +import stripe +import json +import decimal + + +from flask import Blueprint +from flask import request +from flask import current_app +from flask import session +from flask import redirect +from flask import url_for +from flask import jsonify +from flask import flash +from flask import render_template +from werkzeug.exceptions import abort + +from capsulflask.auth import account_required + +from capsulflask.db import get_model + +bp = Blueprint("stripe", __name__, url_prefix="/stripe") + +@bp.route("/", methods=("GET", "POST")) +@account_required +def index(): + + stripe_checkout_session_id=None + + if request.method == "POST": + errors = list() + if "dollars" not in request.form: + errors.append("dollars is required") + elif decimal.Decimal(request.form["dollars"]) < decimal.Decimal(1): + errors.append("dollars must be >= 1") + + if len(errors) == 0: + + print(f"creating stripe checkout session for {session['account']}, ${request.form['dollars']}") + + checkout_session = stripe.checkout.Session.create( + success_url=current_app.config['BASE_URL'] + "/stripe/success?session_id={CHECKOUT_SESSION_ID}", + cancel_url=current_app.config['BASE_URL'] + "/stripe", + payment_method_types=["card"], + customer_email=session["account"], + line_items=[ + { + "name": "Capsul Cloud Compute", + "images": [current_app.config['BASE_URL']+"/static/capsul-product-image.png"], + "quantity": 1, + "currency": "usd", + "amount": request.form["dollars"] + } + ] + ) + stripe_checkout_session_id = checkout_session['id'] + + print(f"stripe_checkout_session_id={stripe_checkout_session_id} ( {session['account']}, ${request.form['dollars']} )") + + get_model().create_stripe_checkout_session(stripe_checkout_session_id, session["account"], request.form["dollars"]) + + for error in errors: + flash(error) + + return render_template( + "stripe.html", + stripe_checkout_session_id=stripe_checkout_session_id, + stripe_public_key=current_app.config["STRIPE_PUBLISHABLE_KEY"] + ) + +@bp.route("/success", methods=("GET",)) +def success(): + stripe_checkout_session_id = request.args.get('session_id') + if not stripe_checkout_session_id: + print("/stripe/success returned 400: missing required URL parameter session_id") + abort(400, "missing required URL parameter session_id") + else: + checkout_session = stripe.checkout.Session.retrieve(stripe_checkout_session_id) + if checkout_session and 'display_items' in checkout_session: + dollars = checkout_session['display_items'][0]['amount'] + + #consume_stripe_checkout_session deletes the checkout session row and inserts a payment row + # its ok to call consume_stripe_checkout_session more than once because it only takes an action if the session exists + success_account = get_model().consume_stripe_checkout_session(stripe_checkout_session_id, dollars) + if success_account: + print(f"{success_account} paid ${dollars} successfully (stripe_checkout_session_id={stripe_checkout_session_id})") + + return redirect(url_for("console.account_balance")) + +# I don't think the webhook is needed +# @bp.route("/webhook", methods=("POST",)) +# def webhook(): + +# request_data = json.loads(request.data) +# signature = request.headers.get('stripe-signature') +# try: +# event = stripe.Webhook.construct_event( +# payload=request_data, +# sig_header=signature, +# secret=current_app.config['STRIPE_WEBHOOK_SECRET'] +# ) +# if event['type'] == 'checkout.session.completed': +# dollars = event['data']['object']['display_items'][0]['amount'] +# stripe_checkout_session_id = event['data']['object']['id'] + +# #consume_stripe_checkout_session deletes the checkout session row and inserts a payment row +# # its ok to call consume_stripe_checkout_session more than once because it only takes an action if the session exists +# get_model().consume_stripe_checkout_session(stripe_checkout_session_id, dollars) + +# return jsonify({'status': 'success'}) +# except ValueError as e: +# print("/stripe/webhook returned 400: bad request", e) +# abort(400, "bad request") +# except stripe.error.SignatureVerificationError: +# print("/stripe/webhook returned 400: invalid signature") +# abort(400, "invalid signature") + \ No newline at end of file diff --git a/capsulflask/templates/account-balance.html b/capsulflask/templates/account-balance.html index 69779ff..4062f8f 100644 --- a/capsulflask/templates/account-balance.html +++ b/capsulflask/templates/account-balance.html @@ -38,10 +38,10 @@

PAYMENT OPTIONS

diff --git a/capsulflask/templates/base.html b/capsulflask/templates/base.html index 90df517..2b7b57c 100644 --- a/capsulflask/templates/base.html +++ b/capsulflask/templates/base.html @@ -5,6 +5,7 @@ + {% block head %}{% endblock %} diff --git a/capsulflask/templates/stripe.html b/capsulflask/templates/stripe.html new file mode 100644 index 0000000..df51f53 --- /dev/null +++ b/capsulflask/templates/stripe.html @@ -0,0 +1,39 @@ +{% extends 'base.html' %} + +{% block title %}Capsuls{% endblock %} + +{% block head %}{% endblock %} + +{% block content %} +
+

PAY WITH STRIPE

+
+
+
+
+ + +
+
+ +
+
+
+ +{% if stripe_checkout_session_id %} + +{% endif %} + +{% endblock %} + +{% block pagesource %}/templates/stripe.html{% endblock %}