gnunet-svn
[Top][All Lists]
Advanced

[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.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]