stripe payment processor
This commit is contained in:
parent
d293d43392
commit
08e23cf0d1
1
Pipfile
1
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]
|
||||
|
||||
|
46
Pipfile.lock
generated
46
Pipfile.lock
generated
@ -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",
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
@ -151,3 +151,34 @@ class DBModel:
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
@ -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)'),
|
||||
|
BIN
capsulflask/static/capsul-product-image.png
Normal file
BIN
capsulflask/static/capsul-product-image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
@ -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;
|
||||
|
@ -1 +1,116 @@
|
||||
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")
|
||||
|
@ -38,10 +38,10 @@
|
||||
<h1>PAYMENT OPTIONS</h1>
|
||||
<ul>
|
||||
<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>
|
||||
</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>
|
||||
</ul>
|
||||
|
@ -5,6 +5,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<meta name="Description" content="Cyberia Capsul">
|
||||
{% block head %}{% endblock %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
|
39
capsulflask/templates/stripe.html
Normal file
39
capsulflask/templates/stripe.html
Normal 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 %}
|
Loading…
Reference in New Issue
Block a user