stripe payment processor

This commit is contained in:
forest 2020-05-12 12:38:36 -05:00
parent d293d43392
commit 08e23cf0d1
12 changed files with 264 additions and 11 deletions

View File

@ -24,6 +24,7 @@ toml = "==0.10.0"
typed-ast = "==1.4.1" typed-ast = "==1.4.1"
Werkzeug = "==1.0.1" Werkzeug = "==1.0.1"
wrapt = "==1.12.1" wrapt = "==1.12.1"
stripe = "*"
[dev-packages] [dev-packages]

46
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "9b88525881f174f421ccb5b49fc34948053fb5d9d9eafa90bdec8ef0bfbc30ea" "sha256": "8355b0bc9024432220ab4f05b2997f827af534691520249c6ff8bb2db9014dc8"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -31,6 +31,20 @@
"index": "pypi", "index": "pypi",
"version": "==1.4" "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": { "click": {
"hashes": [ "hashes": [
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
@ -62,6 +76,13 @@
"index": "pypi", "index": "pypi",
"version": "==20.0.4" "version": "==20.0.4"
}, },
"idna": {
"hashes": [
"sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
"sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"
],
"version": "==2.9"
},
"isort": { "isort": {
"hashes": [ "hashes": [
"sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1",
@ -195,6 +216,14 @@
"index": "pypi", "index": "pypi",
"version": "==2.5.2" "version": "==2.5.2"
}, },
"requests": {
"hashes": [
"sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee",
"sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"
],
"markers": "python_version >= '3.0'",
"version": "==2.23.0"
},
"six": { "six": {
"hashes": [ "hashes": [
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
@ -203,6 +232,14 @@
"index": "pypi", "index": "pypi",
"version": "==1.14.0" "version": "==1.14.0"
}, },
"stripe": {
"hashes": [
"sha256:515fe2cc915e639468f30150a39c162fc0fb090256ae9d6a04e5022925d136f1",
"sha256:bdbbea632b8faa983c670db61debbe0bdb5802ef98fd0613a03aa466e56cdade"
],
"index": "pypi",
"version": "==2.48.0"
},
"toml": { "toml": {
"hashes": [ "hashes": [
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
@ -240,6 +277,13 @@
"markers": "implementation_name == 'cpython' and python_version < '3.8'", "markers": "implementation_name == 'cpython' and python_version < '3.8'",
"version": "==1.4.1" "version": "==1.4.1"
}, },
"urllib3": {
"hashes": [
"sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527",
"sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"
],
"version": "==1.25.9"
},
"werkzeug": { "werkzeug": {
"hashes": [ "hashes": [
"sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43",

View File

@ -1,3 +1,4 @@
import stripe
import os import os
from dotenv import load_dotenv, find_dotenv from dotenv import load_dotenv, find_dotenv
@ -23,11 +24,15 @@ app.config.from_mapping(
MAIL_PASSWORD=os.environ.get("MAIL_PASSWORD", default=""), MAIL_PASSWORD=os.environ.get("MAIL_PASSWORD", default=""),
MAIL_DEFAULT_SENDER=os.environ.get("MAIL_DEFAULT_SENDER", default="forest@nullhex.com"), 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_SECRET_KEY=os.environ.get("STRIPE_SECRET_KEY", default=""),
STRIPE_PUBLISHABLE_KEY=os.environ.get("STRIPE_PUBLISHABLE_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['FLASK_MAIL_INSTANCE'] = Mail(app)
app.config['VIRTUALIZATION_MODEL'] = virt_model.MockVirtualization() app.config['VIRTUALIZATION_MODEL'] = virt_model.MockVirtualization()
@ -35,11 +40,12 @@ from capsulflask import db
db.init_app(app) 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(landing.bp)
app.register_blueprint(auth.bp) app.register_blueprint(auth.bp)
app.register_blueprint(console.bp) app.register_blueprint(console.bp)
app.register_blueprint(stripe.bp)
app.add_url_rule("/", endpoint="index") app.add_url_rule("/", endpoint="index")

View File

@ -5,8 +5,6 @@ from flask import Blueprint
from flask import flash from flask import flash
from flask import current_app from flask import current_app
from flask import g from flask import g
from flask import redirect
from flask import url_for
from flask import request from flask import request
from flask import session from flask import session
from flask import render_template from flask import render_template

View File

@ -23,7 +23,7 @@ class DBModel:
return token return token
def consume_token(self, 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() rows = self.cursor.fetchall()
if len(rows) > 0: if len(rows) > 0:
email = rows[0][0] email = rows[0][0]
@ -151,3 +151,34 @@ class DBModel:
self.cursor.fetchall() 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

View File

@ -62,6 +62,13 @@ CREATE TABLE login_tokens (
PRIMARY KEY (email, created) 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) INSERT INTO os_images (id, template_image_file_name, description)
VALUES ('alpine311', 'alpine-cloud-2020-04-18.qcow2', 'Alpine Linux 3.11'), 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)'), ('ubuntu18', 'ubuntu-18.04-minimal-cloudimg-amd64.img', 'Ubuntu 18.04 LTS (Bionic Beaver)'),

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -122,7 +122,7 @@ select {
padding-right: 2em; padding-right: 2em;
} }
input[type=text], textarea { input[type=text], input[type=number], textarea {
font: calc(0.40rem + 1vmin) monospace; font: calc(0.40rem + 1vmin) monospace;
border: 1px solid #777e73; border: 1px solid #777e73;
outline: 0; outline: 0;
@ -152,6 +152,17 @@ input[type=submit], select {
cursor: pointer; 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 { h1, h2, h3, h4, h5 {
font-size:calc(0.40rem + 1vmin); font-size:calc(0.40rem + 1vmin);
margin: initial; margin: initial;
@ -195,7 +206,7 @@ td {
.code { .code {
display: inline-block; display: inline-block;
padding: 0.5em 2em; padding: 0.5em 1.2em;
border-radius: 0.5em; border-radius: 0.5em;
border: 1px solid #777e73; border: 1px solid #777e73;
background: #bdc7b810; background: #bdc7b810;

View File

@ -1 +1,116 @@
import stripe 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")

View File

@ -38,10 +38,10 @@
<h1>PAYMENT OPTIONS</h1> <h1>PAYMENT OPTIONS</h1>
<ul> <ul>
<li> <li>
<a href="/console/stripe">Add funds with Credit/Debit (stripe)</a> <a href="/stripe">Add funds with Credit/Debit (stripe)</a>
<ul><li>notice: stripe will load nonfree javascript </li></ul> <ul><li>notice: stripe will load nonfree javascript </li></ul>
</li> </li>
<li><a href="/console/btcpay">Add funds with Bitcoin/Litecoin/Monero (btcpay)</a></li> <li><a href="/btcpay">Add funds with Bitcoin/Litecoin/Monero (btcpay)</a></li>
<li>Cash: email treasurer@cyberia.club</li> <li>Cash: email treasurer@cyberia.club</li>
</ul> </ul>

View File

@ -5,6 +5,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="Description" content="Cyberia Capsul"> <meta name="Description" content="Cyberia Capsul">
{% block head %}{% endblock %}
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head> </head>
<body> <body>

View File

@ -0,0 +1,39 @@
{% extends 'base.html' %}
{% block title %}Capsuls{% endblock %}
{% block head %}<script src="https://js.stripe.com/v3/"></script>{% endblock %}
{% block content %}
<div class="third-margin">
<h1>PAY WITH STRIPE</h1>
</div>
<div class="row half-margin">
<form method="post">
<div class="row justify-start">
<label for="content">$</label>
<input type="number" id="name" name="name"></input>
</div>
<div class="row justify-end">
<input type="submit" value="Pay With Stripe">
</div>
</form>
</div>
{% if stripe_checkout_session_id %}
<script>
Stripe("{{ stripe_public_key }}")
.redirectToCheckout({
sessionId: "{{ stripe_checkout_session_id }}",
})
.then(function(result) {
if (result.error) {
alert("Stripe.redirectToCheckout() failed with: " + result.error.message)
}
});
</script>
{% endif %}
{% endblock %}
{% block pagesource %}/templates/stripe.html{% endblock %}