taler-typescript-core

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

commit d5900403fd5f42fcf4fbb46a1076fa595ed3aa10
parent 6a3943243068c8d864d3587145d4a146ebb98f77
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Sat,  7 Mar 2026 17:23:04 -0300

fix #11173

Diffstat:
Mpackages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx | 15+++++++++------
Mpackages/merchant-backoffice-ui/src/paths/admin/list/View.tsx | 23++++++++++++++---------
Mpackages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/CreatePage.tsx | 14+++++++++-----
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx | 50+++++++++++++++++++++++++++-----------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx | 100++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/password/index.tsx | 23+++++++++++++++--------
Mpackages/merchant-backoffice-ui/src/paths/instance/update/DeletePage.tsx | 22+++++++++++++---------
Mpackages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx | 11++++++++---
Mpackages/merchant-backoffice-ui/src/paths/login/index.tsx | 9++++++---
Mpackages/merchant-backoffice-ui/src/paths/newAccount/index.tsx | 12++++++++----
Mpackages/merchant-backoffice-ui/src/paths/resetAccount/index.tsx | 5+++--
11 files changed, 165 insertions(+), 119 deletions(-)

diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx @@ -42,9 +42,10 @@ import { FormErrors, FormProvider, } from "../../../components/form/FormProvider.js"; -import { Input } from "../../../components/form/Input.js"; +import { InputPassword } from "../../../components/form/InputPassword.js"; import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js"; import { SolveMFAChallenges } from "../../../components/SolveMFA.js"; +import { Tooltip } from "../../../components/Tooltip.js"; import { useSessionContext } from "../../../context/session.js"; import { EMAIL_REGEX, @@ -52,10 +53,8 @@ import { PHONE_JUST_NUMBERS_REGEX, } from "../../../utils/constants.js"; import { undefinedIfEmpty } from "../../../utils/table.js"; -import { FOREVER_REFRESHABLE_TOKEN } from "../../login/index.js"; -import { InputPassword } from "../../../components/form/InputPassword.js"; -import { Tooltip } from "../../../components/Tooltip.js"; import { maybeTryFirstMFA } from "../../instance/accounts/create/CreatePage.js"; +import { FOREVER_REFRESHABLE_TOKEN } from "../../login/index.js"; const TALER_SCREEN_ID = 25; @@ -187,7 +186,12 @@ export function CreatePage({ onConfirm, onBack, forceId }: Props): VNode { data.auth.password, FOREVER_REFRESHABLE_TOKEN(i18n.str`Instance created`), ); - if (tokenResp.type === "fail") return tokenResp; + if (tokenResp.type === "fail") { + if (tokenResp.case === HttpStatusCode.Accepted) { + await maybeTryFirstMFA(lib.instance, mfa, tokenResp.body); + } + return tokenResp; + } return opFixedSuccess(tokenResp.body); } return opEmptySuccess(); @@ -203,7 +207,6 @@ export function CreatePage({ onConfirm, onBack, forceId }: Props): VNode { create.onFail = (fail) => { switch (fail.case) { case HttpStatusCode.Accepted: - maybeTryFirstMFA(lib.instance, mfa, fail.body); return undefined; case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized.`; diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx @@ -77,13 +77,24 @@ export function View({ const mfa = useChallengeHandler(); const deleteAction = safeFunctionHandler( i18n.str`delete instance`, - ( + async ( token: AccessToken, instance: TalerMerchantApi.Instance, purge: boolean, challengeIds: string[], - ) => - lib.instance.deleteInstance(token, instance.id, { challengeIds, purge }), + ) => { + const resp = await lib.instance.deleteInstance(token, instance.id, { challengeIds, purge }); + if (resp.type === "fail" && resp.case === HttpStatusCode.Accepted) { + await maybeTryFirstMFA( + lib.instance, + mfa, + resp.body, + deleteAction.lambda((ids) => [token, instance, purge, ids]), + ) + } + return resp + } + , ); const [deleting, setDeleting] = useState<TalerMerchantApi.Instance>(); @@ -97,12 +108,6 @@ export function View({ deleteAction.onFail = (fail, t, i, p) => { switch (fail.case) { case HttpStatusCode.Accepted: - maybeTryFirstMFA( - lib.instance, - mfa, - fail.body, - deleteAction.lambda((ids) => [t, i, p, ids]), - ); return undefined; case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized.`; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/CreatePage.tsx @@ -125,20 +125,24 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { }; const create = safeFunctionHandler( i18n.str`create access token`, - ( + async ( pwd: string, request: TalerMerchantApi.LoginTokenRequest, challengeIds: string[], - ) => - lib.instance.createAccessToken(session.instance, pwd, request, { + ) => { + const resp = await lib.instance.createAccessToken(session.instance, pwd, request, { challengeIds, - }), + }) + if (resp.type === "fail" && resp.case === HttpStatusCode.Accepted) { + await maybeTryFirstMFA(lib.instance, mfa, resp.body) + } + return resp + }, !!errors || !state.password ? undefined : [state.password, data, []], ); create.onFail = (fail) => { switch (fail.case) { case HttpStatusCode.Accepted: - maybeTryFirstMFA(lib.instance, mfa, fail.body); return undefined; case HttpStatusCode.Unauthorized: return i18n.str`Check the password.`; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx @@ -108,17 +108,17 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { !state.credit_facade_credentials || !state.credit_facade_url ? undefined : { - username: - state.credit_facade_credentials.type === "basic" && + username: + state.credit_facade_credentials.type === "basic" && !state.credit_facade_credentials.username - ? i18n.str`Required` - : undefined, - password: - state.credit_facade_credentials.type === "basic" && + ? i18n.str`Required` + : undefined, + password: + state.credit_facade_credentials.type === "basic" && !state.credit_facade_credentials.password - ? i18n.str`Required` - : undefined, - }, + ? i18n.str`Required` + : undefined, + }, ) as any, credit_facade_url: !state.credit_facade_url ? undefined @@ -145,38 +145,42 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { ? undefined : state.credit_facade_credentials?.type === "basic" ? { - type: "basic", - password: state.credit_facade_credentials.password, - username: state.credit_facade_credentials.username, - } + type: "basic", + password: state.credit_facade_credentials.password, + username: state.credit_facade_credentials.username, + } : { - type: "none", - }; + type: "none", + }; const { state: session, lib } = useSessionContext(); const request: TalerMerchantApi.AccountAddDetails | undefined = !state.payto_uri ? undefined : { - payto_uri: state.payto_uri, - credit_facade_credentials, - credit_facade_url, - extra_wire_subject_metadata: state.extra_wire_subject_metadata, - }; + payto_uri: state.payto_uri, + credit_facade_credentials, + credit_facade_url, + extra_wire_subject_metadata: state.extra_wire_subject_metadata, + }; const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const mfa = useChallengeHandler(); const add = safeFunctionHandler( i18n.str`add bank account`, - (token: AccessToken, request: Entity, challengeIds: string[]) => - lib.instance.addBankAccount(token, request, { challengeIds }), + async (token: AccessToken, request: Entity, challengeIds: string[]) => { + const resp = await lib.instance.addBankAccount(token, request, { challengeIds }) + if (resp.type === "fail" && resp.case === HttpStatusCode.Accepted) { + await maybeTryFirstMFA(lib.instance, mfa, resp.body); + } + return resp + }, !session.token || !request ? undefined : [session.token, request, []], ); add.onSuccess = onCreated; add.onFail = (fail) => { switch (fail.case) { case HttpStatusCode.Accepted: - maybeTryFirstMFA(lib.instance, mfa, fail.body); return undefined; case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized.`; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx @@ -128,33 +128,33 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { !state.credit_facade_credentials || !state.credit_facade_url ? undefined : (undefinedIfEmpty({ - type: - replacingAccountId && + type: + replacingAccountId && // @ts-expect-error unedit is not in facade creds state.credit_facade_credentials?.type === "unedit" + ? i18n.str`Required` + : undefined, + username: + state.credit_facade_credentials?.type !== "basic" + ? undefined + : !state.credit_facade_credentials.username ? i18n.str`Required` : undefined, - username: - state.credit_facade_credentials?.type !== "basic" - ? undefined - : !state.credit_facade_credentials.username - ? i18n.str`Required` - : undefined, - token: - state.credit_facade_credentials?.type !== "bearer" - ? undefined - : !state.credit_facade_credentials.token - ? i18n.str`Required` - : undefined, + token: + state.credit_facade_credentials?.type !== "bearer" + ? undefined + : !state.credit_facade_credentials.token + ? i18n.str`Required` + : undefined, - password: - state.credit_facade_credentials?.type !== "basic" - ? undefined - : !state.credit_facade_credentials.password - ? i18n.str`Required` - : undefined, - }) as any), + password: + state.credit_facade_credentials?.type !== "basic" + ? undefined + : !state.credit_facade_credentials.password + ? i18n.str`Required` + : undefined, + }) as any), }); const hasErrors = errors !== undefined; @@ -167,25 +167,25 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { | TalerMerchantApi.FacadeCredentials | undefined = credit_facade_url == undefined || - state.credit_facade_credentials === undefined + state.credit_facade_credentials === undefined ? undefined : // @ts-expect-error unedit is not in facade creds - state.credit_facade_credentials.type === "unedit" + state.credit_facade_credentials.type === "unedit" ? undefined : state.credit_facade_credentials.type === "basic" ? { - type: "basic", - password: state.credit_facade_credentials.password, - username: state.credit_facade_credentials.username, - } + type: "basic", + password: state.credit_facade_credentials.password, + username: state.credit_facade_credentials.username, + } : state.credit_facade_credentials.type === "bearer" ? { - type: "bearer", - token: state.credit_facade_credentials.token, - } + type: "bearer", + token: state.credit_facade_credentials.token, + } : { - type: "none", - }; + type: "none", + }; const { state: session, lib } = useSessionContext(); const [notification, safeFunctionHandler] = useLocalNotificationBetter(); @@ -204,9 +204,16 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { const created = await lib.instance.addBankAccount(token, details, { challengeIds, }); - if (created.type === "fail") return created; + if (created.type === "fail") { + if (created.case === HttpStatusCode.Accepted) { + await maybeTryFirstMFA(lib.instance, mfa, created.body); + } + return created; + } const deleted = await lib.instance.deleteBankAccount(token, id); - if (deleted.type === "fail") return deleted; + if (deleted.type === "fail") { + return deleted; + } } else { const resp = await lib.instance.updateBankAccount(token, id, data); if (resp.type === "fail") return resp; @@ -221,24 +228,23 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { !session.token ? undefined : [ - session.token, - replacingAccountId ? state.payto_uri! : undefined, - account.h_wire, - { - credit_facade_credentials, - credit_facade_url, - extra_wire_subject_metadata: !state.extra_wire_subject_metadata - ? undefined - : state.extra_wire_subject_metadata, - }, - [], - ], + session.token, + replacingAccountId ? state.payto_uri! : undefined, + account.h_wire, + { + credit_facade_credentials, + credit_facade_url, + extra_wire_subject_metadata: !state.extra_wire_subject_metadata + ? undefined + : state.extra_wire_subject_metadata, + }, + [], + ], ); update.onSuccess = onUpdated; update.onFail = (fail) => { switch (fail.case) { case HttpStatusCode.Accepted: - maybeTryFirstMFA(lib.instance, mfa, fail.body); return undefined; case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/password/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/password/index.tsx @@ -82,22 +82,23 @@ export default function PasswordPage({ onCancel, onChange }: Props): VNode { next: string, challengeIds: string[], ) => { - const resp = await lib.instance.createAccessToken( + + const created = await lib.instance.createAccessToken( instanceId, current, TEMP_TEST_TOKEN(i18n.str`Testing password change`), ); - if (resp.type === "fail") { - switch (resp.case) { + if (created.type === "fail") { + switch (created.case) { case HttpStatusCode.Unauthorized: return opKnownFailure("bad-current-pwd"); case HttpStatusCode.NotFound: - return resp; + return created; case HttpStatusCode.Accepted: break; //2fa required but the pwd is ok, continue } } - return lib.instance.updateCurrentInstanceAuthentication( + const updated = await lib.instance.updateCurrentInstanceAuthentication( token, { method: MerchantAuthMethod.TOKEN, @@ -105,6 +106,10 @@ export default function PasswordPage({ onCancel, onChange }: Props): VNode { }, { challengeIds }, ); + if (updated.type === "fail" && updated.case === HttpStatusCode.Accepted) { + await maybeTryFirstMFA(lib.instance, mfa, updated.body); + } + return updated }, !session.token ? undefined : [session.token, "", "", []], ); @@ -115,7 +120,6 @@ export default function PasswordPage({ onCancel, onChange }: Props): VNode { changePassword.onFail = (fail) => { switch (fail.case) { case HttpStatusCode.Accepted: - maybeTryFirstMFA(lib.instance, mfa, fail.body); return undefined; case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized.`; @@ -219,7 +223,7 @@ export function AdminPassword({ // break; //2fa required but the pwd is ok, continue // } // } - return await lib.instance.updateInstanceAuthentication( + const resp = await lib.instance.updateInstanceAuthentication( token, id, { @@ -228,6 +232,10 @@ export function AdminPassword({ }, { challengeIds }, ); + if (resp.type === "fail" && resp.case === HttpStatusCode.Accepted) { + await maybeTryFirstMFA(lib.instance, mfa, resp.body); + } + return resp }, !session.token ? undefined : [session.token, instanceId, "", []], ); @@ -238,7 +246,6 @@ export function AdminPassword({ changePassword.onFail = (fail) => { switch (fail.case) { case HttpStatusCode.Accepted: - maybeTryFirstMFA(lib.instance, mfa, fail.body); return undefined; case HttpStatusCode.Unauthorized: return i18n.str`No enough rights to change the password.`; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/DeletePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/DeletePage.tsx @@ -79,11 +79,21 @@ export function DeletePage({ instanceId, onBack, onDeleted }: Props): VNode { const remove = safeFunctionHandler( i18n.str`delete current instance`, - (token: AccessToken, purge: boolean, challengeIds: string[]) => - lib.instance.deleteCurrentInstance(token, { + async (token: AccessToken, purge: boolean, challengeIds: string[]) => { + const resp = await lib.instance.deleteCurrentInstance(token, { purge, challengeIds, - }), + }) + if (resp.type === "fail" && resp.case === HttpStatusCode.Accepted) { + await maybeTryFirstMFA( + lib.instance, + mfa, + resp.body, + remove.lambda((ids: string[]) => [token, purge, ids]), + ); + } + return resp + }, !session.token ? undefined : [session.token, form.purge!!, []], ); @@ -91,12 +101,6 @@ export function DeletePage({ instanceId, onBack, onDeleted }: Props): VNode { remove.onFail = (fail, t, p) => { switch (fail.case) { case HttpStatusCode.Accepted: - maybeTryFirstMFA( - lib.instance, - mfa, - fail.body, - remove.lambda((ids: string[]) => [t, p, ids]), - ); return undefined; case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized.`; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx @@ -137,18 +137,23 @@ export function UpdatePage({ const mfa = useChallengeHandler(); const update = safeFunctionHandler( i18n.str`update instance settings`, - ( + async ( token: AccessToken, d: TalerMerchantApi.InstanceReconfigurationMessage, challengeIds: string[], - ) => doUpdate(token, d, { challengeIds }), + ) => { + const resp = await doUpdate(token, d, { challengeIds }) + if (resp.type === "fail" && resp.case === HttpStatusCode.Accepted) { + await maybeTryFirstMFA(lib.instance, mfa, resp.body); + } + return resp + }, hasErrors || !state.token ? undefined : [state.token, result, []], ); update.onSuccess = onConfirm; update.onFail = (fail) => { switch (fail.case) { case HttpStatusCode.Accepted: - maybeTryFirstMFA(lib.instance, mfa, fail.body); return undefined; case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized.`; diff --git a/packages/merchant-backoffice-ui/src/paths/login/index.tsx b/packages/merchant-backoffice-ui/src/paths/login/index.tsx @@ -81,14 +81,18 @@ export function LoginPage({ showCreateAccount, focus }: Props): VNode { const login = safeFunctionHandler( i18n.str`login`, - (usr: string, pwd: string, challengeIds: string[]) => { + async (usr: string, pwd: string, challengeIds: string[]) => { const api = getInstanceForUsername(usr); - return api.createAccessToken( + const resp = await api.createAccessToken( usr, pwd, FOREVER_REFRESHABLE_TOKEN(i18n.str`Logged in`), { challengeIds }, ); + if (resp.type === "fail" && resp.case === HttpStatusCode.Accepted) { + await maybeTryFirstMFA(lib.instance, mfa, resp.body); + } + return resp }, !username || !password ? undefined : [username, password, []], ); @@ -98,7 +102,6 @@ export function LoginPage({ showCreateAccount, focus }: Props): VNode { login.onFail = (fail) => { switch (fail.case) { case HttpStatusCode.Accepted: - maybeTryFirstMFA(lib.instance, mfa, fail.body); return undefined; case HttpStatusCode.Unauthorized: return i18n.str`Wrong password.`; diff --git a/packages/merchant-backoffice-ui/src/paths/newAccount/index.tsx b/packages/merchant-backoffice-ui/src/paths/newAccount/index.tsx @@ -200,11 +200,16 @@ export function NewAccount({ onCancel, onCreated }: Props): VNode { const create = safeFunctionHandler( i18n.str`self provision instance`, - (req: InstanceConfigurationMessage, challengeIds: string[]) => - lib.instance.createInstanceSelfProvision(req, { + async (req: InstanceConfigurationMessage, challengeIds: string[]) => { + const resp = await lib.instance.createInstanceSelfProvision(req, { challengeIds, tokenValidity: Duration.fromSpec({ months: 6 }), - }), + }) + if (resp.type === "fail" && resp.case === HttpStatusCode.Accepted) { + await maybeTryFirstMFA(lib.instance, mfa, resp.body); + } + return resp + }, !!errors ? undefined : [request, []], ); create.onSuccess = (success, req) => { @@ -217,7 +222,6 @@ export function NewAccount({ onCancel, onCreated }: Props): VNode { create.onFail = (fail) => { switch (fail.case) { case HttpStatusCode.Accepted: - maybeTryFirstMFA(lib.instance, mfa, fail.body); return undefined; case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; diff --git a/packages/merchant-backoffice-ui/src/paths/resetAccount/index.tsx b/packages/merchant-backoffice-ui/src/paths/resetAccount/index.tsx @@ -97,7 +97,9 @@ export function ResetAccount({ { method: MerchantAuthMethod.TOKEN, password }, { challengeIds }, ); - + if (forgot.type === "fail" && forgot.case === HttpStatusCode.Accepted) { + await maybeTryFirstMFA(lib.instance, mfa, forgot.body); + } return forgot; }, hasErrors ? undefined : [value.password!, []], @@ -111,7 +113,6 @@ export function ResetAccount({ case TalerErrorCode.MERCHANT_GENERIC_MFA_MISSING: return i18n.str`The instance is not properly configured to allow MFA.`; case HttpStatusCode.Accepted: - maybeTryFirstMFA(lib.instance, mfa, fail.body); return undefined; // case HttpStatusCode.Unauthorized: // return i18n.str`Unauthorized.`;