taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit b4a2c06b29efe308f2d3aa1d0e790f23d2508d52
parent 5d5ab36686029d4876fbc24171503d606273a767
Author: Sebastian <sebasjm@gmail.com>
Date:   Thu, 16 Oct 2025 11:23:29 -0300

fix #10498

Diffstat:
Mpackages/merchant-backoffice-ui/src/paths/newAccount/index.tsx | 22++++++++++++++++++----
Mpackages/taler-harness/bin/taler-harness.mjs | 2+-
Apackages/taler-harness/src/integrationtests/test-merchant-self-provision-activation-and-login.ts | 184+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 2++
Mpackages/taler-util/src/codec.ts | 2+-
Mpackages/taler-util/src/http-client/merchant.ts | 16+++++++++++++++-
Mpackages/taler-util/src/types-taler-merchant.ts | 4++++
7 files changed, 225 insertions(+), 7 deletions(-)

diff --git a/packages/merchant-backoffice-ui/src/paths/newAccount/index.tsx b/packages/merchant-backoffice-ui/src/paths/newAccount/index.tsx @@ -14,7 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { HttpStatusCode, MerchantAuthMethod } from "@gnu-taler/taler-util"; +import { Duration, HttpStatusCode, MerchantAuthMethod } from "@gnu-taler/taler-util"; import { undefinedIfEmpty, useChallengeHandler, @@ -38,6 +38,7 @@ import { PHONE_JUST_NUMBERS_REGEX, } from "../../utils/constants.js"; import { Notification } from "../../utils/types.js"; +import { FOREVER_REFRESHABLE_TOKEN } from "../login/index.js"; export interface Account { id: string; @@ -56,7 +57,14 @@ export function NewAccount({ onCancel, onCreated }: Props): VNode { const { state: session, lib, logIn } = useSessionContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); - const [value, setValue] = useState<Partial<Account>>({}); + const [value, setValue] = useState<Partial<Account>>({ + id: "rnd"+ Math.random(), + email:"ccc@cc.com", + name: "testing instance", + password: "ccc", + repeat:"ccc", + phone:"+54", + }); const errors = undefinedIfEmpty<FormErrors<Account>>({ id: !value.id ? i18n.str`Required` @@ -102,6 +110,7 @@ export function NewAccount({ onCancel, onCreated }: Props): VNode { ({ challengeIds, onChallengeRequired }) => async function doCreateImpl() { try { + const id = value.id! const resp = await lib.instance.createInstanceSelfProvision( { address: {}, @@ -115,14 +124,16 @@ export function NewAccount({ onCancel, onCreated }: Props): VNode { default_wire_transfer_delay: { d_us: 1000, }, - id: value.id!, + id, jurisdiction: {}, name: value.name!, use_stefan: true, email: value.email!, phone_number: value.phone, }, - { challengeIds }, + { + tokenValidity: Duration.fromSpec({months: 6}), + challengeIds }, ); if (resp.type === "fail") { @@ -137,6 +148,9 @@ export function NewAccount({ onCancel, onCreated }: Props): VNode { } return; } + if (resp.body) { + logIn(id, resp.body.access_token) + } onCreated(); } catch (error) { setNotif({ diff --git a/packages/taler-harness/bin/taler-harness.mjs b/packages/taler-harness/bin/taler-harness.mjs @@ -1,4 +1,4 @@ -#!/usr/bin/env node +#!/usr/bin/env -S node --enable-source-maps /* This file is part of GNU Taler (C) 2022 Taler Systems SA diff --git a/packages/taler-harness/src/integrationtests/test-merchant-self-provision-activation-and-login.ts b/packages/taler-harness/src/integrationtests/test-merchant-self-provision-activation-and-login.ts @@ -0,0 +1,184 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU 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 General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Imports. + */ +import { + alternativeOrThrow, + Duration, + HttpStatusCode, + MerchantAuthMethod, + succeedOrThrow, + TalerMerchantInstanceHttpClient, + TalerMerchantManagementHttpClient +} from "@gnu-taler/taler-util"; +import { createSimpleTestkudosEnvironmentV3 } from "harness/environments.js"; +import { startTanHelper } from "harness/tan-helper.js"; +import { randomBytes } from "node:crypto"; +import { chmodSync, writeFileSync } from "node:fs"; +import { GlobalTestState } from "../harness/harness.js"; + +/** + * Do basic checks on instance management and authentication. + */ +export async function runMerchantSelfProvisionActivationAndLoginTest( + t: GlobalTestState, +) { + // Set up test environment + + // FIXME: maybe merchant can use commands? + const RND = randomBytes(10).toString("hex"); + const socketFile = `${t.testDir}/tan-helper-${RND}.socket`; + const helperScript = `${t.testDir}/harness-helper-${RND}.sh`; + writeFileSync( + helperScript, + `#!/bin/bash +taler-harness run-helper --socket ${socketFile} -- $@ +`, + ); + chmodSync(helperScript, "777"); + + const { + walletClient, + bankClient, + exchange, + merchant, + bank, + merchantAdminAccessToken, + } = await createSimpleTestkudosEnvironmentV3(t, undefined, { + additionalMerchantConfig(m) { + m.modifyConfig(async (cfg) => { + cfg.setString("merchant", "ENABLE_SELF_PROVISIONING", "yes"); + cfg.setString("merchant", "HELPER_SMS", helperScript); + cfg.setString("merchant", "HELPER_EMAIL", helperScript); + cfg.setString("merchant", "MANDATORY_TAN_CHANNELS", "email sms"); + }); + }, + }); + const helper = await startTanHelper({ socketFile }); + + const merchantClient = new TalerMerchantManagementHttpClient( + merchant.makeInstanceBaseUrl(), + ); + + { + const r = succeedOrThrow( + await merchantClient.listInstances(merchantAdminAccessToken), + ); + t.assertDeepEqual(r.instances.length, 2); + } + + const instanceInfo = { + id: "self-instance", + name: "My instance", + auth: { + method: MerchantAuthMethod.TOKEN, + password: "123", + }, + default_pay_delay: { d_us: "forever" as const }, + default_wire_transfer_delay: { d_us: "forever" as const }, + jurisdiction: {}, + address: {}, + email: "some@taler.net", + phone_number: "+1111", + use_stefan: false, + }; + const loginTokenDuration = Duration.fromSpec({ months: 6 }); + const signupStart = alternativeOrThrow( + await merchantClient.createInstanceSelfProvision(instanceInfo, { + tokenValidity: loginTokenDuration, + }), + HttpStatusCode.Accepted, + ); + + // creation requires 2fa + t.assertDeepEqual(signupStart.challenges.length, 2); + t.assertDeepEqual(signupStart.combi_and, true); + + const firstChallenge = signupStart.challenges[0]; + const secondChallenge = signupStart.challenges[1]; + + //FIXME: check the order + // always first emails since is cheaper + // t.assertDeepEqual(firstChallenge.challenge_type, TanChannel.EMAIL); + // t.assertDeepEqual(secondChallenge.challenge_type, TanChannel.SMS); + + { + // new instance is pending, then is not listed + const r = succeedOrThrow( + await merchantClient.listInstances(merchantAdminAccessToken), + ); + t.assertDeepEqual(r.instances.length, 2); + } + + { + succeedOrThrow( + await merchantClient.sendChallenge(firstChallenge.challenge_id), + ); + + const message = helper.getLastCodeForAddress(instanceInfo.phone_number); + const [tanCode] = message.split("\n"); + succeedOrThrow( + await merchantClient.confirmChallenge(firstChallenge.challenge_id, { + tan: tanCode, + }), + ); + } + + { + succeedOrThrow( + await merchantClient.sendChallenge(secondChallenge.challenge_id), + ); + + const message = helper.getLastCodeForAddress(instanceInfo.email); + const [tanCode] = message.split("\n"); + succeedOrThrow( + await merchantClient.confirmChallenge(secondChallenge.challenge_id, { + tan: tanCode, + }), + ); + } + + const completeSignup = succeedOrThrow( + await merchantClient.createInstanceSelfProvision(instanceInfo, { + tokenValidity: loginTokenDuration, + challengeIds: [firstChallenge.challenge_id, secondChallenge.challenge_id], + }), + ); + + t.assertTrue(completeSignup !== null); + + const instanceApi = new TalerMerchantInstanceHttpClient( + merchantClient.getSubInstanceAPI(instanceInfo.id), + merchantClient.httpLib, + ); + + const { access_token: token } = completeSignup!; + const det = succeedOrThrow( + await instanceApi.getCurrentInstanceDetails(token), + ); + + // check that the instance has the new email + t.assertDeepEqual(det.email, instanceInfo.email); + t.assertDeepEqual(det.email_validated, true); + t.assertDeepEqual(det.phone_number, instanceInfo.phone_number); + t.assertDeepEqual(det.phone_validated, true); + + helper.stop(); +} + +runMerchantSelfProvisionActivationAndLoginTest.suites = ["merchant", "self-provision"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -109,6 +109,7 @@ import { runMerchantInstancesTest } from "./test-merchant-instances.js"; import { runMerchantLongpollingTest } from "./test-merchant-longpolling.js"; import { runMerchantRefundApiTest } from "./test-merchant-refund-api.js"; import { runMerchantSelfProvisionActivationTest } from "./test-merchant-self-provision-activation.js"; +import { runMerchantSelfProvisionActivationAndLoginTest } from "./test-merchant-self-provision-activation-and-login.js"; import { runMerchantSelfProvisionForgotPasswordTest } from "./test-merchant-self-provision-forgot-password.js"; import { runMerchantSelfProvisionInactiveAccountPermissionsTest } from "./test-merchant-self-provision-inactive-account-permissions.js"; import { runMerchantSpecPublicOrdersTest } from "./test-merchant-spec-public-orders.js"; @@ -318,6 +319,7 @@ const allTests: TestMainFunction[] = [ runExchangeMasterPubChangeTest, runMerchantCategoriesTest, runMerchantSelfProvisionActivationTest, + runMerchantSelfProvisionActivationAndLoginTest, runMerchantSelfProvisionForgotPasswordTest, runMerchantSelfProvisionInactiveAccountPermissionsTest, runWithdrawalExternalTest, diff --git a/packages/taler-util/src/codec.ts b/packages/taler-util/src/codec.ts @@ -630,7 +630,7 @@ export function codecForEither<T extends Array<Codec<unknown>>>( logger.trace(`offending value: ${j2s(x)}`); } throw new DecodingError( - `No alternative matched at at ${renderContext(c)}`, + `No alternative matched at ${renderContext(c)}`, ); }, }; diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts @@ -19,6 +19,7 @@ import { CancellationToken, ChallengeRequestResponse, ChallengeSolveRequest, + Duration, FailCasesByMethod, HttpStatusCode, LibtoolVersion, @@ -2732,6 +2733,7 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp async createInstanceSelfProvision( body: TalerMerchantApi.InstanceConfigurationMessage, params: { + tokenValidity?: Duration; challengeIds?: string[]; } = {}, ) { @@ -2741,6 +2743,9 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp if (params.challengeIds && params.challengeIds.length > 0) { headers["Taler-Challenge-Ids"] = params.challengeIds.join(", "); } + if (params.tokenValidity) { + url.searchParams.append("token_validity_ms", String(params.tokenValidity.d_ms)) + } const resp = await this.httpLib.fetch(url.href, { method: "POST", body, @@ -2748,11 +2753,20 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp }); switch (resp.status) { + case HttpStatusCode.Ok: { + this.cacheManagementEvictor.notifySuccess( + TalerMerchantManagementCacheEviction.CREATE_INSTANCE, + ); + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.CREATE_ACCESSTOKEN, + ); + return opSuccessFromHttp(resp, codecForLoginTokenSuccessResponse()); + } case HttpStatusCode.NoContent: { this.cacheManagementEvictor.notifySuccess( TalerMerchantManagementCacheEviction.CREATE_INSTANCE, ); - return opEmptySuccess(); + return opFixedSuccess(undefined); } case HttpStatusCode.Accepted: return opKnownAlternativeHttpFailure( diff --git a/packages/taler-util/src/types-taler-merchant.ts b/packages/taler-util/src/types-taler-merchant.ts @@ -1662,12 +1662,14 @@ export interface InstanceAuthConfigurationMessage { export enum LoginTokenScope { ReadOnly = "readonly", All = "all", + Spa = "spa", OrderSimple = "order-simple", OrderPos = "order-pos", OrderManagement = "order-mgmt", OrderFull = "order-full", ReadOnly_Refreshable = "readonly:refreshable", All_Refreshable = "all:refreshable", + Spa_Refreshable = "spa:refreshable", OrderSimple_Refreshable = "order-simple:refreshable", OrderPos_Refreshable = "order-pos:refreshable", OrderManagement_Refreshable = "order-mgmt:refreshable", @@ -3816,12 +3818,14 @@ export const codecForMerchantAccountKycRedirect = export const codecForTokenScope = codecForEither( codecForConstString(LoginTokenScope.All), + codecForConstString(LoginTokenScope.Spa), codecForConstString(LoginTokenScope.OrderFull), codecForConstString(LoginTokenScope.OrderManagement), codecForConstString(LoginTokenScope.OrderPos), codecForConstString(LoginTokenScope.OrderSimple), codecForConstString(LoginTokenScope.ReadOnly), codecForConstString(LoginTokenScope.All_Refreshable), + codecForConstString(LoginTokenScope.Spa_Refreshable), codecForConstString(LoginTokenScope.OrderFull_Refreshable), codecForConstString(LoginTokenScope.OrderManagement_Refreshable), codecForConstString(LoginTokenScope.OrderPos_Refreshable),