taler-merchant-demos

Python-based Frontends for the Demonstration Web site
Log | Files | Refs | Submodules | README | LICENSE

commit f6387885d49a0261323dc1d8653944624453b81a
parent 91001dd6f624dd82bed78d0a48aba48cef7b2e8e
Author: Florian Dold <florian@dold.me>
Date:   Thu, 30 Oct 2025 11:28:00 +0100

remote self-provision support

Now supported directly by the merchant

Diffstat:
Mdebian/rules | 1-
Ddebian/taler-merchant-demos.taler-demo-provision.service | 12------------
Mtalermerchantdemos/cli.py | 6+++---
Dtalermerchantdemos/provision/__init__.py | 3---
Dtalermerchantdemos/provision/provision.py | 283-------------------------------------------------------------------------------
5 files changed, 3 insertions(+), 302 deletions(-)

diff --git a/debian/rules b/debian/rules @@ -40,7 +40,6 @@ override_dh_installsystemd: dh_installsystemd -ptaler-merchant-demos --name=taler-demo-landing --no-start --no-enable dh_installsystemd -ptaler-merchant-demos --name=taler-demo-blog --no-start --no-enable dh_installsystemd -ptaler-merchant-demos --name=taler-demo-donations --no-start --no-enable - dh_installsystemd -ptaler-merchant-demos --name=taler-demo-provision --no-start --no-enable # final invocation to generate daemon reload dh_installsystemd diff --git a/debian/taler-merchant-demos.taler-demo-provision.service b/debian/taler-merchant-demos.taler-demo-provision.service @@ -1,12 +0,0 @@ -[Unit] -Description=Taler Demo Merchant Self-Provisioning Service - -[Service] -User=taler-merchant-demos -ExecStart=/usr/bin/taler-merchant-demos -c /etc/taler/taler-merchant-frontends.conf provision -EnvironmentFile=/etc/taler/taler-merchant-frontends.env -Restart=on-failure -RestartSec=1s - -[Install] -WantedBy=multi-user.target diff --git a/talermerchantdemos/cli.py b/talermerchantdemos/cli.py @@ -100,12 +100,12 @@ class StandaloneApplication(gunicorn.app.base.BaseApplication): ) @click.argument("which-shop") def demos(config_filename, http_port, which_shop): - """WHICH_SHOP is one of: blog, donations, provision, or landing.""" + """WHICH_SHOP is one of: blog, donations, or landing.""" logging.basicConfig(level=logging.INFO) - if which_shop not in ["blog", "donations", "provision", "landing"]: - print("Please use a valid shop name: blog, donations, provision, landing.") + if which_shop not in ["blog", "donations", "landing"]: + print("Please use a valid shop name: blog, donations, landing.") sys.exit(1) config = TalerConfig.from_file(config_filename) options = { diff --git a/talermerchantdemos/provision/__init__.py b/talermerchantdemos/provision/__init__.py @@ -1,3 +0,0 @@ -from talermerchantdemos.provision.provision import app - -__all__ = ["app"] diff --git a/talermerchantdemos/provision/provision.py b/talermerchantdemos/provision/provision.py @@ -1,283 +0,0 @@ -## -# This file is part of GNU TALER. -# Copyright (C) 2024 Taler Systems SA -# -# TALER is free software; you can redistribute it and/or modify it under the -# terms of the GNU Lesser General Public License as published by the Free Software -# Foundation; either version 2.1, 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 Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License along with -# GNU TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -# -# @author Özgür Kesim -# @brief Implementation of a merchant self-provision service - -import base64 -import logging -import flask -from flask import request, url_for -from flask_babel import Babel -from flask_babel import gettext -from werkzeug.middleware.proxy_fix import ProxyFix -import os -import babel -from datetime import datetime -import re -import hashlib -import struct -import subprocess -from ..httpcommon import backend_post, backend_get, make_utility_processor -from ..appconfig import load_taler_config -import sys - -if not sys.version_info.major == 3 and sys.version_info.minor >= 6: - print("Python 3.6 or higher is required.") - print( - "You are using Python {}.{}.".format( - sys.version_info.major, sys.version_info.minor - ) - ) - sys.exit(1) - -logger = logging.getLogger(__name__) - -app = flask.Flask(__name__, - template_folder="../templates", - static_folder="../static", - static_url_path="/static") -app.wsgi_app = ProxyFix(app.wsgi_app, x_host=1) -app.debug = True -app.secret_key = base64.b64encode(os.urandom(64)).decode("utf-8") -app.config.from_object(__name__) - -# The path prefix for the provision system, without the leading or trailing "/" -provision_prefix=os.environ.get("TALER_ENV_URL_MERCHANT_PROVISION_PREFIX", "provision") - -# We need a special local handler for this app, -# as our path's have the prefix {provision_prefix} -def get_locale(): - parts = request.path.split("/", 3) - if 3 >= len(parts): - # Totally unexpected path format, do not localize - return "en" - if parts[1] == f"{provision_prefix}": - lang = parts[2] - else: - lang = parts[1] - - # Sanity check on the language code. - try: - babel.core.Locale.parse(lang) - except Exception as err: - # Not a locale, default to english. - logger.error(f"language {lang} did not parse, default to english") - return "en" - if lang == "static": - # Static resource, not a language indicator. - # Do not localize then. - return "en" - return lang - -BABEL_TRANSLATION_DIRECTORIES = "../translations" -babbel = Babel(app) -babbel.localeselector(get_locale) - -config = load_taler_config() - -CURRENCY = config["taler"]["currency"].value_string(required=True) - -backend_urls = {} -backend_apikeys = {} -timeouts = {} - -timeouts["init"] = config["frontend-demo-provision"][f"timeout_init"].value_string(required=True) -timeouts["idle"] = config["frontend-demo-provision"][f"timeout_idle"].value_string(required=True) - -def add_backend(name): - backend_urls[name] = config["frontend-demo-provision"][f"backend_url_{name}"].value_string(required=True) - backend_apikeys[name] = config["frontend-demo-provision"][f"backend_apikey_{name}"].value_string(required=True) - -add_backend("merchant") -add_backend("bank") - - - -logger.info("Using translations from:" + ":".join(list(babbel.translation_directories))) -logger.info("currency: " + CURRENCY) -translations = [str(translation) for translation in babbel.list_translations()] -if not "en" in translations: - translations.append("en") -logger.info( - "Operating with the following translations available: " + " ".join(translations) -) - -# Add context processor that will make additional variables -# and functions available in the template. -# Overwrite the "getlang" helper from httpcommon with the -# specific one for provision. -app.context_processor( - make_utility_processor( - "provision", os.environ.get("TALER_ENV_URL_MERCHANT_PROVISION"), - dict(getlang=get_locale) - ) -) - - -## -# Return a error response to the client. -# -# @param abort_status_code status code to return along the response. -# @param params _kw_ arguments to passed verbatim to the templating engine. -def err_abort(abort_status_code, **params): - t = flask.render_template("provision-error.html.j2", **params) - flask.abort(flask.make_response(t, abort_status_code)) - - -## -# Issue a GET request to the backend. -# -# @param endpoint the backend endpoint where to issue the request. -# @param params (dict type of) URL parameters to append to the request. -# @return the JSON response from the backend, or a error response -# if something unexpected happens. -def backend_instanced_get(instance, endpoint, params): - return backend_get(backend_urls[instance], endpoint, params, auth_token=backend_apikeys[instance]) - - -## -# POST a request to the backend, and return a error -# response if any error occurs. -# -# @param endpoint the backend endpoint where to POST -# this request. -# @param json the POST's body. -# @return the backend response (JSON format). -def backend_instanced_post(instance, endpoint, json): - return backend_post(backend_urls[instance], endpoint, json, auth_token=backend_apikeys[instance]) - - -## -# "Fallback" exception handler to capture all the unmanaged errors. -# -# @param e the Exception object, currently unused. -# @return flask-native response object carrying the error message -# (and execution stack!). -@app.errorhandler(Exception) -def internal_error(e): - t = flask.render_template( - "provision-error.html.j2", - page_title=gettext("GNU Taler Demo: Error"), - message=str(e), - ) - flask.abort(flask.make_response(t, 500)) - - -## -# Serve the main index page, redirecting to /<lang>/ -# -# @return response object of the index page. -@app.route(f"/{provision_prefix}/") -def index(): - default = "en" - target = flask.request.accept_languages.best_match(translations, default) - return flask.redirect(url_for("index") + "/"+ target + "/", code=302) - - -## -# Serve the main index page. -# -# @return response object of the index page. -@app.route(f"/{provision_prefix}/<lang>/") -def start(lang): - # get_locale defaults to english, hence the - # condition below happens only when lang is - # wrong or unsupported, respond 404. - if lang != get_locale(): - err_abort( - 404, - message=f"Language {lang} not found", - ) - - return flask.render_template( - "provision-index.html.j2", - page_title=gettext("GNU Taler Demo: Provision"), - merchant_currency=CURRENCY, - merchant_url=backend_urls["merchant"], - bank_url=backend_urls["bank"], - ) - - -# Acceptable merchant names must match this regular expression -allowed = re.compile("^[a-zA-Z]([a-zA-Z0-9_. -]+)[a-zA-Z0-9][.]?$") - -## -# Handle the "/create" request -# and create a merchant instance and bank account. -# -# @return response object for the /provision page. -@app.route(f"/{provision_prefix}/<lang>/create", methods=["POST"]) -def create(lang): - fullname = flask.request.form.get("fullname") - fullname = fullname.strip(' \t\n\r') - if not fullname: - return err_abort(400, message=gettext("Full name required.")) - if not allowed.match(fullname): - return err_abort(400, message=gettext("Full name not acceptable.")) - - # Only create an merchant with the same name every 15 minute - n = datetime.now() - ts = datetime(n.year, n.month, n.day, n.hour, n.minute % 15).timestamp() - - m = hashlib.sha256() - m.update(fullname.encode('utf-8')) - m.update(struct.pack('d', ts)) - hash = m.hexdigest() - - merchant_id = "merchant-"+hash[:8] - access_token = hash[8:20] - - ret = subprocess.run(["taler-harness", - "deployment", - "provision-bank-and-merchant", - "--legal-name={n}".format(n=fullname), - "--id={id}".format(id=merchant_id), - "--password={pw}".format(pw=access_token), - "--merchant-management-token={t}".format(t=backend_apikeys["merchant"]), - "--bank-admin-token={t}".format(t=backend_apikeys["bank"]), - backend_urls["merchant"], - backend_urls["bank"], - ], capture_output=True) - - if ret.returncode != 0: - logger.error("taler-harness returned {d},\nstdout:>>>>{o}<<<<\nstderr:>>>>{e}<<<<<\n" - .format(d=ret.returncode,o=ret.stdout.decode(),e=ret.stderr.decode())) - return internal_error("Internal error, couldn't create instance. Soooo sorry! 🤷") - - - logger.debug("taler-harness output:>>>>{o}<<<<)".format(o=ret.stdout.decode())) - logger.info("merchant instance {id} created with hash: {hash}".format(id=merchant_id,hash=hash)) - return flask.render_template( - "provision-done.html.j2", - page_title=gettext("GNU Taler Demo: Self-Provision"), - fullname=fullname, - merchant_id=merchant_id, - access_token=access_token, - bank_url=backend_urls["bank"], - merchant_url=backend_urls["merchant"], - timeout_init=timeouts["init"], - timeout_idle=timeouts["idle"], - currency=CURRENCY, - ) - - -@app.errorhandler(404) -def handler(e): - return flask.render_template( - "provision-error.html.j2", - page_title=gettext("GNU Taler Demo: Provision Error"), - message=gettext("Page not found"), - )