libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit 743b40b1062b0c62d5f1475acd7c121378baccaa
parent 3a107bb8b15e04c50f9b5a9f41ab64ec6dc24d48
Author: Florian Dold <dold@taler.net>
Date:   Mon, 24 Aug 2026 01:52:03 +0200

libeufin-bank: load WebUI from separate package

Diffstat:
M.gitmodules | 4----
MMakefile | 3+--
MREADME | 15++++++++-------
Mbootstrap | 1-
Dcontrib/bank-spa.lock | 2--
Mcontrib/bank.conf | 4++--
Dcontrib/check-prebuilt | 15---------------
Mcontrib/ci/jobs/0-codespell/job.sh | 1-
Acontrib/spa/index.html | 11+++++++++++
Dcontrib/wallet-core | 1-
Mdebian/control | 1+
Ddebian/etc/libeufin/settings.json | 34----------------------------------
Mdebian/libeufin-bank.install | 7++-----
Mlibeufin-bank/src/main/kotlin/tech/libeufin/bank/Config.kt | 2++
Mlibeufin-bank/src/main/kotlin/tech/libeufin/bank/Main.kt | 17++++++++++++-----
Alibeufin-bank/src/test/kotlin/WebUiTest.kt | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
16 files changed, 153 insertions(+), 79 deletions(-)

diff --git a/.gitmodules b/.gitmodules @@ -1,10 +1,6 @@ [submodule "build-system/taler-build-scripts"] path = build-system/taler-build-scripts url = ../build-common.git -[submodule "contrib/wallet-core"] - path = contrib/wallet-core - url = ../taler-typescript-core.git - branch = prebuilt [submodule "doc/prebuilt"] path = doc/prebuilt url = ../taler-docs.git diff --git a/Makefile b/Makefile @@ -57,7 +57,7 @@ install-nobuild-files: install -D -t $(bin_dir) contrib/libeufin-ebisync-dbconfig install -D -t $(bin_dir) contrib/libeufin-tan-*.sh install -d $(share_dir)/libeufin/spa - cp contrib/wallet-core/bank/* $(share_dir)/libeufin/spa/ + cp contrib/spa/* $(share_dir)/libeufin/spa/ install -d $(share_dir)/libeufin-ebisync/spa cp libeufin-ebisync/src/spa/* $(share_dir)/libeufin-ebisync/spa/ @@ -137,4 +137,3 @@ bank-bench-db: install-nobuild-files .PHONY: nexus-bench-db nexus-bench-db: install-nobuild-files ./gradlew cleanTest :libeufin-nexus:test --tests Bench.benchDb -i --no-build-cache - diff --git a/README b/README @@ -72,13 +72,14 @@ executable found under the "bin/" folder. User interface ============== -This repository does not ship any UI, rather it downloads -one from the following project along the "make deb" target: -https://git.taler.net/wallet-core.git/tree/packages/bank-ui +The LibEuFin Bank WebUI is built separately from the +`libeufin-bank-webui` package in taler-typescript-core: +https://git.taler.net/taler-typescript-core.git/tree/packages/libeufin-bank-webui -This way, the libeufin-bank Debian package provides one -self-contained solution including Nginx, LibEuFin Bank, -and the UI. +The `libeufin-bank` Debian package depends on `libeufin-bank-webui` and +serves its installed files from `$PREFIX/share/libeufin-bank-webui/`. +When those files are unavailable, the bank serves a small fallback page +with installation guidance. -Note: the UI an independent Web app that could even be served +Note: the UI is an independent Web app that could even be served from a different host than the one running the backend. diff --git a/bootstrap b/bootstrap @@ -21,6 +21,5 @@ git config --local submodule.recurse true git submodule sync git submodule update --init -./contrib/check-prebuilt rm -f ./configure cp build-system/taler-build-scripts/configure ./configure diff --git a/contrib/bank-spa.lock b/contrib/bank-spa.lock @@ -1 +0,0 @@ -1.5.14 -\ No newline at end of file diff --git a/contrib/bank.conf b/contrib/bank.conf @@ -76,8 +76,8 @@ BIND_TO = 0.0.0.0 # Which unix domain path should we bind to? Only used if SERVE is unix. # UNIXPATH = libeufin-bank.sock -# Path to spa files -SPA = $DATADIR/spa/ +# Path to the separately installed WebUI files +SPA = $PREFIX/share/libeufin-bank-webui/ # Exchange that is suggested to wallets when withdrawing. # SUGGESTED_WITHDRAWAL_EXCHANGE = https://exchange.demo.taler.net/ diff --git a/contrib/check-prebuilt b/contrib/check-prebuilt @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 - -import os -import sys - -contrib = os.path.abspath(os.path.dirname(__file__)) - -bank_ver_lock = open(contrib + "/" + "bank-spa.lock").read().strip() -bank_ver_prebuilt = open(contrib + "/" + "wallet-core/bank/version.txt").read().strip() - -if bank_ver_lock != bank_ver_prebuilt: - print("bank SPA version mismatch: bank-spa.lock") - print("lockfile has version", bank_ver_lock) - print("prebuilt has version", bank_ver_prebuilt) - sys.exit(1) diff --git a/contrib/ci/jobs/0-codespell/job.sh b/contrib/ci/jobs/0-codespell/job.sh @@ -14,7 +14,6 @@ configure~ */.git/* */.gradle/* */contrib/ci/* -*/contrib/wallet-core/* */frontend/* */build/* */*.xsd diff --git a/contrib/spa/index.html b/contrib/spa/index.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> +<body> + <h1>LibEuFin Bank Web UI not installed</h1> + <p>This is a fallback page shown when the LibEuFin Bank Web UI is not + available.</p> + <p>Please make sure the package is installed and the + <code>SPA</code> option in the <code>[libeufin-bank]</code> section of + the LibEuFin configuration is set correctly.</p> +</body> +</html> diff --git a/contrib/wallet-core b/contrib/wallet-core @@ -1 +0,0 @@ -Subproject commit 45c7768f9153846198dac99b6a9015477b9d7d2a diff --git a/debian/control b/debian/control @@ -20,6 +20,7 @@ Architecture: all Depends: default-jre-headless | java-runtime-headless (>= 17), libeufin-common (= ${binary:Version}), + libeufin-bank-webui, ${misc:Depends} Recommends: nginx | apache2 | httpd, diff --git a/debian/etc/libeufin/settings.json b/debian/etc/libeufin/settings.json @@ -1,34 +0,0 @@ -// This file is an example of configuration of Bank SPA -// Remove all the comments to make the file a valid -// JSON file, otherwise no value here will make any -// effect. -// All the settings are optionals. -{ - // Where libeufin backend is localted - // default: window.origin without "webui/" - "backendBaseURL": "http://bank.taler.test:1180/", - // Shows a button "create random account" in the registration form - // Useful for testing - // default: false - "allowRandomAccountCreation": false, - // Create all random accounts with password "123" - // Useful for testing - // default: false - "simplePasswordForRandomAccounts": false, - // Bank name shown in the header - // default: "Taler Bank" - "bankName": "Taler TESTING Bank", - // URL where the user is going to be redirected after - // clicking in Taler Logo - // default: home page - "iconLinkURL": "#", - // Mapping for every link shown in the top navitation bar - // - key: link label, what the user will read - // - value: link target, where the user is going to be redirected - // default: empty list - "topNavSites": { - "Exchange": "http://exchange.taler.test:1180/", - "Bank": "http://bank-ui.taler.test:1180/", - "Merchant": "http://merchant.taler.test:1180/" - } -} diff --git a/debian/libeufin-bank.install b/debian/libeufin-bank.install @@ -1,15 +1,13 @@ debian/etc/libeufin/libeufin-bank.conf etc/libeufin/ debian/etc/nginx/sites-available/libeufin-bank etc/nginx/sites-available/ debian/etc/apache2/sites-available/libeufin-bank.conf etc/apache2/sites-available/ -debian/etc/libeufin/settings.json etc/libeufin/ - libeufin-bank/build/install/libeufin-bank-shadow/bin/libeufin-bank usr/bin/ contrib/libeufin-bank-dbinit usr/bin/ contrib/libeufin-tan-*.sh usr/bin/ database-versioning/libeufin-bank*.sql usr/share/libeufin/sql/ -contrib/wallet-core/bank/* usr/share/libeufin/spa +contrib/spa/* usr/share/libeufin/spa contrib/bank.conf usr/share/libeufin/config.d/ @@ -19,4 +17,4 @@ libeufin-bank/build/install/libeufin-bank-shadow/lib/libeufin-bank-all.jar usr/l doc/prebuilt/man/libeufin-bank.1 usr/share/man/man1 doc/prebuilt/man/libeufin-bank.conf.5 usr/share/man/man5 -debian/libeufin-bank.conf /usr/lib/sysusers.d/ -\ No newline at end of file +debian/libeufin-bank.conf /usr/lib/sysusers.d/ diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/Config.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/Config.kt @@ -48,6 +48,7 @@ data class BankConfig( val fiatCurrency: String?, val fiatCurrencySpec: CurrencySpecification?, val spaPath: Path?, + val fallbackSpaPath: Path, val tanChannels: Map<TanChannel, Pair<Path, Map<String, String>>>, val payto: BankPaytoCtx, val wireMethod: WireMethod, @@ -179,6 +180,7 @@ private fun TalerConfig.loadBankConfig(): BankConfig = section("libeufin-bank"). maxAmount = amount("max_wire_transfer_amount", regionalCurrency).default(MAX), suggestedWithdrawalExchange = string("suggested_withdrawal_exchange").orNull(), spaPath = path("spa").orNull(), + fallbackSpaPath = this@loadBankConfig.section("paths").path("datadir").require().resolve("spa"), baseUrl = baseUrl, fiatCurrency = fiatCurrency, fiatCurrencySpec = fiatCurrencySpec, diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -32,6 +32,7 @@ import tech.libeufin.common.api.OpenApiInfo import tech.libeufin.common.api.talerApi import tech.libeufin.common.VERSION import com.github.ajalt.clikt.core.main +import kotlin.io.path.isDirectory val logger: Logger = LoggerFactory.getLogger("libeufin-bank") @@ -65,15 +66,21 @@ fun Application.corebankWebApp(db: Database, cfg: BankConfig, serveSpec: Boolean preparedTransferApi(db, cfg) revenueApi(db, cfg) observabilityApi(db, cfg) - cfg.spaPath?.let { - get("/") { - call.respondRedirect("/webui/") + val spaPath = cfg.spaPath?.takeIf { it.isDirectory() } ?: run { + cfg.spaPath?.let { + logger.warn("SPA not found at '{}', using fallback", it) } - staticFiles("/webui/", it.toFile()) + check(cfg.fallbackSpaPath.isDirectory()) { + "SPA fallback directory not found at '${cfg.fallbackSpaPath}'" + } + cfg.fallbackSpaPath + } + get("/") { + call.respondRedirect("/webui/") } + staticFiles("/webui/", spaPath.toFile()) } fun main(args: Array<String>) { LibeufinBank().main(args) } - diff --git a/libeufin-bank/src/test/kotlin/WebUiTest.kt b/libeufin-bank/src/test/kotlin/WebUiTest.kt @@ -0,0 +1,114 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2026 Taler Systems S.A. + + * LibEuFin is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation; either version 3, or + * (at your option) any later version. + + * LibEuFin 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 + * Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public + * License along with LibEuFin; see the file COPYING. If not, see + * <http://www.gnu.org/licenses/> + */ + +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import io.ktor.http.HttpStatusCode +import io.ktor.server.testing.testApplication +import org.junit.Test +import sun.misc.Unsafe +import tech.libeufin.bank.BankConfig +import tech.libeufin.bank.bankConfig +import tech.libeufin.bank.corebankWebApp +import tech.libeufin.bank.db.Database +import java.nio.file.Path +import kotlin.io.path.createDirectories +import kotlin.io.path.createTempDirectory +import kotlin.io.path.createTempFile +import kotlin.io.path.readText +import kotlin.io.path.writeText +import kotlin.test.assertContains +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class WebUiTest { + private fun fakeDatabase(): Database { + val field = Unsafe::class.java.getDeclaredField("theUnsafe") + field.isAccessible = true + val unsafe = field.get(null) as Unsafe + return unsafe.allocateInstance(Database::class.java) as Database + } + + private fun config(dataDir: Path, spaPath: Path?): BankConfig { + val file = createTempFile(suffix = ".conf") + file.writeText(buildString { + appendLine(Path.of("conf/test_no_conversion.conf").readText()) + appendLine("[paths]") + appendLine("DATADIR = $dataDir") + if (spaPath != null) { + appendLine("[libeufin-bank]") + appendLine("SPA = $spaPath") + } + }) + return bankConfig(file) + } + + @Test + fun configuredSpa() { + val root = createTempDirectory() + val spa = root.resolve("webui").createDirectories() + spa.resolve("index.html").writeText("configured WebUI") + val cfg = config(root.resolve("data"), spa) + + testApplication { + application { corebankWebApp(fakeDatabase(), cfg) } + + val noRedirectClient = createClient { followRedirects = false } + val redirect = noRedirectClient.get("/") + assertEquals(HttpStatusCode.Found, redirect.status) + assertEquals("/webui/", redirect.headers["Location"]) + + val response = client.get("/webui/") + assertEquals(HttpStatusCode.OK, response.status) + assertEquals("configured WebUI", response.bodyAsText()) + } + } + + @Test + fun fallbackSpa() { + val root = createTempDirectory() + val fallback = root.resolve("spa").createDirectories() + fallback.resolve("index.html").writeText("fallback WebUI") + val configuredFile = root.resolve("not-a-directory") + configuredFile.writeText("not a WebUI") + val cfg = config(root, configuredFile) + + testApplication { + application { corebankWebApp(fakeDatabase(), cfg) } + + val response = client.get("/webui/") + assertEquals(HttpStatusCode.OK, response.status) + assertEquals("fallback WebUI", response.bodyAsText()) + } + } + + @Test + fun missingFallbackFails() { + val root = createTempDirectory() + val cfg = config(root.resolve("missing-data"), root.resolve("missing-webui")) + + val error = assertFailsWith<IllegalStateException> { + testApplication { + application { corebankWebApp(fakeDatabase(), cfg) } + client.get("/") + } + } + assertContains(error.message.orEmpty(), "SPA fallback directory not found") + } +}