taler-merchant-demos

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

commit eddafc7a4046ef2a2abeed1a2f44e45e7ae92adf
parent 9b30c3fb178fd398016ab19b2b02ae6a9c4135ae
Author: Florian Dold <dold@taler.net>
Date:   Tue, 25 Aug 2026 18:03:18 +0200

merchant demos: read public URLs from configuration

Diffstat:
MREADME.md | 21++++++++++-----------
Mcmd/taler-merchant-demos/main.go | 29++++++++++++++++++++++++++++-
Mcmd/taler-merchant-demos/main_test.go | 30++++++++++++++++++++++++++++++
Mdebian/taler-merchant-demos.taler-demo-blog.service | 3+--
Mdebian/taler-merchant-demos.taler-demo-donations.service | 3+--
Mdebian/taler-merchant-demos.taler-demo-landing.service | 3+--
Minternal/web/app.go | 35+++++++++++++++++++++--------------
Minternal/web/app_test.go | 30++++++++++++++++++++++++++++++
Minternal/web/blog.go | 2+-
Minternal/web/landing.go | 11+++++------
10 files changed, 128 insertions(+), 39 deletions(-)

diff --git a/README.md b/README.md @@ -77,6 +77,12 @@ A configuration containing all three demos looks like this: [taler] currency = KUDOS +[frontend-demo] +landing_url = https://demo.taler.net/ +bank_url = https://bank.demo.taler.net/ +blog_url = https://shop.demo.taler.net/ +donations_url = https://donations.demo.taler.net/ + [frontend-demo-landing] http_serve = tcp http_port = 8080 @@ -119,7 +125,7 @@ commented header lists files in load order, and every setting is annotated with its source location: ```console -$ taler-merchant-demos config dump -c /etc/taler/taler-merchant-frontends.conf +$ taler-merchant-demos config dump -c /etc/taler-merchant-demos/taler-merchant-demos.conf ``` ## Run @@ -127,18 +133,11 @@ $ taler-merchant-demos config dump -c /etc/taler/taler-merchant-frontends.conf Choose exactly one demo per process: ```console -$ taler-merchant-demos -c /etc/taler/taler-merchant-frontends.conf landing -$ taler-merchant-demos -c /etc/taler/taler-merchant-frontends.conf blog -$ taler-merchant-demos -c /etc/taler/taler-merchant-frontends.conf donations +$ taler-merchant-demos -c /etc/taler-merchant-demos/taler-merchant-demos.conf landing +$ taler-merchant-demos -c /etc/taler-merchant-demos/taler-merchant-demos.conf blog +$ taler-merchant-demos -c /etc/taler-merchant-demos/taler-merchant-demos.conf donations ``` -The following environment variables supply links between the demo services: - -- `TALER_ENV_URL_INTRO` -- `TALER_ENV_URL_BANK` -- `TALER_ENV_URL_MERCHANT_BLOG` -- `TALER_ENV_URL_MERCHANT_DONATIONS` - When deployed behind a reverse proxy, `X-Forwarded-Host`, `X-Forwarded-Proto`, and `X-Forwarded-Prefix` are used for public fulfillment URLs and generated links. These headers should be removed from untrusted diff --git a/cmd/taler-merchant-demos/main.go b/cmd/taler-merchant-demos/main.go @@ -76,7 +76,13 @@ func runWithOutput(args []string, stdout, stderr io.Writer) error { if err != nil { return err } - opts := web.Options{Shop: shop, Currency: currency} + publicURLs, err := readPublicURLs(cfg) + if err != nil { + return err + } + opts := web.Options{ + Shop: shop, Currency: currency, PublicURLs: publicURLs, + } if shop == "blog" { backendURL, err := cfg.Require("frontend-demo-blog", "backend_url") if err != nil { @@ -146,6 +152,27 @@ func runWithOutput(args []string, stdout, stderr io.Writer) error { } } +func readPublicURLs(cfg *config.Config) (web.PublicURLs, error) { + var result web.PublicURLs + urls := []struct { + option string + target *string + }{ + {"landing_url", &result.Landing}, + {"bank_url", &result.Bank}, + {"blog_url", &result.Blog}, + {"donations_url", &result.Donations}, + } + for _, item := range urls { + value, err := cfg.Require("frontend-demo", item.option) + if err != nil { + return web.PublicURLs{}, err + } + *item.target = value + } + return result, nil +} + func runConfig(args []string, configFile string, stdout, stderr io.Writer) error { usage := func() { fmt.Fprintln(stderr, "Usage: taler-merchant-demos config {dump}") diff --git a/cmd/taler-merchant-demos/main_test.go b/cmd/taler-merchant-demos/main_test.go @@ -7,6 +7,8 @@ import ( "path/filepath" "strings" "testing" + + "git.taler.net/taler-merchant-demos/internal/config" ) func TestBrowserURL(t *testing.T) { @@ -53,3 +55,31 @@ func TestConfigDumpCommand(t *testing.T) { } } } + +func TestReadPublicURLs(t *testing.T) { + path := filepath.Join(t.TempDir(), "demo.conf") + contents := `[frontend-demo] +landing_url = https://landing.example/ +bank_url = https://bank.example/ +blog_url = https://blog.example/ +donations_url = https://donations.example/ +` + if err := os.WriteFile(path, []byte(contents), 0o600); err != nil { + t.Fatal(err) + } + t.Setenv("TALER_MERCHANT_DEMOS_BASE_CONFIG", t.TempDir()) + cfg, err := config.Load(path) + if err != nil { + t.Fatal(err) + } + got, err := readPublicURLs(cfg) + if err != nil { + t.Fatal(err) + } + if got.Landing != "https://landing.example/" || + got.Bank != "https://bank.example/" || + got.Blog != "https://blog.example/" || + got.Donations != "https://donations.example/" { + t.Fatalf("public URLs = %#v", got) + } +} diff --git a/debian/taler-merchant-demos.taler-demo-blog.service b/debian/taler-merchant-demos.taler-demo-blog.service @@ -3,8 +3,7 @@ Description=Taler Demo Blog Merchant [Service] User=taler-merchant-demos -ExecStart=/usr/bin/taler-merchant-demos -c /etc/taler/taler-merchant-frontends.conf blog -EnvironmentFile=/etc/taler/taler-merchant-frontends.env +ExecStart=/usr/bin/taler-merchant-demos -c /etc/taler-merchant-demos/taler-merchant-demos.conf blog Restart=on-failure RestartSec=1s diff --git a/debian/taler-merchant-demos.taler-demo-donations.service b/debian/taler-merchant-demos.taler-demo-donations.service @@ -3,8 +3,7 @@ Description=Taler Demo Donations Merchant [Service] User=taler-merchant-demos -ExecStart=/usr/bin/taler-merchant-demos -c /etc/taler/taler-merchant-frontends.conf donations -EnvironmentFile=/etc/taler/taler-merchant-frontends.env +ExecStart=/usr/bin/taler-merchant-demos -c /etc/taler-merchant-demos/taler-merchant-demos.conf donations Restart=on-failure RestartSec=1s diff --git a/debian/taler-merchant-demos.taler-demo-landing.service b/debian/taler-merchant-demos.taler-demo-landing.service @@ -3,8 +3,7 @@ Description=Taler Demo Landing Page [Service] User=taler-merchant-demos -ExecStart=/usr/bin/taler-merchant-demos -c /etc/taler/taler-merchant-frontends.conf landing -EnvironmentFile=/etc/taler/taler-merchant-frontends.env +ExecStart=/usr/bin/taler-merchant-demos -c /etc/taler-merchant-demos/taler-merchant-demos.conf landing Restart=on-failure RestartSec=1s diff --git a/internal/web/app.go b/internal/web/app.go @@ -14,7 +14,6 @@ import ( "log" "net/http" "net/url" - "os" "path" "sort" "strconv" @@ -31,10 +30,19 @@ const ( subscriptionPrefix = "blog_abo_" ) +// PublicURLs contains the externally visible entry points linked by the demos. +type PublicURLs struct { + Landing string + Bank string + Blog string + Donations string +} + // Options configures one of the three demo HTTP applications. type Options struct { Shop string Currency string + PublicURLs PublicURLs BlogBackend *backend.Client DonationBackends map[string]*backend.Client DonauURL string @@ -361,10 +369,10 @@ func (a *App) makePage(r *http.Request, lang, title string, content any) page { Prefix: prefix, StaticPrefix: prefix + "/static/", Languages: languages, LanguageName: template.HTML("en"), Content: content, Links: links{ - Landing: appLanguageURL(env("TALER_ENV_URL_INTRO", "#"), lang), - Bank: env("TALER_ENV_URL_BANK", "#") + "?lang=" + url.QueryEscape(lang), - Blog: appLanguageURL(env("TALER_ENV_URL_MERCHANT_BLOG", "#"), lang), - Donations: appLanguageURL(env("TALER_ENV_URL_MERCHANT_DONATIONS", "#"), lang), + Landing: appLanguageURL(a.opts.PublicURLs.Landing, lang), + Bank: configuredURL(a.opts.PublicURLs.Bank) + "?lang=" + url.QueryEscape(lang), + Blog: appLanguageURL(a.opts.PublicURLs.Blog, lang), + Donations: appLanguageURL(a.opts.PublicURLs.Donations, lang), }, } for _, language := range languages { @@ -376,16 +384,16 @@ func (a *App) makePage(r *http.Request, lang, title string, content any) page { switch a.opts.Shop { case "landing": p.HeaderTitle = a.catalogs.translate(lang, "Introduction") - p.HeaderURL = env("TALER_ENV_URL_INTRO", "#") + p.HeaderURL = configuredURL(a.opts.PublicURLs.Landing) p.HeaderText = a.translatedHTML(lang, "Try GNU Taler with a toy currency.") case "donations": p.HeaderTitle = a.catalogs.translate(lang, "Donations") - p.HeaderURL = env("TALER_ENV_URL_MERCHANT_DONATIONS", "#") + p.HeaderURL = configuredURL(a.opts.PublicURLs.Donations) p.HeaderText = a.translatedHTML(lang, "Support Free Software projects with a toy currency.") p.Styles = []string{"colors-donations.css"} case "blog": p.HeaderTitle = a.catalogs.translate(lang, "Essay Shop") - p.HeaderURL = env("TALER_ENV_URL_MERCHANT_BLOG", "#") + p.HeaderURL = configuredURL(a.opts.PublicURLs.Blog) p.HeaderText = a.translatedHTML(lang, "Buy chapters from <cite>Free Software, Free Society</cite> with a toy currency.") p.Styles = []string{"blog.css", "colors-blog.css"} } @@ -441,17 +449,16 @@ func (a *App) renderErrorf(w http.ResponseWriter, r *http.Request, status int, l a.render(w, "error", a.makePage(r, lang, "GNU Taler Demo: Error", content), status) } -func env(name, fallback string) string { - if value := strings.TrimSpace(getenv(name)); value != "" { +func configuredURL(value string) string { + if value = strings.TrimSpace(value); value != "" { return value } - return fallback + return "#" } -var getenv = os.Getenv - func appLanguageURL(base, lang string) string { - if base == "#" || base == "" { + base = configuredURL(base) + if base == "#" { return "#" } return strings.TrimRight(base, "/") + "/" + url.PathEscape(lang) + "/" diff --git a/internal/web/app_test.go b/internal/web/app_test.go @@ -86,6 +86,36 @@ func TestLandingRoutesAndEmbeddedStatic(t *testing.T) { } } +func TestPagesUseConfiguredPublicURLs(t *testing.T) { + app, err := New(Options{ + Shop: "landing", Currency: "KUDOS", + PublicURLs: PublicURLs{ + Landing: "https://landing.example/", Bank: "https://bank.example/", + Blog: "https://blog.example/", Donations: "https://donations.example/", + }, + }) + if err != nil { + t.Fatal(err) + } + response := httptest.NewRecorder() + app.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/de/", nil)) + if response.Code != http.StatusOK { + t.Fatalf("response status = %d", response.Code) + } + body := response.Body.String() + for _, expected := range []string{ + `href="https://landing.example/de/"`, + `href="https://bank.example/?lang=de"`, + `href="https://bank.example"`, + `href="https://blog.example/de/"`, + `href="https://donations.example/de/"`, + } { + if !strings.Contains(body, expected) { + t.Errorf("response missing configured public URL %q", expected) + } + } +} + func TestDemoPagesUseDistinctBrandThemes(t *testing.T) { merchant, err := backend.New("https://merchant.example/", "secret-token:test") if err != nil { diff --git a/internal/web/blog.go b/internal/web/blog.go @@ -57,7 +57,7 @@ func (a *App) blogIndex(w http.ResponseWriter, r *http.Request) { articles := sortedArticles(a.articles[lang]) content := blogIndexContent{ Articles: articles, - BankURL: env("TALER_ENV_URL_BANK", "#"), + BankURL: configuredURL(a.opts.PublicURLs.Bank), Price: a.opts.Currency + ":" + articlePriceUnits, } a.render(w, "blog-index", a.makePage(r, lang, "GNU Taler Demo: Essay Shop", content), http.StatusOK) diff --git a/internal/web/landing.go b/internal/web/landing.go @@ -3,7 +3,6 @@ package web import ( "net/http" "net/url" - "os" "strings" ) @@ -25,15 +24,15 @@ func (a *App) landing(w http.ResponseWriter, r *http.Request) { a.renderError(w, r, http.StatusNotFound, "en", "Page not found", nil) return } - bank := strings.TrimRight(os.Getenv("TALER_ENV_URL_BANK"), "/") + bank := strings.TrimRight(a.opts.PublicURLs.Bank, "/") bankRegister := "#" bankPublic := "#" if bank != "" { bankRegister = bank bankPublic = bank + "#public-accounts" } - localizedMerchant := func(name string) string { - base := strings.TrimRight(os.Getenv(name), "/") + localizedMerchant := func(base string) string { + base = strings.TrimRight(base, "/") if base == "" { return "#" } @@ -42,8 +41,8 @@ func (a *App) landing(w http.ResponseWriter, r *http.Request) { content := landingContent{ Currency: a.opts.Currency, BankRegisterURL: bankRegister, BankPublicAccountsURL: bankPublic, - BlogURL: localizedMerchant("TALER_ENV_URL_MERCHANT_BLOG"), - DonationsURL: localizedMerchant("TALER_ENV_URL_MERCHANT_DONATIONS"), + BlogURL: localizedMerchant(a.opts.PublicURLs.Blog), + DonationsURL: localizedMerchant(a.opts.PublicURLs.Donations), } a.render(w, "landing", a.makePage(r, lang, "GNU Taler Demo", content), http.StatusOK) }