[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-merchant-demos] 01/08: [provision] initial work on self-provision
From: |
Admin |
Subject: |
[taler-merchant-demos] 01/08: [provision] initial work on self-provisioning |
Date: |
Wed, 12 Feb 2025 00:15:14 +0100 |
This is an automated email from the git hooks/post-receive script.
dold pushed a commit to branch master
in repository taler-merchant-demos.
commit 836fa32d29ecdbe13d7eb703c3fc02a34dec7882
Author: Özgür Kesim <oec@codeblau.de>
AuthorDate: Thu Oct 3 17:35:19 2024 +0200
[provision] initial work on self-provisioning
---
pyproject.toml | 6 +-
talermerchantdemos/cli.py | 6 +-
talermerchantdemos/provision/__init__.py | 3 +
talermerchantdemos/provision/provision.py | 273 +++++++++++++++++++++
talermerchantdemos/static/provision.css | 49 ++++
.../templates/provision-base.html.j2 | 17 ++
.../templates/provision-done.html.j2 | 40 +++
.../templates/provision-error.html.j2 | 24 ++
.../templates/provision-index.html.j2 | 40 +++
9 files changed, 454 insertions(+), 4 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index ff357bc..f9d1b96 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,11 +1,12 @@
[tool.poetry]
name = "talermerchantdemos"
-version = "0.8.2"
+version = "0.9.0"
description = "Taler merchant demos"
authors = [
"Florian Dold <dold@taler.net>",
"Marcello Stanisci <ms@taler.net>",
"Christian Grothoff <grothoff@taler.net>",
+ "Özgür Kesim <oec-taler@kesim.org>",
]
license = "AGPL3+"
include = [
@@ -27,6 +28,9 @@ include = [
# Donation files
"talermerchantdemos/donations/templates/*.j2",
"talermerchantdemos/donations/static/*.css",
+ # Provision files
+ "talermerchantdemos/provision/templates/*.j2",
+ "talermerchantdemos/provision/static/*.css",
]
diff --git a/talermerchantdemos/cli.py b/talermerchantdemos/cli.py
index 69fefd9..9a299c9 100644
--- a/talermerchantdemos/cli.py
+++ b/talermerchantdemos/cli.py
@@ -99,10 +99,10 @@ class
StandaloneApplication(gunicorn.app.base.BaseApplication):
)
@click.argument("which-shop")
def demos(config, http_port, which_shop):
- """WHICH_SHOP is one of: blog, donations or landing."""
+ """WHICH_SHOP is one of: blog, donations, provision, or landing."""
- if which_shop not in ["blog", "donations", "landing"]:
- print("Please use a valid shop name: blog, donations, landing.")
+ if which_shop not in ["blog", "donations", "provision", "landing"]:
+ print("Please use a valid shop name: blog, donations, provision,
landing.")
sys.exit(1)
options = {
diff --git a/talermerchantdemos/provision/__init__.py
b/talermerchantdemos/provision/__init__.py
new file mode 100644
index 0000000..e7286f6
--- /dev/null
+++ b/talermerchantdemos/provision/__init__.py
@@ -0,0 +1,3 @@
+from talermerchantdemos.provision.provision import app
+
+__all__ = ["app"]
diff --git a/talermerchantdemos/provision/provision.py
b/talermerchantdemos/provision/provision.py
new file mode 100644
index 0000000..9606a01
--- /dev/null
+++ b/talermerchantdemos/provision/provision.py
@@ -0,0 +1,273 @@
+##
+# 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
+import uwsgi
+from flask import request, url_for
+from flask_babel import Babel
+from flask_babel import refresh
+from flask_babel import force_locale
+from flask_babel import gettext
+from werkzeug.middleware.proxy_fix import ProxyFix
+import os
+import time
+from datetime import datetime
+import traceback
+import re
+import hashlib
+import struct
+import subprocess
+import urllib
+from ..util.talerconfig import TalerConfig, ConfigurationError
+from urllib.parse import urljoin
+from ..httpcommon import backend_post, backend_get, make_utility_processor,
get_locale
+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)
+
+logging.basicConfig()
+LOGGER = logging.getLogger(__name__)
+
+BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+
+app = flask.Flask(__name__, template_folder="../templates",
static_folder="../static")
+app.wsgi_app = ProxyFix(app.wsgi_app, x_host=1, x_prefix=1)
+app.debug = True
+app.secret_key = base64.b64encode(os.urandom(64)).decode("utf-8")
+app.config.from_object(__name__)
+
+BABEL_TRANSLATION_DIRECTORIES = "../translations"
+babel = Babel(app)
+babel.localeselector(get_locale)
+
+config_filename = uwsgi.opt["config_filename"].decode("utf-8")
+if config_filename == "":
+ config_filename = None
+config = TalerConfig.from_file(config_filename)
+
+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(babel.translation_directories)))
+LOGGER.info("currency: " + CURRENCY)
+translations = [str(translation) for translation in babel.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.
+app.context_processor(
+ make_utility_processor(
+ "provision", os.environ.get("TALER_ENV_URL_MERCHANT_PROVISION")
+ )
+)
+
+
+##
+# 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 /favicon.ico requests.
+#
+# @return the favicon.ico file.
+@app.route("/favicon.ico")
+def favicon():
+ LOGGER.info("will look into: " + os.path.join(app.root_path, "static"))
+ return flask.send_from_directory(
+ os.path.join(app.root_path, "static"),
+ "favicon.ico",
+ mimetype="image/vnd.microsoft.ico",
+ )
+
+
+##
+# Serve the main index page, redirecting to /<lang>/
+#
+# @return response object of the index page.
+@app.route("/")
+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("/<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 "/provision" request.
+#
+# @return response object for the /provision page.
+@app.route("/<lang>/provision", methods=["POST"])
+def provision(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"],
+ )
+
+
+@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"),
+ )
diff --git a/talermerchantdemos/static/provision.css
b/talermerchantdemos/static/provision.css
new file mode 100644
index 0000000..96c6e56
--- /dev/null
+++ b/talermerchantdemos/static/provision.css
@@ -0,0 +1,49 @@
+@import url(/static/theme.css);
+nav,
+nav a,
+nav span,
+.navcontainer,
+.demobar,
+nav button,
+.navbtn {
+ color: white;
+ background: DarkSlateGray;
+}
+
+nav a.active,
+nav span.active,
+.navbtn.active {
+ background-color: LightSlateGray;
+}
+
+nav a.active:hover,
+nav span.active:hover,
+.navbtn.active:hover,
+nav a:hover,
+nav button:hover,
+nav span:hover,
+.navbtn:hover {
+ background: SlateGray;
+}
+
+form {
+ padding: 1em;
+ background: SlateGray;
+ border-radius: 5px;
+ font-size: large;
+}
+
+form label {
+ color: white;
+}
+
+table td,th {
+ padding: 0.25em;
+}
+table th {
+ text-align: right;
+}
+
+ol, ul {
+ line-height: 1.5em;
+}
diff --git a/talermerchantdemos/templates/provision-base.html.j2
b/talermerchantdemos/templates/provision-base.html.j2
new file mode 100644
index 0000000..615f88e
--- /dev/null
+++ b/talermerchantdemos/templates/provision-base.html.j2
@@ -0,0 +1,17 @@
+{% extends "common-base.html.j2" %}
+
+{% block head %}
+ <link rel="stylesheet" type="text/css" href="{{ static('provision.css') }}"
/>
+{% endblock head %}
+
+
+{% block header_content %}
+
+<h1>
+<span class="it"><a href="{{ env('TALER_ENV_URL_MERCHANT_PROVISION', '#')
}}">{{gettext("Self-Provision")}}</a></span></h1>
+<p>{{
+gettext ("This is the self-provision page for merchant instances on our demo
site.")
+}}
+</p>
+
+{% endblock %}
diff --git a/talermerchantdemos/templates/provision-done.html.j2
b/talermerchantdemos/templates/provision-done.html.j2
new file mode 100644
index 0000000..2688d06
--- /dev/null
+++ b/talermerchantdemos/templates/provision-done.html.j2
@@ -0,0 +1,40 @@
+{% extends "provision-base.html.j2" %}
+
+{% block main %}
+ <h2>{{ gettext("Your merchant demo instance has been created!") }}</h2>
+ <p>
+ {{ gettext("Please write this information down:") }}
+ <table>
+ <tr>
+ <th>{{ gettext("merchant full name") }}:</th>
+ <td>{{fullname}}</td>
+ </tr>
+ <tr>
+ <th>{{ gettext("merchant-id") }}:</th>
+ <td>{{merchant_id}}</td>
+ </tr>
+ <tr>
+ <th>{{ gettext("access-token") }}:</th>
+ <td>{{access_token}}</td>
+ </tr>
+ </table>
+ </p>
+ <p>
+ {{ gettext("With the merchant-id and access-token, you can now") }}
+ <ul>
+ <li>{{ gettext("Login to the <a
href='{url}/instances/{id}/webui/#/inventory'>your merchant's backend</a>
instance for administration.").format(url=merchant_url,id=merchant_id) }}</li>
+ <li>{{ gettext("Create orders via the API at
<tt>{url}/instances/{id}/private/orders</tt>, see <a
href='https://docs.taler.net/taler-merchant-api-tutorial.html#merchant-payment-processing'>the
documentation for details</a>.").format(url=merchant_url,id=merchant_id)
}}</li>
+ <li>{{ gettext("Login to the <a href='{url}'>demo bank</a> as merchant
to see incoming wire transfers from the exchange.").format(url=bank_url) }}</li>
+ </ul>
+ </p>
+ <p>
+ {{ gettext("<b>Note:</b> the merchant instance will be automatically removed
when") }}
+ <ol>
+ <li>{{ gettext("no order has been created within {timeout} <i>right
after</i> creation, or").format(timeout=timeout_init) }} </li>
+ <li>{{ gettext("no order has been created for
{timeout}.".format(timeout=timeout_idle)) }}</li>
+ </ol>
+ </p>
+
+ <h3>Happy Hacking!</h3>
+
+{% endblock main %}
diff --git a/talermerchantdemos/templates/provision-error.html.j2
b/talermerchantdemos/templates/provision-error.html.j2
new file mode 100644
index 0000000..42a4a72
--- /dev/null
+++ b/talermerchantdemos/templates/provision-error.html.j2
@@ -0,0 +1,24 @@
+{% extends "provision-base.html.j2" %}
+{% block main %}
+ <h2>{{ gettext("Error encountered during provisioning") }}</h2>
+
+ <p>{{ message }}</p>
+
+ {% if status_code %}
+ <p>
+ {{ gettext ("The backend returned status code
{code}.").format(code=status_code) }}.
+ </p>
+ {% endif %}
+
+ {% if json %}
+ <p>{{gettext("Backend response:")}}</p>
+ <pre>{{ json }}</pre>
+ {% endif %}
+
+ {% if stack %}
+ <p>{{gettext("Stack trace:")}}</p>
+ <pre>
+ {{ stack }}
+ </pre>
+ {% endif %}
+{% endblock main %}
diff --git a/talermerchantdemos/templates/provision-index.html.j2
b/talermerchantdemos/templates/provision-index.html.j2
new file mode 100644
index 0000000..a9940ff
--- /dev/null
+++ b/talermerchantdemos/templates/provision-index.html.j2
@@ -0,0 +1,40 @@
+{% extends "provision-base.html.j2" %}
+
+{% block main %}
+<h2>{{ gettext("Provision yourself a demo merchant instance!") }}</h2>
+
+<p>
+{{
+ gettext("If you are a frontend developer and just want to integrate payments
with Taler into your UI, simply provision yourself here a merchant instance and
a corresponding bank account in the demo-environment, and start using the
merchant API on this demo site!")
+}}
+</p>
+
+<p>
+{{ gettext("On success you will receive a <b>merchant-ID</b> and an
<b>access-token</b>. You need that information") }}
+<ul>
+ <li>{{ gettext("to create orders via your merchant's own backend API and
make changes in the merchant's inventory at our demo <a
href='{backend}'>merchant backend</a>, and").format(backend=backend_url) }}</li>
+ <li>{{ gettext("to use the merchant's bank account at our <a
href='{bank}'>demo bank</a> and see the incoming wire transfers from the
payment service provider.").format(bank=bank_url) }}</li>
+</ul>
+
+</p>
+
+<div>
+ <form action="{{ url_for('provision',lang=getlang()) }}" method="post"
class="pure-form">
+ <div class="form">
+ <div>
+ <label for="fullname">
+ {{ gettext("Enter the full name for your merchant:") }}
+ </label>
+ <input name="fullname" id="fullname" type="text" />
+
+ <button class="pure-button pure-button-primary" name="submit">
+ {{gettext("Provision!")}}
+ </button>
+ </div>
+ </div>
+ </form>
+</div>
+
+
+
+{% endblock %}
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
- [taler-merchant-demos] branch master updated (8c32ddc -> 8730588), Admin, 2025/02/11
- [taler-merchant-demos] 04/08: [provision] fix string parameter for subprocess.run, Admin, 2025/02/11
- [taler-merchant-demos] 01/08: [provision] initial work on self-provisioning,
Admin <=
- [taler-merchant-demos] 06/08: [provision] rename endpoint /provision to /create, Admin, 2025/02/11
- [taler-merchant-demos] 03/08: [provision] added service file and updated rules for debian, Admin, 2025/02/11
- [taler-merchant-demos] 02/08: [provision] added self-provision in nav-bar, Admin, 2025/02/11
- [taler-merchant-demos] 05/08: [provision] layout and template tuning, Admin, 2025/02/11
- [taler-merchant-demos] 08/08: [provision] use .../orders as landing link for backend, Admin, 2025/02/11
- [taler-merchant-demos] 07/08: [provision] fix typo, Admin, 2025/02/11