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:
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)
}