commit b4a2c06b29efe308f2d3aa1d0e790f23d2508d52
parent 5d5ab36686029d4876fbc24171503d606273a767
Author: Sebastian <sebasjm@gmail.com>
Date: Thu, 16 Oct 2025 11:23:29 -0300
fix #10498
Diffstat:
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),