From dd5aa2145f06a4744cf8f4f3fe14525b613acf87 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 26 Mar 2024 16:54:35 -0300 Subject: fix #8660 --- .../create_merchantAndBankAccount_pdf.sh | 23 ++ packages/taler-harness/pdf-template.html | 65 ++++ packages/taler-harness/src/index.ts | 393 ++++++++++++++++++--- 3 files changed, 428 insertions(+), 53 deletions(-) create mode 100644 packages/taler-harness/create_merchantAndBankAccount_pdf.sh create mode 100644 packages/taler-harness/pdf-template.html (limited to 'packages/taler-harness') diff --git a/packages/taler-harness/create_merchantAndBankAccount_pdf.sh b/packages/taler-harness/create_merchantAndBankAccount_pdf.sh new file mode 100644 index 000000000..4593cc9a0 --- /dev/null +++ b/packages/taler-harness/create_merchantAndBankAccount_pdf.sh @@ -0,0 +1,23 @@ +DATA=$(mktemp) +set -e + +[ -z "$1" ] && echo First parameter must be the json file result from \'taler-harness deployment provision-bank-and-merchant\'. Alternative \'-\' can be used if the file is provided from stdin. && exit 1 + +cat $1 > $DATA + +[ -z "$(jq -r '.bankUser//empty' $DATA)" ] && echo the json file is not complete: missing bankUser && exit 1 +[ -z "$(jq -r '.bankURL//empty' $DATA)" ] && echo the json file is not complete: missing bankURL && exit 1 +[ -z "$(jq -r '.merchantURL//empty' $DATA)" ] && echo the json file is not complete: missing merchantURL && exit 1 +[ -z "$(jq -r '.templateURI//empty' $DATA)" ] && echo the json file is not complete: missing templateURI && exit 1 +[ -z "$(jq -r '.password//empty' $DATA)" ] && echo the json file is not complete: missing password && exit 1 + +add_qr_image(){ + jq -r $1 $DATA | qrencode -l Q -m 2 -s 5 -o - | base64 -w 0 | jq -Rn '{"'$2'":inputs}' | jq -s add - $DATA | sponge $DATA +} + +add_qr_image .templateURI templateQR +add_qr_image .bankURL bankQR +add_qr_image .merchantURL merchantQR + +chevron pdf-template.html -d $DATA | wkhtmltopdf - out.pdf + diff --git a/packages/taler-harness/pdf-template.html b/packages/taler-harness/pdf-template.html new file mode 100644 index 000000000..d308d67c4 --- /dev/null +++ b/packages/taler-harness/pdf-template.html @@ -0,0 +1,65 @@ + + + +

Account information

+ +

The information in this page is confidentail, do not share with others.

+ +

Bank

+

+ In your bank account you will be able to see how much revenue + has been consolidated. +

+
+ +
+

URL: {{bankURL}}

+

accounts id: {{bankUser}}

+

password: {{password}}

+
+ +
+
+ +
bank URL
+
+
+
+ +
+ +

Backoffice

+

+ In this site you will be able to see how much are you selling, + make refunds or create new QR codes. +

+ +
+
+

URL: {{merchantURL}}

+

password: {{password}}

+
+
+
+ +
merchant URL
+
+
+
+ +
+
+ +
+

Payme QR code

+

+ The following QR code can be utilized in + public settings to request payments. +

+
+ +
{{templateURI}}
+
+ + + \ No newline at end of file diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts index 3bec1698a..4b0319a3e 100644 --- a/packages/taler-harness/src/index.ts +++ b/packages/taler-harness/src/index.ts @@ -18,6 +18,7 @@ * Imports. */ import { + AccessToken, AmountString, Amounts, BalancesResponse, @@ -25,14 +26,23 @@ import { Duration, HttpStatusCode, Logger, - MerchantInstanceConfig, - RegisterAccountRequest, + PaytoString, + TalerAuthenticationHttpClient, + TalerBankConversionHttpClient, + TalerCoreBankHttpClient, + TalerErrorCode, + TalerMerchantInstanceHttpClient, + TalerMerchantManagementHttpClient, TransactionsResponse, decodeCrock, + encodeCrock, generateIban, j2s, + randomBytes, rsaBlind, setGlobalLogLevelFromString, + setPrintHttpRequestAsCurl, + stringifyPayTemplateUri } from "@gnu-taler/taler-util"; import { clk } from "@gnu-taler/taler-util/clk"; import { @@ -69,6 +79,7 @@ import { } from "./harness/helpers.js"; import { getTestInfo, runTests } from "./integrationtests/testrunner.js"; import { lintExchangeDeployment } from "./lint.js"; +import { randomUUID } from "crypto"; const logger = new Logger("taler-harness:index.ts"); @@ -585,6 +596,291 @@ deploymentCli console.log(generateIban(args.genIban.countryCode, args.genIban.length)); }); +deploymentCli + .subcommand("provisionBankMerchant", "provision-bank-and-merchant", { + help: "Provision a bank account, merchant instance and link them together.", + }) + .requiredArgument("merchantApiBaseUrl", clk.STRING, { + help: "URL location of the merchant backend" + }) + .requiredArgument("corebankApiBaseUrl", clk.STRING, { + help: "URL location of the libeufin bank backend" + }) + .requiredOption("merchantToken", ["--merchant-management-token"], clk.STRING, { + help: "acces token of the default instance in the merchant backend" + }) + .maybeOption("bankToken", ["--bank-admin-token"], clk.STRING, { + help: "libeufin bank admin's password if the account creation is restricted" + }) + .requiredOption("name", ["--legal-name"], clk.STRING, { + help: "legal name of the merchant" + }) + .maybeOption("email", ["--email"], clk.STRING, { + help: "email contact of the merchant" + }) + .maybeOption("phone", ["--phone"], clk.STRING, { + help: "phone contact of the merchant" + }) + .requiredOption("id", ["--id"], clk.STRING, { + help: "login id for the bank account and instance id of the merchant backend" + }) + .flag("template", ["--create-template"], { + help: "use this flag to create a default template for the merchant with fixed summary" + }) + .requiredOption("password", ["--password"], clk.STRING, { + help: "password of the accounts in libeufin bank and merchant backend" + }) + .flag("randomPassword", ["--set-random-password"], { + help: "if everything worked ok, change the password of the accounts at the end" + }) + .action(async (args) => { + const managementToken = args.provisionBankMerchant.merchantToken as AccessToken; + const bankAdminPassword = args.provisionBankMerchant.bankToken as AccessToken; + const id = args.provisionBankMerchant.id; + const name = args.provisionBankMerchant.name; + const email = args.provisionBankMerchant.email; + const phone = args.provisionBankMerchant.phone; + const password = args.provisionBankMerchant.password; + + + const httpLib = createPlatformHttpLib({}); + const merchantManager = new TalerMerchantManagementHttpClient(args.provisionBankMerchant.merchantApiBaseUrl, httpLib); + const bank = new TalerCoreBankHttpClient(args.provisionBankMerchant.corebankApiBaseUrl, httpLib); + const instanceURL = merchantManager.getSubInstanceAPI(id).href + const merchantInstance = new TalerMerchantInstanceHttpClient(instanceURL, httpLib); + const conv = new TalerBankConversionHttpClient(bank.getConversionInfoAPI().href, httpLib) + const bankAuth = new TalerAuthenticationHttpClient(bank.getAuthenticationAPI(id).href, httpLib) + + + const bc = await bank.getConfig() + if (!bank.isCompatible(bc.body.version)) { + logger.error( + `bank server version is not compatible: ${bc.body.version}, client version: ${bank.PROTOCOL_VERSION}`, + ); + return; + } + const mc = await merchantManager.getConfig() + if (!merchantManager.isCompatible(mc.body.version)) { + logger.error( + `merchant server version is not compatible: ${mc.body.version}, client version: ${merchantManager.PROTOCOL_VERSION}`, + ); + return; + } + + /** + * create bank account + */ + let accountPayto: PaytoString; + { + const resp = await bank.createAccount(bankAdminPassword, { + name: name, + password: password, + username: id, + contact_data: email || phone ? { + email: email, + phone: phone, + } : undefined, + }) + + if (resp.type === "fail") { + logger.error(`unable to provision bank account, HTTP response status ${resp.case}`); + process.exit(2); + } + logger.info(`account ${id} successfully provisioned`); + accountPayto = resp.body.internal_payto_uri + } + + /** + * create merchant account + */ + { + const resp = await merchantManager.createInstance(managementToken, { + address: {}, + auth: { + method: "token", + token: `secret-token:${password}`, + }, + default_pay_delay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ hours: 1 }), + ), + default_wire_transfer_delay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ hours: 1 }), + ), + id: id, + jurisdiction: {}, + name: name, + use_stefan: true, + }) + + if (resp.type === "ok") { + logger.info(`instance ${id} created successfully`); + } else if (resp.case === HttpStatusCode.Conflict) { + logger.info(`instance ${id} already exists`); + } else { + logger.error( + `unable to create instance ${id}, HTTP status ${resp.case}`, + ); + process.exit(2); + } + } + + let wireAccount: string; + /** + * link bank account and merchant + */ + { + const resp = await merchantInstance.addAccount(password as AccessToken, { + payto_uri: accountPayto, + credit_facade_url: bank.getRevenueAPI(id).href, + credit_facade_credentials: { + type: "basic", + username: id, + password: password, + } + }) + if (resp.type === "fail") { + console.error(`unable to configure bank account for instance ${id}, status ${resp.case}`) + console.error(j2s(resp.detail)); + process.exit(2); + } + wireAccount = resp.body.h_wire + } + + logger.info(`successfully configured bank account for ${id}`); + + let templateURI; + /** + * create template + */ + if (args.provisionBankMerchant.template) { + let currency = bc.body.currency; + if (bc.body.allow_conversion) { + const cc = await conv.getConfig(); + if (cc.type === "ok") { + currency = cc.body.fiat_currency + } else { + console.error( + `could not get fiat currency status ${cc.case}`, + ); + console.error(j2s(cc.detail)); + } + } else { + console.log(`conversion is disabled, using bank currency`) + } + + { + const resp = await merchantInstance.addTemplate(password as AccessToken, { + template_id: "default", + template_description: "First template", + template_contract: { + pay_duration: Duration.toTalerProtocolDuration( + Duration.fromSpec({ hours: 1 }), + ), + minimum_age: 0, + currency, + summary: "Pay me!" + } + }) + if (resp.type === "fail") { + console.error(`unable to create template for insntaince ${id}, status ${resp.case}`) + console.error(j2s(resp.detail)); + process.exit(2); + } + } + + logger.info(`template default successfully created`); + templateURI = stringifyPayTemplateUri({ + merchantBaseUrl: instanceURL, + templateId: "default", + templateParams: { + amount: currency + } + }) + } + + let finalPassword = password; + if (args.provisionBankMerchant.randomPassword) { + const prevPassword = password as AccessToken + const randomPassword = encodeCrock(randomBytes(16)); + logger.info("random password: ", randomPassword) + let token: AccessToken; + { + const resp = await bankAuth.createAccessTokenBasic(id, prevPassword, { + scope: "readwrite", + duration: Duration.toTalerProtocolDuration(Duration.fromSpec({ minutes: 1 })), + refreshable: false, + }) + if (resp.type === "fail") { + console.error(`unable to login into bank accountfor user ${id}, status ${resp.case}`) + console.error(j2s(resp.detail)); + process.exit(2); + } + token = resp.body.access_token; + } + + { + const resp = await bank.updatePassword({ username: id, token }, { + old_password: prevPassword, + new_password: randomPassword, + }); + if (resp.type === "fail") { + console.error(`unable to change bank pasword for user ${id}, status ${resp.case}`) + if (resp.case !== HttpStatusCode.Accepted) { + console.error(j2s(resp.detail)); + } else { + console.error("2FA required") + } + process.exit(2); + } + } + + { + const resp = await merchantInstance.updateCurrentInstanceAuthentication(prevPassword, { + method: "token", + token: `secret-token:${randomPassword}` as AccessToken + }) + if (resp.type === "fail") { + console.error(`unable to change merchant password for instance ${id}, status ${resp.case}`) + console.error(j2s(resp.detail)); + process.exit(2); + } + } + + { + const resp = await merchantInstance.updateAccount(randomPassword as AccessToken, wireAccount, { + credit_facade_url: bank.getRevenueAPI(id).href, + credit_facade_credentials: { + type: "basic", + username: id, + password: randomPassword, + } + }) + if (resp.type != "ok") { + console.error( + `unable to update bank account for instance ${id}, status ${resp.case}`, + ); + console.error(j2s(resp.detail)); + process.exit(2); + } + } + finalPassword = randomPassword; + } + logger.info(`successfully configured bank account for ${id}`); + + /** + * show result + */ + console.log(JSON.stringify({ + bankUser: id, + bankURL: args.provisionBankMerchant.corebankApiBaseUrl, + merchantURL: instanceURL, + templateURI, + password: finalPassword, + }, undefined, 2)) + + }); + + deploymentCli .subcommand("provisionMerchantInstance", "provision-merchant-instance", { help: "Provision a merchant backend instance.", @@ -595,17 +891,27 @@ deploymentCli .requiredOption("name", ["--name"], clk.STRING) .requiredOption("id", ["--id"], clk.STRING) .requiredOption("payto", ["--payto"], clk.STRING) + .maybeOption("bankURL", ["--bankURL"], clk.STRING) + .maybeOption("bankUser", ["--bankUser"], clk.STRING) + .maybeOption("bankPassword", ["--bankPassword"], clk.STRING) .action(async (args) => { - const httpLib = createPlatformHttpLib(); + const httpLib = createPlatformHttpLib({}); const baseUrl = args.provisionMerchantInstance.merchantApiBaseUrl; - const managementToken = args.provisionMerchantInstance.managementToken; - const instanceToken = args.provisionMerchantInstance.instanceToken; + const api = new TalerMerchantManagementHttpClient(baseUrl, httpLib) + const managementToken = args.provisionMerchantInstance.managementToken as AccessToken; + const instanceToken = args.provisionMerchantInstance.instanceToken as AccessToken; const instanceId = args.provisionMerchantInstance.id; - const body: MerchantInstanceConfig = { + const instancceName = args.provisionMerchantInstance.name; + const bankURL = args.provisionMerchantInstance.bankURL; + const bankUser = args.provisionMerchantInstance.bankUser; + const bankPassword = args.provisionMerchantInstance.bankPassword; + const accountPayto = args.provisionMerchantInstance.payto as PaytoString; + + const createResp = await api.createInstance(managementToken, { address: {}, auth: { method: "token", - token: args.provisionMerchantInstance.instanceToken, + token: `secret-token:${instanceToken}`, }, default_pay_delay: Duration.toTalerProtocolDuration( Duration.fromSpec({ hours: 1 }), @@ -613,48 +919,35 @@ deploymentCli default_wire_transfer_delay: { d_us: 1 }, id: instanceId, jurisdiction: {}, - name: args.provisionMerchantInstance.name, + name: instancceName, use_stefan: true, - }; - const url = new URL("management/instances", baseUrl); - const createResp = await httpLib.fetch(url.href, { - method: "POST", - body, - headers: { - Authorization: `Bearer ${managementToken}`, - }, - }); - if (createResp.status >= 200 && createResp.status <= 299) { + }) + + if (createResp.type === "ok") { logger.info(`instance ${instanceId} created successfully`); - } else if (createResp.status === HttpStatusCode.Conflict) { + } else if (createResp.case === HttpStatusCode.Conflict) { logger.info(`instance ${instanceId} already exists`); } else { logger.error( - `unable to create instance ${instanceId}, HTTP status ${createResp.status}`, + `unable to create instance ${instanceId}, HTTP status ${createResp.case}`, ); process.exit(2); } - const accountsUrl = new URL( - `instances/${instanceId}/private/accounts`, - baseUrl, - ); - const accountBody = { - payto_uri: args.provisionMerchantInstance.payto, - }; - const createAccountResp = await httpLib.fetch(accountsUrl.href, { - method: "POST", - body: accountBody, - headers: { - Authorization: `Bearer ${instanceToken}`, - }, - }); - if (createAccountResp.status != 200) { + const createAccountResp = await api.addAccount(instanceToken, { + payto_uri: accountPayto, + credit_facade_url: bankURL, + credit_facade_credentials: bankUser && bankPassword ? { + type: "basic", + username: bankUser, + password: bankPassword, + } : undefined + }) + if (createAccountResp.type != "ok") { console.error( - `unable to configure bank account for instance ${instanceId}, status ${createAccountResp.status}`, + `unable to configure bank account for instance ${instanceId}, status ${createAccountResp.case}`, ); - const resp = await createAccountResp.json(); - console.error(j2s(resp)); + console.error(j2s(createAccountResp.detail)); process.exit(2); } logger.info(`successfully configured bank account for ${instanceId}`); @@ -673,31 +966,25 @@ deploymentCli .maybeOption("internalPayto", ["--payto"], clk.STRING) .action(async (args) => { const httpLib = createPlatformHttpLib(); - const corebankApiBaseUrl = args.provisionBankAccount.corebankApiBaseUrl; - const url = new URL("accounts", corebankApiBaseUrl); + const baseUrl = args.provisionBankAccount.corebankApiBaseUrl; + const api = new TalerCoreBankHttpClient(baseUrl, httpLib); + const accountLogin = args.provisionBankAccount.login; - const body: RegisterAccountRequest = { + const resp = await api.createAccount(undefined, { name: args.provisionBankAccount.name, password: args.provisionBankAccount.password, username: accountLogin, is_public: !!args.provisionBankAccount.public, is_taler_exchange: !!args.provisionBankAccount.exchange, - payto_uri: args.provisionBankAccount.internalPayto, - }; - const resp = await httpLib.fetch(url.href, { - method: "POST", - body, - }); - if (resp.status >= 200 && resp.status <= 299) { + payto_uri: args.provisionBankAccount.internalPayto as PaytoString, + }) + + if (resp.type === "ok") { logger.info(`account ${accountLogin} successfully provisioned`); return; } - if (resp.status === HttpStatusCode.Conflict) { - logger.info(`account ${accountLogin} already provisioned`); - return; - } logger.error( - `unable to provision bank account, HTTP response status ${resp.status}`, + `unable to provision bank account, HTTP response status ${resp.case}`, ); process.exit(2); }); -- cgit v1.2.3