From 73ec94f494b2f428d89aab703fd1608634bfd4e9 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 22 Jan 2018 02:58:20 +0100 Subject: use new API, add donor --- talerdonations/donations/donations.py | 225 +++++++++------------ talerdonations/donations/templates/backoffice.html | 46 ----- talerdonations/donations/templates/base.html | 10 +- talerdonations/donations/templates/checkout.html | 1 + .../donations/templates/execute-payment.html | 12 -- talerdonations/donations/templates/fallback.html | 28 --- talerdonations/donations/templates/index.html | 31 ++- .../templates/provider-not-supported.html | 5 + talerdonations/helpers.py | 101 --------- 9 files changed, 116 insertions(+), 343 deletions(-) delete mode 100644 talerdonations/donations/templates/backoffice.html delete mode 100644 talerdonations/donations/templates/execute-payment.html delete mode 100644 talerdonations/donations/templates/fallback.html create mode 100644 talerdonations/donations/templates/provider-not-supported.html delete mode 100644 talerdonations/helpers.py diff --git a/talerdonations/donations/donations.py b/talerdonations/donations/donations.py index 9b3208a..598cca6 100644 --- a/talerdonations/donations/donations.py +++ b/talerdonations/donations/donations.py @@ -20,9 +20,9 @@ import logging import os import base64 import random -from datetime import datetime import requests import flask +import traceback from ..talerconfig import TalerConfig from ..helpers import (make_url, \ expect_parameter, amount_from_float, \ @@ -40,17 +40,64 @@ app.secret_key = base64.b64encode(os.urandom(64)).decode('utf-8') TC = TalerConfig.from_env() BACKEND_URL = TC["frontends"]["backend"].value_string(required=True) CURRENCY = TC["taler"]["currency"].value_string(required=True) -MAX_FEE = dict(value=3, fraction=0, currency=CURRENCY) app.config.from_object(__name__) @app.context_processor def utility_processor(): - def url(my_url): - return join_urlparts(flask.request.script_root, my_url) def env(name, default=None): return os.environ.get(name, default) - return dict(url=url, env=env) + return dict(env=env) + + +def err_abort(abort_status_code, **params): + t = flask.render_template("templates/error.html", **params) + flask.abort(flask.make_response(t, abort_status_code)) + + +def backend_get(endpoint, params): + try: + resp = requests.get(urljoin(BACKEND_URL, endpoint), params=params) + except requests.ConnectionError: + err_abort(500, message="Could not establish connection to backend") + try: + response_json = resp.json() + except ValueError: + err_abort(500, message="Could not parse response from backend") + if resp.status_code != 200: + err_abort(500, message="Backend returned error status", + json=response_json, status_code=resp.status_code) + return response_json + + +def backend_post(endpoint, json): + try: + resp = requests.post(urljoin(BACKEND_URL, endpoint), json=json) + except requests.ConnectionError: + err_abort(500, message="Could not establish connection to backend") + try: + response_json = resp.json() + except ValueError: + err_abort(500, message="Could not parse response from backend", + status_code=resp.status_code) + if resp.status_code != 200: + err_abort(500, message="Backend returned error status", + json=response_json, status_code=resp.status_code) + return response_json + + +def expect_parameter(name): + val = flask.args.get(name) + if not val: + return err_abort(400, "parameter '{}' required".format(name)) + return val + + +@app.errorhandler(Exception) +def internal_error(e): + return flask.render_template("templates/error.html", + message="Internal error", + stack=traceback.format_exc()) @app.route("/") @@ -64,151 +111,65 @@ def javascript_licensing(): @app.route("/checkout", methods=["GET"]) def checkout(): - amount_str = expect_parameter("donation_amount") + amount = expect_parameter("donation_amount") donation_receiver = expect_parameter("donation_receiver") - try: - float(amount_str) - except ValueError: - LOGGER.warning("Invalid amount ('%s')", amount_str) - return flask.make_response( - flask.jsonify(error="invalid amount"), - 400) - display_alert = flask.request.args.get("display_alert", None) + donation_receiver = expect_parameter("donation_donor") return flask.render_template( "templates/checkout.html", donation_amount=amount_str, donation_receiver=donation_receiver, - merchant_currency=CURRENCY, - display_alert=display_alert) + donation_donor=donation_donor, + merchant_currency=CURRENCY) -@app.route("/generate-contract", methods=["GET"]) -def generate_contract(): - try: - donation_receiver = expect_parameter("donation_receiver") - donation_amount = expect_parameter("donation_amount") - except MissingParameterException as exc: - return flask.jsonify( - dict(error="Missing parameter '%s'" % exc)), 400 - amount = amount_from_float(float(donation_amount)) - order_id = "donation-%s-%X-%s" % \ - (donation_receiver, - random.randint(0, 0xFFFFFFFF), - datetime.today().strftime('%H_%M_%S')) - order = dict( - summary="Donation!", - nonce=flask.request.args.get("nonce"), - amount=amount, - max_fee=dict(value=1, fraction=0, currency=CURRENCY), - order_id=order_id, - products=[ - dict( - description="Donation to %s" % (donation_receiver,), - quantity=1, - product_id=0, - price=amount, - ), - ], - fulfillment_url=make_url("/fulfillment", ("order_id", order_id)), - pay_url=make_url("/pay"), - merchant=dict( - instance=donation_receiver, - address="nowhere", - name="Kudos Inc.", - jurisdiction="none", - ), - ) - resp = requests.post(urljoin(BACKEND_URL, 'proposal'), - json=dict(order=order)) - if resp.status_code != 200: - # It is important to use 'backend_error()', as it handles - # the case where the backend gives NO JSON as response. - # For example, if it dies, or nginx hijacks somehow the - # response. - return backend_error(resp) - return flask.jsonify(resp.json()), resp.status_code +@app.route("/provider-not-supported") +def provider_not_supported(): + return flask.render_template( "templates/provider-not-supported.html") + @app.route("/donate") def donate(): donation_receiver = expect_parameter("donation_receiver") donation_amount = expect_parameter("donation_amount") + donation_donor = expect_parameter("donation_donor") payment_system = expect_parameter("payment_system") if payment_system != "taler": - return flask.redirect(make_url("checkout", - ("donation_receiver", donation_receiver), - ("donation_amount", donation_amount), - ("display_alert", True))) - response = flask.make_response(flask.render_template('templates/fallback.html'), 402) - response.headers["X-Taler-Contract-Url"] = \ - make_url("/generate-contract", - ("donation_receiver", donation_receiver), - ("donation_amount", donation_amount)) - return response - - -@app.route("/fulfillment") -def fulfillment(): - order_id = expect_parameter("order_id") - payed_order_ids = flask.session.get("payed_order_ids", []) - print("order_id:", repr(order_id)) - print("session:", repr(flask.session)) - if order_id in payed_order_ids: - data = payed_order_ids[order_id] - return flask.render_template( - "templates/fulfillment.html", - donation_receiver=data["donation_receiver"], - donation_amount=data["donation_amount"], - order_id=order_id, - currency=CURRENCY) - - response = flask.make_response(flask.render_template("templates/fallback.html"), 402) - response.headers["X-Taler-Contract-Query"] = "fulfillment_url" - response.headers["X-Taler-Offer-Url"] = make_url("/") - return response + return flask.redirect(flask.url_for(provider_not_supported)) + fulfillment_url = flask.url_for(fulfillment, order_id=order_id, receiver=donation_receiver, _external=True) + order = dict( + amount=donation_amount, + extra=dict(donor=donation_donor, receiver=donation_receiver), + fulfillment_url=fulfillment_url, + instance=donation_receiver, + summary="Donation to {}".format(donation_receiver), + ) + proposal_resp = backend_post("proposal", dict(order=order)) + order_id = proposal_resp["order_id"] + return flask.redirect(flask.url_for(fulfillment, order_id=order_id)) -@app.route("/pay", methods=["POST"]) -def pay(): - deposit_permission = flask.request.get_json() - if deposit_permission is None: - return flask.jsonify(error="no json in body"), 400 - resp = requests.post(urljoin(BACKEND_URL, "pay"), - json=deposit_permission) - if resp.status_code != 200: - return backend_error(resp) - proposal_data = resp.json()["contract_terms"] - order_id = proposal_data["order_id"] - payed_order_ids = flask.session["payed_order_ids"] \ - = flask.session.get("payed_order_ids", {}) - payed_order_ids[order_id] = dict( - donation_receiver=proposal_data["merchant"]["instance"], - donation_amount=amount_to_float(proposal_data["amount"]) +@app.route("/fulfillment//") +def fulfillment(order_id): + pay_params = dict( + instance=INSTANCE, + order_id=order_id, + resource_url=flask.request.base_url, + session_id=session_id, + session_sig=session_sig, ) - print("received payment for", order_id) - return flask.jsonify(resp.json()), resp.status_code - -@app.route("/backoffice") -def track(): - response = flask.make_response(flask.render_template("templates/backoffice.html")) - return response + pay_status = backend_get("check-payment", pay_params) -@app.route("/history") -def history(): - qs = get_query_string().decode("utf-8") - url = urljoin(BACKEND_URL, "history") - resp = requests.get(url, params=dict(parse_qsl(qs))) - if resp.status_code != 200: - return backend_error(resp) - return flask.jsonify(resp.json()), resp.status_code + if pay_status.get("payment_redirect_url"): + return flask.redirect(pay_status["payment_redirect_url"]) + if pay_status.get("paid"): + return flask.render_template( + "templates/fulfillment.html", + donation_receiver=data["donation_receiver"], + donation_amount=data["donation_amount"], + order_id=order_id, + currency=CURRENCY) -@app.route("/track/order") -def track_order(): - instance = expect_parameter("instance") - order_id = expect_parameter("order_id") - url = urljoin(BACKEND_URL, "track/transaction") - resp = requests.get(url, params=dict(order_id=order_id, instance=instance)) - if resp.status_code != 200: - return backend_error(resp) - return flask.jsonify(resp.json()), resp.status_code + # no pay_redirect but article not paid, this should never happen! + err_abort(500, message="Internal error, invariant failed", json=pay_status) diff --git a/talerdonations/donations/templates/backoffice.html b/talerdonations/donations/templates/backoffice.html deleted file mode 100644 index 719d4b6..0000000 --- a/talerdonations/donations/templates/backoffice.html +++ /dev/null @@ -1,46 +0,0 @@ -{% extends "templates/base.html" %} -{% block main %} -

Backoffice

-

This page simulates a backoffice facility. Through it, - the user can see the money flow from Taler transactions to - wire transfers and viceversa.

- - - - - - - - - - -
-
- -
- Fake scroll -{% endblock main %} - -{% block styles %} - -{% endblock styles %} - -{% block scripts %} - -{% endblock scripts %} diff --git a/talerdonations/donations/templates/base.html b/talerdonations/donations/templates/base.html index b464752..374e426 100644 --- a/talerdonations/donations/templates/base.html +++ b/talerdonations/donations/templates/base.html @@ -18,11 +18,9 @@ Taler Donation Demo - - - - - + + + {% block styles %}{% endblock %} {% block scripts %}{% endblock %} @@ -43,7 +41,7 @@
- + - - - {% endblock %} diff --git a/talerdonations/donations/templates/provider-not-supported.html b/talerdonations/donations/templates/provider-not-supported.html new file mode 100644 index 0000000..e10d66a --- /dev/null +++ b/talerdonations/donations/templates/provider-not-supported.html @@ -0,0 +1,5 @@ +{% extends "templates/base.html" %} + +{% block main %} +Unfortunately the selected payment provider is not supported in this demo. +{% endblock main %} diff --git a/talerdonations/helpers.py b/talerdonations/helpers.py deleted file mode 100644 index 614e463..0000000 --- a/talerdonations/helpers.py +++ /dev/null @@ -1,101 +0,0 @@ -# This file is part of TALER -# (C) 2016 INRIA -# -# TALER is free software; you can redistribute it and/or modify it under the -# terms of the GNU Affero General Public License as published by the Free Software -# Foundation; either version 3, or (at your option) any later version. -# -# TALER is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -# A PARTICULAR PURPOSE. See the GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with -# TALER; see the file COPYING. If not, see -# -# @author Florian Dold -# @author Marcello Stanisci - -from urllib.parse import urljoin, urlencode -import logging -import json -import flask -from .talerconfig import TalerConfig - -LOGGER = logging.getLogger(__name__) - -TC = TalerConfig.from_env() -BACKEND_URL = TC["frontends"]["backend"].value_string(required=True) -NDIGITS = TC["frontends"]["NDIGITS"].value_int() -CURRENCY = TC["taler"]["CURRENCY"].value_string() - -FRACTION_BASE = 1e8 - -if not NDIGITS: - NDIGITS = 2 - -class MissingParameterException(Exception): - def __init__(self, param): - self.param = param - super().__init__() - -def amount_to_float(amount): - return amount['value'] + (float(amount['fraction']) / float(FRACTION_BASE)) - - -def amount_from_float(floatx): - value = int(floatx) - fraction = int((floatx - value) * FRACTION_BASE) - return dict(currency=CURRENCY, value=value, fraction=fraction) - - -def join_urlparts(*parts): - ret = "" - part = 0 - while part < len(parts): - buf = parts[part] - part += 1 - if ret.endswith("/"): - buf = buf.lstrip("/") - elif ret and not buf.startswith("/"): - buf = "/" + buf - ret += buf - return ret - - -def make_url(page, *query_params): - """ - Return a URL to a page in the current Flask application with the given - query parameters (sequence of key/value pairs). - """ - query = urlencode(query_params) - if page.startswith("/"): - root = flask.request.url_root - page = page.lstrip("/") - else: - root = flask.request.base_url - url = urljoin(root, "%s?%s" % (page, query)) - # urlencode is overly eager with quoting, the wallet right now - # needs some characters unquoted. - return url.replace("%24", "$").replace("%7B", "{").replace("%7D", "}") - - -def expect_parameter(name, alt=None): - value = flask.request.args.get(name, None) - if value is None and alt is None: - LOGGER.error("Missing parameter '%s' from '%s'." % (name, flask.request.args)) - raise MissingParameterException(name) - return value if value else alt - - -def get_query_string(): - return flask.request.query_string - -def backend_error(requests_response): - LOGGER.error("Backend error: status code: " - + str(requests_response.status_code)) - try: - return flask.jsonify(requests_response.json()), requests_response.status_code - except json.decoder.JSONDecodeError: - LOGGER.error("Backend error (NO JSON returned): status code: " - + str(requests_response.status_code)) - return flask.jsonify(dict(error="Backend died, no JSON got from it")), 502 -- cgit v1.2.3