taler-typescript-core

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

commit 0e6ec2c2b2b016539c5a5539e98b525d0f44a24c
parent 95528ff732e0fda0ad643b7ec52b7e16d497ece3
Author: Sebastian <sebasjm@gmail.com>
Date:   Mon, 23 Jun 2025 14:56:34 -0300

fix #10110

Diffstat:
Mpackages/aml-backoffice-ui/src/components/MeasureList.tsx | 1-
Mpackages/aml-backoffice-ui/src/components/MeasuresTable.tsx | 28+++++++++++++++++-----------
Apackages/aml-backoffice-ui/src/components/ShowLegitimizationInfo.tsx | 175+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/aml-backoffice-ui/src/hooks/legitimizations.ts | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/aml-backoffice-ui/src/pages/AccountDetails.tsx | 52++++++++++++++++++++++++++++++++++++++++++----------
Mpackages/aml-backoffice-ui/src/pages/decision/Measures.tsx | 6+++---
Mpackages/aml-backoffice-ui/src/pages/decision/Summary.tsx | 4++--
Mpackages/aml-backoffice-ui/src/utils/computeAvailableMesaures.ts | 97++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/admin/create/index.tsx | 9++-------
Mpackages/merchant-backoffice-ui/src/paths/instance/token/index.tsx | 21++++++---------------
Mpackages/merchant-backoffice-ui/src/paths/login/index.tsx | 8++++----
Mpackages/taler-harness/src/integrationtests/test-tops-aml-legi.ts | 6++----
Mpackages/taler-util/src/http-client/exchange-client.ts | 72++++++++++++++++++++++++++++++++++++++++++------------------------------
Mpackages/taler-util/src/types-taler-exchange.ts | 35+++++++++++++++++++++++++++++++----
14 files changed, 469 insertions(+), 137 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/components/MeasureList.tsx b/packages/aml-backoffice-ui/src/components/MeasureList.tsx @@ -91,7 +91,6 @@ export function MeasureList({ routeToNew }: { routeToNew: RouteDefinition }) { const ms = computeAvailableMesaures( measures.body, - // , custom ); return ( diff --git a/packages/aml-backoffice-ui/src/components/MeasuresTable.tsx b/packages/aml-backoffice-ui/src/components/MeasuresTable.tsx @@ -50,7 +50,7 @@ export type InfoMeasure = { context?: object; custom: boolean; }; -export type Mesaures = { +export type UiMeasureInformation = { procedures: ProcedureMeasure[]; forms: FormMeasure[]; info: InfoMeasure[]; @@ -58,8 +58,10 @@ export type Mesaures = { export function CurrentMeasureTable({ measures, onSelect, + hideMeasureNames, }: { - measures: Mesaures; + measures: UiMeasureInformation; + hideMeasureNames?: boolean; onSelect?: (m: MeasureInfo) => void; }): VNode { const { i18n } = useTranslationContext(); @@ -92,12 +94,14 @@ export function CurrentMeasureTable({ ) : ( <Fragment /> )} - <th - scope="col" - class="p-2 text-left text-sm font-semibold text-gray-900 sm:pl-6" - > - <i18n.Translate>Name</i18n.Translate> - </th> + {hideMeasureNames ? undefined : ( + <th + scope="col" + class="p-2 text-left text-sm font-semibold text-gray-900 sm:pl-6" + > + <i18n.Translate>Name</i18n.Translate> + </th> + )} <th scope="col" class="p-2 text-left text-sm font-semibold text-gray-900" @@ -132,9 +136,11 @@ export function CurrentMeasureTable({ </button> </td> )} - <td class="whitespace-nowrap p-2 text-sm font-medium text-gray-900 "> - {m.name} - </td> + {hideMeasureNames ? undefined : ( + <td class="whitespace-nowrap p-2 text-sm font-medium text-gray-900 "> + {m.name} + </td> + )} <td class="whitespace-nowrap p-2 text-sm text-gray-500"> {m.check?.description ?? ""} </td> diff --git a/packages/aml-backoffice-ui/src/components/ShowLegitimizationInfo.tsx b/packages/aml-backoffice-ui/src/components/ShowLegitimizationInfo.tsx @@ -0,0 +1,175 @@ +/* + This file is part of GNU Taler + (C) 2022-2025 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/> + */ +import { + AbsoluteTime, + AvailableMeasureSummary, + KycRule, + LegitimizationMeasures, + TalerExchangeApi, +} from "@gnu-taler/taler-util"; +import { Time, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { RulesInfo } from "./RulesInfo.js"; +import { CurrentMeasureTable } from "./MeasuresTable.js"; +import { computeAvailableMesaures } from "../utils/computeAvailableMesaures.js"; +import { CheckCircleIcon } from "@heroicons/react/20/solid"; + +const TALER_SCREEN_ID = 120; + +// function ShowLegistimizationInfo({info}:{info: LegitimizationMeasureDetails}): VNode { +// return <CurrentMeasureTable measures={res[0]} hideMeasureNames />; +// } + +export function ShowLegistimizationInfo({ + since, + startOpen, + fixed, + legitimization, + completed, + serverMeasures, +}: { + since: AbsoluteTime; + startOpen?: boolean; + fixed?: boolean; + legitimization: LegitimizationMeasures; + completed: boolean; + serverMeasures: AvailableMeasureSummary | undefined; +}): VNode { + const { i18n } = useTranslationContext(); + const [opened, setOpened] = useState(startOpen ?? false); + + function Header() { + return ( + <div + data-fixed={!!fixed} + class="p-4 relative bg-gray-200 flex justify-between data-[fixed=false]:cursor-pointer" + onClick={() => { + if (!fixed) { + setOpened((o) => !o); + } + }} + > + <div class="flex min-w-0 gap-x-4"> + <div class="flex rounded-md shadow-sm border-0 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600"> + <div class="pointer-events-none bg-gray-300 inset-y-0 flex items-center px-3"> + <i18n.Translate>Since</i18n.Translate> + </div> + <div class="p-2 bg-gray-50 text-right rounded-md rounded-l-none data-[left=true]:text-left w-full py-1.5 pl-3 text-gray-900 placeholder:text-gray-50 sm:text-sm sm:leading-6"> + <Time format="dd/MM/yyyy HH:mm:ss" timestamp={since} /> + </div> + </div> + </div> + <div class="flex shrink-0 items-center gap-x-4"> + {completed ? ( + <CheckCircleIcon title={i18n.str`Already completed.`} /> + ) : ( + <ClockIcon title={i18n.str`Waiting to be completed.`} /> + )} + {fixed ? ( + <Fragment /> + ) : ( + <div class="rounded-full bg-gray-50 p-2"> + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="size-6 w-6 h-6" + > + {opened ? ( + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m19.5 8.25-7.5 7.5-7.5-7.5" + /> + ) : ( + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m4.5 15.75 7.5-7.5 7.5 7.5" + /> + )} + </svg> + </div> + )} + </div> + </div> + ); + } + + if (!opened) { + return ( + <div class="overflow-hidden border border-gray-800 rounded-xl"> + <Header /> + </div> + ); + } + + const info = computeAvailableMesaures(serverMeasures, { + measures: legitimization.measures, + }); + + return ( + <div class="overflow-hidden border border-gray-800 rounded-xl"> + <Header /> + <div class="p-4 grid gap-y-4"> + <CurrentMeasureTable measures={info} hideMeasureNames /> + </div> + </div> + ); +} + +function CircleCheckIcon({ title }: { title: string }): VNode { + return ( + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="size-6" + title={title} + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" + /> + </svg> + ); +} + +function ClockIcon({ title }: { title: string }): VNode { + return ( + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="size-6" + title={title} + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" + /> + </svg> + ); +} diff --git a/packages/aml-backoffice-ui/src/hooks/legitimizations.ts b/packages/aml-backoffice-ui/src/hooks/legitimizations.ts @@ -0,0 +1,92 @@ +/* + This file is part of GNU Taler + (C) 2022-2025 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/> + */ +import { useState } from "preact/hooks"; + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import { + OfficerAccount, + opFixedSuccess, + TalerExchangeResultByMethod2, + TalerHttpError, +} from "@gnu-taler/taler-util"; +import { + buildPaginatedResult, + useExchangeApiContext, +} from "@gnu-taler/web-util/browser"; +import _useSWR, { mutate, SWRHook } from "swr"; +import { useOfficer } from "./officer.js"; +const useSWR = _useSWR as unknown as SWRHook; + +export const PAGINATED_LIST_SIZE = 10; +// when doing paginated request, ask for one more +// and use it to know if there are more to request +export const PAGINATED_LIST_REQUEST = PAGINATED_LIST_SIZE + 1; + +export function revalidateCurrentLegitimizations() { + return mutate( + (key) => + Array.isArray(key) && key[key.length - 1] === "getAmlLegitimizations", + undefined, + { revalidate: true }, + ); +} + +/** + * @param account + * @param args + * @returns + */ +export function useCurrentLegitimizations(accoutnStr: string) { + const officer = useOfficer(); + const session = officer.state === "ready" ? officer.account : undefined; + const { + lib: { exchange: api }, + } = useExchangeApiContext(); + + const [offset, setOffset] = useState<string>(); + + async function fetcher([officer, account, offset]: [ + OfficerAccount, + string | undefined, + string | undefined, + boolean | undefined, + ]) { + return await api.getAmlLegitimizations(officer, { + order: "dec", + offset, + active: true, + account, + limit: PAGINATED_LIST_REQUEST, + }); + } + + const { data, error } = useSWR< + TalerExchangeResultByMethod2<"getAmlLegitimizations">, + TalerHttpError + >(!session ? undefined : [session, accoutnStr, offset, "getAmlLegitimizations"], fetcher); + + if (error) return error; + if (data === undefined) return undefined; + // if (data.type !== "ok") return data; + + return buildPaginatedResult( + data.body.measures, + offset, + setOffset, + (d) => String(d.rowid), + PAGINATED_LIST_REQUEST, + ); +} diff --git a/packages/aml-backoffice-ui/src/pages/AccountDetails.tsx b/packages/aml-backoffice-ui/src/pages/AccountDetails.tsx @@ -17,6 +17,7 @@ import { AbsoluteTime, assertUnreachable, HttpStatusCode, + LegitimizationMeasureDetails, TalerError, TalerExchangeApi, TalerFormAttributes, @@ -39,6 +40,10 @@ import { ShowDefaultRules } from "../components/ShowDefaultRules.js"; import { Fragment } from "preact/jsx-runtime"; import { useServerMeasures } from "../hooks/server-info.js"; import { BANK_RULES, WALLET_RULES } from "./decision/Rules.js"; +import { useCurrentLegitimizations } from "../hooks/legitimizations.js"; +import { computeAvailableMesaures } from "../utils/computeAvailableMesaures.js"; +import { CurrentMeasureTable } from "../components/MeasuresTable.js"; +import { ShowLegistimizationInfo } from "../components/ShowLegitimizationInfo.js"; const TALER_SCREEN_ID = 116; @@ -56,11 +61,11 @@ export function AccountDetails({ const { i18n } = useTranslationContext(); const details = useAccountInformation(account); const history = useAccountDecisions(account); + const legistimizations = useCurrentLegitimizations(account); const measures = useServerMeasures(); - - if (!details || !history) { + if (!details || !history || !legistimizations) { return <Loading />; } if (details instanceof TalerError) { @@ -89,14 +94,16 @@ export function AccountDetails({ assertUnreachable(history); } } + if (legistimizations instanceof TalerError) { + return <ErrorLoadingWithDebug error={legistimizations} />; + } + const { details: collectionEvents } = details.body; const activeDecision = history.body.find((d) => d.is_active); const restDecisions = !activeDecision ? history.body : history.body.filter((d) => d.rowid !== activeDecision.rowid); - // const events = getEventsFromAmlHistory(accountDetails, i18n); - function ShortcutActionButtons(): VNode { return ( <div> @@ -128,17 +135,23 @@ export function AccountDetails({ ); } - const defaultRules = !measures || measures instanceof TalerError || measures.type === "fail" ? [] : measures.body.default_rules; - const filteredRulesByType = !activeDecision ? defaultRules : defaultRules.filter((r) => { - return activeDecision.is_wallet - ? WALLET_RULES.includes(r.operation_type) - : BANK_RULES.includes(r.operation_type); - }) + const serverMeasures = + !measures || measures instanceof TalerError || measures.type === "fail" + ? undefined + : measures.body; + + const filteredRulesByType = !activeDecision + ? defaultRules + : defaultRules.filter((r) => { + return activeDecision.is_wallet + ? WALLET_RULES.includes(r.operation_type) + : BANK_RULES.includes(r.operation_type); + }); return ( <div class="min-w-60"> @@ -235,6 +248,25 @@ export function AccountDetails({ <Attention title={i18n.str`No aml history found`} type="warning" /> </div> ) : undefined} + {legistimizations.body.length > 0 ? ( + <div class="my-4 grid gap-y-4"> + <h1 class="text-base font-semibold leading-6 text-black"> + <i18n.Translate>Current active measures</i18n.Translate> + </h1> + {legistimizations.body.map((d) => { + return ( + <ShowLegistimizationInfo + since={AbsoluteTime.fromProtocolTimestamp(d.start_time)} + completed={d.is_finished} + legitimization={d.measures} + serverMeasures={serverMeasures} + /> + ); + })} + </div> + ) : ( + <Fragment /> + )} </div> ); } diff --git a/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx b/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx @@ -33,7 +33,7 @@ import { useState } from "preact/hooks"; import { CurrentMeasureTable, MeasureInfo, - Mesaures, + UiMeasureInformation, } from "../../components/MeasuresTable.js"; import { MeasureDefinition, NewMeasure } from "../../components/NewMeasure.js"; import { useCurrentDecisionRequest } from "../../hooks/decision-request.js"; @@ -442,8 +442,8 @@ function computeAvailableMesauresCustom( customMeasures: Record<string, MeasureInformation> | undefined, serverMeasures: TalerExchangeApi.AvailableMeasureSummary | undefined, skpiFilter?: (m: MeasureInfo) => boolean, -): Mesaures { - const init: Mesaures = { forms: [], procedures: [], info: [] }; +): UiMeasureInformation { + const init: UiMeasureInformation = { forms: [], procedures: [], info: [] }; if (!customMeasures || !serverMeasures) { return init; diff --git a/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx b/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx @@ -37,7 +37,7 @@ import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { CurrentMeasureTable, - Mesaures, + UiMeasureInformation, } from "../../components/MeasuresTable.js"; import { ShowDecisionLimitInfo } from "../../components/ShowDecisionLimitInfo.js"; import { useCurrentDecisionRequest } from "../../hooks/decision-request.js"; @@ -83,7 +83,7 @@ export function Summary({ ); const d = decision.new_measures === undefined ? [] : decision.new_measures; - const activeMeasureInfo: Mesaures = { + const activeMeasureInfo: UiMeasureInformation = { forms: allMeasures.forms.filter((m) => d.indexOf(m.name) !== -1), procedures: allMeasures.procedures.filter((m) => d.indexOf(m.name) !== -1), info: allMeasures.info.filter((m) => d.indexOf(m.name) !== -1), diff --git a/packages/aml-backoffice-ui/src/utils/computeAvailableMesaures.ts b/packages/aml-backoffice-ui/src/utils/computeAvailableMesaures.ts @@ -14,67 +14,72 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import type { TalerExchangeApi } from "@gnu-taler/taler-util"; -import { MeasureInfo, Mesaures } from "../components/MeasuresTable.js"; +import { MeasureInfo, UiMeasureInformation } from "../components/MeasuresTable.js"; export function computeAvailableMesaures( serverMeasures: TalerExchangeApi.AvailableMeasureSummary | undefined, - // customMeasures?: Readonly<CustomMeasures>, - skpiFilter?: (m: MeasureInfo) => boolean, -): Mesaures { - const init: Mesaures = { forms: [], procedures: [], info: [] }; + opts: { + measures?: TalerExchangeApi.MeasureInformation[]; + skpiFilter?: (m: MeasureInfo) => boolean; + } = {}, +): UiMeasureInformation { + const init: UiMeasureInformation = { forms: [], procedures: [], info: [] }; if (!serverMeasures) { return init; } - const server = Object.entries(serverMeasures.roots).reduce( - (prev, [key, value]) => { - if (value.check_name !== "SKIP") { - if (!value.prog_name) { - const r: MeasureInfo = { - type: "info", - name: key, - context: value.context, - checkName: value.check_name, - check: serverMeasures.checks[value.check_name], - custom: true, - }; - if (skpiFilter && skpiFilter(r)) return prev; // skip - prev.info.push(r); - } else { - const r: MeasureInfo = { - type: "form", - name: key, - context: value.context, - programName: value.prog_name, - program: serverMeasures.programs[value.prog_name], - checkName: value.check_name, - check: serverMeasures.checks[value.check_name], - custom: false, - }; - if (skpiFilter && skpiFilter(r)) return prev; // skip - prev.forms.push(r); - } + + const defaultServerMeasures = Object.entries(serverMeasures.roots); + const measures: typeof defaultServerMeasures = !opts.measures + ? defaultServerMeasures + : opts.measures.map((m) => ["", m]); // we don't have the names in this case + + const server = measures.reduce((prev, [key, value]) => { + if (value.check_name !== "SKIP") { + if (!value.prog_name) { + const r: MeasureInfo = { + type: "info", + name: key, + context: value.context, + checkName: value.check_name, + check: serverMeasures.checks[value.check_name], + custom: true, + }; + if (opts.skpiFilter && opts.skpiFilter(r)) return prev; // skip + prev.info.push(r); } else { - if (!value.prog_name) { - console.error( - `ERROR: program name can't be empty for measure "${key}"`, - ); - return prev; - } const r: MeasureInfo = { - type: "procedure", + type: "form", name: key, context: value.context, programName: value.prog_name, program: serverMeasures.programs[value.prog_name], + checkName: value.check_name, + check: serverMeasures.checks[value.check_name], custom: false, }; - if (skpiFilter && skpiFilter(r)) return prev; // skip - prev.procedures.push(r); + if (opts.skpiFilter && opts.skpiFilter(r)) return prev; // skip + prev.forms.push(r); + } + } else { + if (!value.prog_name) { + console.error( + `ERROR: program name can't be empty for measure "${key}"`, + ); + return prev; } - return prev; - }, - init, - ); + const r: MeasureInfo = { + type: "procedure", + name: key, + context: value.context, + programName: value.prog_name, + program: serverMeasures.programs[value.prog_name], + custom: false, + }; + if (opts.skpiFilter && opts.skpiFilter(r)) return prev; // skip + prev.procedures.push(r); + } + return prev; + }, init); return server; } diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx @@ -25,6 +25,7 @@ import { NotificationCard } from "../../../components/menu/index.js"; import { useSessionContext } from "../../../context/session.js"; import { Notification } from "../../../utils/types.js"; import { CreatePage } from "./CreatePage.js"; +import { FOREVER_REFRESHABLE_TOKEN } from "../../login/index.js"; interface Props { onBack?: () => void; @@ -61,13 +62,7 @@ export default function Create({ onBack, onConfirm, forceId }: Props): VNode { //if auth has been updated, request a new access token const result = await lib.instance.createAuthTokenFromToken( d.auth.token, - { - scope: "write", - duration: { - d_us: "forever", - }, - refreshable: true, - }, + FOREVER_REFRESHABLE_TOKEN, ); if (result.type === "ok") { const { token } = result.body; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx @@ -34,7 +34,7 @@ import { useManagedInstanceDetails, } from "../../../hooks/instance.js"; import { Notification } from "../../../utils/types.js"; -import { LoginPage } from "../../login/index.js"; +import { FOREVER_REFRESHABLE_TOKEN, LoginPage } from "../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../notfound/index.js"; import { DetailPage } from "./DetailPage.js"; @@ -63,13 +63,7 @@ export default function Token(props: Props): VNode { throw Error(resp.detail?.hint ?? "The request failed"); } } - const resp = await lib.instance.createAuthTokenFromToken(newToken, { - scope: "write", - duration: { - d_us: "forever", - }, - refreshable: true, - }); + const resp = await lib.instance.createAuthTokenFromToken(newToken, FOREVER_REFRESHABLE_TOKEN); if (resp.type === "ok") { logIn(state.instance, resp.body.token); return; @@ -108,13 +102,10 @@ export function AdminToken(props: Props & { instanceId: string }): VNode { throw Error(resp.detail?.hint ?? "The request failed"); } } - const resp = await subInstanceLib.createAuthTokenFromToken(newToken, { - scope: "write", - duration: { - d_us: "forever", - }, - refreshable: true, - }); + const resp = await subInstanceLib.createAuthTokenFromToken( + newToken, + FOREVER_REFRESHABLE_TOKEN, + ); if (resp.type === "ok") { return; } else { diff --git a/packages/merchant-backoffice-ui/src/paths/login/index.tsx b/packages/merchant-backoffice-ui/src/paths/login/index.tsx @@ -25,16 +25,16 @@ import { createRFC8959AccessTokenEncoded, } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { ComponentChildren, Fragment, VNode, h } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { AsyncButton } from "../../components/exception/AsyncButton.js"; import { NotificationCard } from "../../components/menu/index.js"; import { useSessionContext } from "../../context/session.js"; import { Notification } from "../../utils/types.js"; -import { AsyncButton } from "../../components/exception/AsyncButton.js"; interface Props {} -const tokenRequest = { +export const FOREVER_REFRESHABLE_TOKEN = { scope: "write", duration: { d_us: "forever" as const, @@ -56,7 +56,7 @@ export function LoginPage(_p: Props): VNode { const result = await api.createAuthTokenFromToken( createRFC8959AccessTokenEncoded(token), - tokenRequest, + FOREVER_REFRESHABLE_TOKEN, ); if (result.type === "ok") { const { token } = result.body; diff --git a/packages/taler-harness/src/integrationtests/test-tops-aml-legi.ts b/packages/taler-harness/src/integrationtests/test-tops-aml-legi.ts @@ -57,7 +57,7 @@ export async function runTopsAmlLegiTest(t: GlobalTestState) { }); // Do KYC auth transfer - const { accessToken, merchantPaytoHash } = await doTopsKycAuth(t, { + const { accessToken } = await doTopsKycAuth(t, { merchantClient, exchangeBankAccount, wireGatewayApi, @@ -72,9 +72,7 @@ export async function runTopsAmlLegiTest(t: GlobalTestState) { }); const legis = succeedOrThrow( - await exchangeClient.getAmlLegitimizations({ - officerAcc, - }), + await exchangeClient.getAmlLegitimizations(officerAcc), ); console.log(j2s(legis)); diff --git a/packages/taler-util/src/http-client/exchange-client.ts b/packages/taler-util/src/http-client/exchange-client.ts @@ -98,6 +98,7 @@ import { codecForExchangeWithdrawResponse, codecForKycProcessClientInformation, codecForKycProcessStartInformation, + codecForLegitimizationMeasuresList, codecForLegitimizationNeededResponse, codecForPurseConflict, codecForPurseConflictPartial, @@ -600,35 +601,6 @@ export class TalerExchangeHttpClient { } } - async getAmlLegitimizations(args: { - officerAcc: OfficerAccount; - }): Promise<OperationOk<LegitimizationMeasuresList>> { - const url = new URL( - `aml/${args.officerAcc.id}/legitimizations`, - this.baseUrl, - ); - - const resp = await this.httpLib.fetch(url.href, { - headers: { - "Taler-AML-Officer-Signature": encodeCrock( - signAmlQuery(args.officerAcc.signingKey), - ), - }, - }); - - switch (resp.status) { - case HttpStatusCode.Ok: - // FIXME: Parse properly. - return opSuccessFromHttp(resp, codecForAny()); - case HttpStatusCode.NoContent: - return opFixedSuccess({ - measures: [], - }); - default: - return opUnknownHttpFailure(resp); - } - } - /** * https://docs.taler.net/core/api-exchange.html#get--kyc-check-$H_NORMALIZED_PAYTO * @@ -955,7 +927,7 @@ export class TalerExchangeHttpClient { } /** - * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-measures + * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-kyc-statistics-$NAME * */ async getAmlKycStatistics( @@ -1060,6 +1032,46 @@ export class TalerExchangeHttpClient { } /** + * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-legitimizations + */ + async getAmlLegitimizations( + officer: OfficerAccount, + params: PaginationParams & { + account?: PaytoHash; + active?: boolean; + } = {}, + ): Promise<OperationOk<LegitimizationMeasuresList>> { + const url = new URL(`aml/${officer.id}/legitimizations`, this.baseUrl); + + addPaginationParams(url, params); + if (params.account !== undefined) { + url.searchParams.set("h_payto", params.account); + } + if (params.active !== undefined) { + url.searchParams.set("active", params.active ? "YES" : "NO"); + } + + const resp = await this.httpLib.fetch(url.href, { + headers: { + "Taler-AML-Officer-Signature": encodeCrock( + signAmlQuery(officer.signingKey), + ), + }, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForLegitimizationMeasuresList()); + case HttpStatusCode.NoContent: + return opFixedSuccess({ + measures: [], + }); + default: + return opUnknownHttpFailure(resp); + } + } + + /** * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-attributes-$H_PAYTO * */ diff --git a/packages/taler-util/src/types-taler-exchange.ts b/packages/taler-util/src/types-taler-exchange.ts @@ -2612,10 +2612,32 @@ export const codecForEventCounter = (): Codec<EventCounter> => .property("counter", codecForNumber()) .build("TalerExchangeApi.EventCounter"); -export const codecForAmlDecisionsResponse = (): Codec<AmlDecisionsResponse> => - buildCodecForObject<AmlDecisionsResponse>() - .property("records", codecForList(codecForAmlDecision())) - .build("TalerExchangeApi.AmlDecisionsResponse"); +export const codecForLegitimizationMeasuresList = + (): Codec<LegitimizationMeasuresList> => + buildCodecForObject<LegitimizationMeasuresList>() + .property( + "measures", + codecForList(codecForLegitimizationMeasureDetails()), + ) + .build("TalerExchangeApi.LegitimizationMeasuresList"); + +export const codecForLegitimizationMeasureDetails = + (): Codec<LegitimizationMeasureDetails> => + buildCodecForObject<LegitimizationMeasureDetails>() + .property("h_payto", codecForAny()) + .property("rowid", codecForAny()) + .property("start_time", codecForAny()) + .property("measures", codecForAny()) + .property("is_finished", codecForAny()) + .build("TalerExchangeApi.LegitimizationMeasureDetails"); + +export const codecForLegitimizationMeasures = + (): Codec<LegitimizationMeasures> => + buildCodecForObject<LegitimizationMeasures>() + .property("is_and_combinator", codecForAny()) + .property("verboten", codecForAny()) + .property("measures", codecForList(codecForMeasureInformation())) + .build("TalerExchangeApi.LegitimizationMeasures"); export const codecForAvailableMeasureSummary = (): Codec<AvailableMeasureSummary> => @@ -2654,6 +2676,11 @@ export const codecForMeasureInformation = (): Codec<MeasureInformation> => .property("voluntary", codecOptional(codecForBoolean())) .build("TalerExchangeApi.MeasureInformation"); +export const codecForAmlDecisionsResponse = (): Codec<AmlDecisionsResponse> => + buildCodecForObject<AmlDecisionsResponse>() + .property("records", codecForList(codecForAmlDecision())) + .build("TalerExchangeApi.AmlDecisionsResponse"); + // export const codecForAmlDecisionDetails = (): Codec<AmlDecisionDetails> => // buildCodecForObject<AmlDecisionDetails>() // .property("aml_history", codecForList(codecForAmlDecisionDetail()))