From 2335c3418cfbcc8a0196f0f161bab31ade99acb2 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 20 Jul 2023 17:01:35 -0300 Subject: make signed request to exchange --- packages/aml-backoffice-ui/src/account.ts | 28 ++- packages/aml-backoffice-ui/src/forms/902_11e.ts | 9 +- packages/aml-backoffice-ui/src/forms/902_12e.ts | 9 +- packages/aml-backoffice-ui/src/forms/902_13e.ts | 9 +- packages/aml-backoffice-ui/src/forms/902_15e.ts | 9 +- packages/aml-backoffice-ui/src/forms/902_1e.ts | 11 +- packages/aml-backoffice-ui/src/forms/902_4e.ts | 15 +- packages/aml-backoffice-ui/src/forms/902_5e.ts | 9 +- packages/aml-backoffice-ui/src/forms/902_9e.ts | 9 +- packages/aml-backoffice-ui/src/forms/simplest.ts | 18 +- packages/aml-backoffice-ui/src/hooks/useBackend.ts | 81 ++++++++ packages/aml-backoffice-ui/src/hooks/useCases.ts | 94 ++++++++++ .../src/pages/AntiMoneyLaunderingForm.tsx | 6 +- .../aml-backoffice-ui/src/pages/CaseDetails.tsx | 45 ++--- packages/aml-backoffice-ui/src/pages/Cases.tsx | 204 +++++++++++---------- .../aml-backoffice-ui/src/pages/NewFormEntry.tsx | 4 +- .../aml-backoffice-ui/src/pages/UnlockAccount.tsx | 3 + packages/aml-backoffice-ui/src/settings.ts | 35 ++++ packages/aml-backoffice-ui/src/types.ts | 167 +++++++++-------- packages/aml-backoffice-ui/src/utils/Loading.tsx | 43 +++++ packages/aml-backoffice-ui/src/utils/QR.tsx | 54 ++++++ packages/aml-backoffice-ui/src/utils/errors.tsx | 77 ++++++++ 22 files changed, 665 insertions(+), 274 deletions(-) create mode 100644 packages/aml-backoffice-ui/src/hooks/useBackend.ts create mode 100644 packages/aml-backoffice-ui/src/hooks/useCases.ts create mode 100644 packages/aml-backoffice-ui/src/settings.ts create mode 100644 packages/aml-backoffice-ui/src/utils/Loading.tsx create mode 100644 packages/aml-backoffice-ui/src/utils/QR.tsx create mode 100644 packages/aml-backoffice-ui/src/utils/errors.tsx diff --git a/packages/aml-backoffice-ui/src/account.ts b/packages/aml-backoffice-ui/src/account.ts index bd3c2003e..1c8cd7f53 100644 --- a/packages/aml-backoffice-ui/src/account.ts +++ b/packages/aml-backoffice-ui/src/account.ts @@ -1,14 +1,20 @@ import { - bytesToString, + PaytoUri, + TalerSignaturePurpose, + bufferForUint32, + buildSigPS, createEddsaKeyPair, decodeCrock, decryptWithDerivedKey, eddsaGetPublic, + eddsaSign, encodeCrock, encryptWithDerivedKey, getRandomBytesF, stringToBytes, + stringifyPaytoUri, } from "@gnu-taler/taler-util"; +import { AmlExchangeBackend } from "./types.js"; export interface Account { accountId: AccountId; @@ -45,6 +51,26 @@ export async function unlockAccount( return { accountId, signingKey }; } +export function buildQuerySignature(key: SigningKey): string { + const sigBlob = buildSigPS( + TalerSignaturePurpose.TALER_SIGNATURE_AML_QUERY, + ).build(); + + return encodeCrock(eddsaSign(sigBlob, key)); +} +export function buildDecisionSignature( + key: SigningKey, + payto: PaytoUri, + state: AmlExchangeBackend.AmlState, +): string { + const sigBlob = buildSigPS(TalerSignaturePurpose.TALER_SIGNATURE_AML_DECISION) + .put(decodeCrock(stringifyPaytoUri(payto))) + .put(bufferForUint32(state)) + .build(); + + return encodeCrock(eddsaSign(sigBlob, key)); +} + declare const opaque_Account: unique symbol; export type LockedAccount = string & { [opaque_Account]: true }; diff --git a/packages/aml-backoffice-ui/src/forms/902_11e.ts b/packages/aml-backoffice-ui/src/forms/902_11e.ts index 24df6a44c..a91e7a866 100644 --- a/packages/aml-backoffice-ui/src/forms/902_11e.ts +++ b/packages/aml-backoffice-ui/src/forms/902_11e.ts @@ -1,12 +1,7 @@ -import { - AbsoluteTime, - AmountJson, - TranslatedString, -} from "@gnu-taler/taler-util"; +import { TranslatedString } from "@gnu-taler/taler-util"; import { FormState } from "../handlers/FormProvider.js"; -import { FlexibleForm } from "./index.js"; import { State } from "../pages/AntiMoneyLaunderingForm.js"; -import { AmlState } from "../types.js"; +import { FlexibleForm } from "./index.js"; import { Simplest, resolutionSection } from "./simplest.js"; export const v1 = (current: State): FlexibleForm => ({ diff --git a/packages/aml-backoffice-ui/src/forms/902_12e.ts b/packages/aml-backoffice-ui/src/forms/902_12e.ts index c80539511..ea95b494b 100644 --- a/packages/aml-backoffice-ui/src/forms/902_12e.ts +++ b/packages/aml-backoffice-ui/src/forms/902_12e.ts @@ -1,12 +1,7 @@ -import { - AbsoluteTime, - AmountJson, - TranslatedString, -} from "@gnu-taler/taler-util"; +import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util"; import { FormState } from "../handlers/FormProvider.js"; -import { FlexibleForm } from "./index.js"; import { State } from "../pages/AntiMoneyLaunderingForm.js"; -import { AmlState } from "../types.js"; +import { FlexibleForm } from "./index.js"; import { Simplest, resolutionSection } from "./simplest.js"; export const v1 = (current: State): FlexibleForm => ({ diff --git a/packages/aml-backoffice-ui/src/forms/902_13e.ts b/packages/aml-backoffice-ui/src/forms/902_13e.ts index 63870f00a..666cf35d4 100644 --- a/packages/aml-backoffice-ui/src/forms/902_13e.ts +++ b/packages/aml-backoffice-ui/src/forms/902_13e.ts @@ -1,12 +1,7 @@ -import { - AbsoluteTime, - AmountJson, - TranslatedString, -} from "@gnu-taler/taler-util"; +import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util"; import { FormState } from "../handlers/FormProvider.js"; -import { FlexibleForm } from "./index.js"; import { State } from "../pages/AntiMoneyLaunderingForm.js"; -import { AmlState } from "../types.js"; +import { FlexibleForm } from "./index.js"; import { Simplest, resolutionSection } from "./simplest.js"; export const v1 = (current: State): FlexibleForm => ({ diff --git a/packages/aml-backoffice-ui/src/forms/902_15e.ts b/packages/aml-backoffice-ui/src/forms/902_15e.ts index 19a16d3f2..502cee8e5 100644 --- a/packages/aml-backoffice-ui/src/forms/902_15e.ts +++ b/packages/aml-backoffice-ui/src/forms/902_15e.ts @@ -1,12 +1,7 @@ -import { - AbsoluteTime, - AmountJson, - TranslatedString, -} from "@gnu-taler/taler-util"; +import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util"; import { FormState } from "../handlers/FormProvider.js"; -import { FlexibleForm } from "./index.js"; import { State } from "../pages/AntiMoneyLaunderingForm.js"; -import { AmlState } from "../types.js"; +import { FlexibleForm } from "./index.js"; import { Simplest, resolutionSection } from "./simplest.js"; export const v1 = (current: State): FlexibleForm => ({ diff --git a/packages/aml-backoffice-ui/src/forms/902_1e.ts b/packages/aml-backoffice-ui/src/forms/902_1e.ts index 654085443..167d1ac19 100644 --- a/packages/aml-backoffice-ui/src/forms/902_1e.ts +++ b/packages/aml-backoffice-ui/src/forms/902_1e.ts @@ -1,14 +1,7 @@ -import { - AbsoluteTime, - AmountJson, - Amounts, - TranslatedString, -} from "@gnu-taler/taler-util"; -import { FlexibleForm, languageList } from "./index.js"; +import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util"; import { FormState } from "../handlers/FormProvider.js"; import { State } from "../pages/AntiMoneyLaunderingForm.js"; -import { AmlState } from "../types.js"; -import { amlStateConverter } from "../pages/CaseDetails.js"; +import { FlexibleForm, languageList } from "./index.js"; import { Simplest, resolutionSection } from "./simplest.js"; export const v1 = (current: State): FlexibleForm => ({ diff --git a/packages/aml-backoffice-ui/src/forms/902_4e.ts b/packages/aml-backoffice-ui/src/forms/902_4e.ts index f77a2f63a..cecd74390 100644 --- a/packages/aml-backoffice-ui/src/forms/902_4e.ts +++ b/packages/aml-backoffice-ui/src/forms/902_4e.ts @@ -1,17 +1,10 @@ -import { - AbsoluteTime, - AmountJson, - Amounts, - TranslatedString, -} from "@gnu-taler/taler-util"; -import { FormState } from "../handlers/FormProvider.js"; -import { FlexibleForm } from "./index.js"; +import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util"; import { ArrowRightIcon } from "@heroicons/react/24/outline"; -import { h as create } from "preact"; import { ChevronRightIcon } from "@heroicons/react/24/solid"; +import { h as create } from "preact"; +import { FormState } from "../handlers/FormProvider.js"; import { State } from "../pages/AntiMoneyLaunderingForm.js"; -import { AmlState } from "../types.js"; -import { amlStateConverter } from "../pages/CaseDetails.js"; +import { FlexibleForm } from "./index.js"; import { Simplest, resolutionSection } from "./simplest.js"; export const v1 = (current: State): FlexibleForm => ({ diff --git a/packages/aml-backoffice-ui/src/forms/902_5e.ts b/packages/aml-backoffice-ui/src/forms/902_5e.ts index bd27b7a7f..501a3b23c 100644 --- a/packages/aml-backoffice-ui/src/forms/902_5e.ts +++ b/packages/aml-backoffice-ui/src/forms/902_5e.ts @@ -1,12 +1,7 @@ -import { - AbsoluteTime, - AmountJson, - TranslatedString, -} from "@gnu-taler/taler-util"; +import { TranslatedString } from "@gnu-taler/taler-util"; import { FormState } from "../handlers/FormProvider.js"; -import { FlexibleForm, currencyList } from "./index.js"; import { State } from "../pages/AntiMoneyLaunderingForm.js"; -import { AmlState } from "../types.js"; +import { FlexibleForm, currencyList } from "./index.js"; import { Simplest, resolutionSection } from "./simplest.js"; export const v1 = (current: State): FlexibleForm => ({ diff --git a/packages/aml-backoffice-ui/src/forms/902_9e.ts b/packages/aml-backoffice-ui/src/forms/902_9e.ts index e79597bfb..04f0a1572 100644 --- a/packages/aml-backoffice-ui/src/forms/902_9e.ts +++ b/packages/aml-backoffice-ui/src/forms/902_9e.ts @@ -1,12 +1,7 @@ -import { - AbsoluteTime, - AmountJson, - TranslatedString, -} from "@gnu-taler/taler-util"; +import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util"; import { FormState } from "../handlers/FormProvider.js"; -import { FlexibleForm } from "./index.js"; import { State } from "../pages/AntiMoneyLaunderingForm.js"; -import { AmlState } from "../types.js"; +import { FlexibleForm } from "./index.js"; import { Simplest, resolutionSection } from "./simplest.js"; export const v1 = (current: State): FlexibleForm => ({ diff --git a/packages/aml-backoffice-ui/src/forms/simplest.ts b/packages/aml-backoffice-ui/src/forms/simplest.ts index 7eda03fef..023c1765f 100644 --- a/packages/aml-backoffice-ui/src/forms/simplest.ts +++ b/packages/aml-backoffice-ui/src/forms/simplest.ts @@ -5,11 +5,11 @@ import { TranslatedString, } from "@gnu-taler/taler-util"; import { FormState } from "../handlers/FormProvider.js"; -import { FlexibleForm } from "./index.js"; -import { AmlState } from "../types.js"; -import { amlStateConverter } from "../pages/CaseDetails.js"; +import { DoubleColumnFormSection } from "../handlers/forms.js"; import { State } from "../pages/AntiMoneyLaunderingForm.js"; -import { DoubleColumnFormSection, UIFormField } from "../handlers/forms.js"; +import { amlStateConverter } from "../pages/CaseDetails.js"; +import { AmlExchangeBackend } from "../types.js"; +import { FlexibleForm } from "./index.js"; export const v1 = (current: State): FlexibleForm => ({ versionId: "2023-05-25", @@ -36,7 +36,7 @@ export const v1 = (current: State): FlexibleForm => ({ disabled: true, }, threshold: { - disabled: v.state === AmlState.frozen, + disabled: v.state === AmlExchangeBackend.AmlState.frozen, }, }; }, @@ -46,7 +46,7 @@ export namespace Simplest { export interface WithResolution { when: AbsoluteTime; threshold: AmountJson; - state: AmlState; + state: AmlExchangeBackend.AmlState; } export interface Form extends WithResolution { comment: string; @@ -77,15 +77,15 @@ export function resolutionSection(current: State): DoubleColumnFormSection { converter: amlStateConverter, choices: [ { - value: AmlState.frozen, + value: AmlExchangeBackend.AmlState.frozen, label: "Frozen" as TranslatedString, }, { - value: AmlState.pending, + value: AmlExchangeBackend.AmlState.pending, label: "Pending" as TranslatedString, }, { - value: AmlState.normal, + value: AmlExchangeBackend.AmlState.normal, label: "Normal" as TranslatedString, }, ], diff --git a/packages/aml-backoffice-ui/src/hooks/useBackend.ts b/packages/aml-backoffice-ui/src/hooks/useBackend.ts new file mode 100644 index 000000000..e68efb2e3 --- /dev/null +++ b/packages/aml-backoffice-ui/src/hooks/useBackend.ts @@ -0,0 +1,81 @@ +import { + HttpResponseOk, + RequestOptions, + useApiContext, +} from "@gnu-taler/web-util/browser"; +import { useCallback } from "preact/hooks"; +import { uiSettings } from "../settings.js"; +import { canonicalizeBaseUrl } from "@gnu-taler/taler-util"; +import { useOfficer } from "./useOfficer.js"; +import { buildQuerySignature } from "../account.js"; + +interface useBackendType { + request: ( + path: string, + options?: RequestOptions, + ) => Promise>; + fetcher: (endpoint: string) => Promise>; + paginatedFetcher: ( + args: [string, number, number, string], + ) => Promise>; +} +export function usePublicBackend(): useBackendType { + const { request: requestHandler } = useApiContext(); + + const baseUrl = getInitialBackendBaseURL(); + + const request = useCallback( + function requestImpl( + path: string, + options: RequestOptions = {}, + ): Promise> { + return requestHandler(baseUrl, path, options); + }, + [baseUrl], + ); + + const fetcher = useCallback( + function fetcherImpl(endpoint: string): Promise> { + return requestHandler(baseUrl, endpoint); + }, + [baseUrl], + ); + const paginatedFetcher = useCallback( + function fetcherImpl([endpoint, page, size, talerAmlOfficerSignature]: [ + string, + number, + number, + string, + ]): Promise> { + return requestHandler(baseUrl, endpoint, { + params: { page: page || 1, size }, + talerAmlOfficerSignature, + }); + }, + [baseUrl], + ); + return { + request, + fetcher, + paginatedFetcher, + }; +} + +export function getInitialBackendBaseURL(): string { + const overrideUrl = + typeof localStorage !== "undefined" + ? localStorage.getItem("exchange-aml-base-url") + : undefined; + if (!overrideUrl) { + //normal path + if (!uiSettings.backendBaseURL) { + console.error( + "ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'", + ); + return canonicalizeBaseUrl(window.origin); + } + return canonicalizeBaseUrl(uiSettings.backendBaseURL); + } + // testing/development path + return canonicalizeBaseUrl(overrideUrl); +} diff --git a/packages/aml-backoffice-ui/src/hooks/useCases.ts b/packages/aml-backoffice-ui/src/hooks/useCases.ts new file mode 100644 index 000000000..04b7c383b --- /dev/null +++ b/packages/aml-backoffice-ui/src/hooks/useCases.ts @@ -0,0 +1,94 @@ +import { useEffect, useState } from "preact/hooks"; + +import { AmlExchangeBackend } from "../types.js"; +import { + HttpResponse, + HttpResponseOk, + HttpResponsePaginated, + RequestError, +} from "@gnu-taler/web-util/browser"; +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import _useSWR, { SWRHook } from "swr"; +import { usePublicBackend } from "./useBackend.js"; +import { AccountId, buildQuerySignature } from "../account.js"; +import { useOfficer } from "./useOfficer.js"; +const useSWR = _useSWR as unknown as SWRHook; + +const PAGE_SIZE = 10; +const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1; +/** + * FIXME: mutate result when balance change (transaction ) + * @param account + * @param args + * @returns + */ +export function useCases( + account: AccountId, + state: AmlExchangeBackend.AmlState, + signature: string | undefined, +): HttpResponsePaginated< + AmlExchangeBackend.AmlRecords, + AmlExchangeBackend.AmlError +> { + const { paginatedFetcher } = usePublicBackend(); + + const [page, setPage] = useState(1); + + const { + data: afterData, + error: afterError, + isValidating: loadingAfter, + } = useSWR< + HttpResponseOk, + RequestError + >( + [ + `aml/${account}/decisions/${AmlExchangeBackend.AmlState[state]}`, + page, + PAGE_SIZE, + signature, + ], + paginatedFetcher, + ); + + const [lastAfter, setLastAfter] = useState< + HttpResponse + >({ loading: true }); + + useEffect(() => { + if (afterData) setLastAfter(afterData); + }, [afterData]); + + if (afterError) { + return afterError.cause; + } + + // if the query returns less that we ask, then we have reach the end or beginning + const isReachingEnd = + afterData && afterData.data && afterData.data.records.length < PAGE_SIZE; + const isReachingStart = false; + + const pagination = { + isReachingEnd, + isReachingStart, + loadMore: () => { + if (!afterData || isReachingEnd) return; + if (afterData.data && afterData.data.records.length < MAX_RESULT_SIZE) { + setPage(page + 1); + } + }, + loadMorePrev: () => { + null; + }, + }; + + const records = !afterData + ? [] + : ((afterData ?? lastAfter).data ?? { records: [] }).records; + console.log("afterdata", afterData, lastAfter, records) + if (loadingAfter) return { loading: true, data: { records } }; + if (afterData) { + return { ok: true, data: { records }, ...pagination }; + } + return { loading: true }; +} diff --git a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx index 713c0d7c1..c3fb7dafe 100644 --- a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx +++ b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx @@ -11,8 +11,8 @@ import { v1 as form_902_9e_v1 } from "../forms/902_9e.js"; import { v1 as simplest } from "../forms/simplest.js"; import { DocumentDuplicateIcon } from "@heroicons/react/24/solid"; import { AbsoluteTime } from "@gnu-taler/taler-util"; -import { AmlState } from "../types.js"; import { AmountJson, Amounts } from "@gnu-taler/taler-util"; +import { AmlExchangeBackend } from "../types.js"; export function AntiMoneyLaunderingForm({ number }: { number?: string }) { const selectedForm = Number.parseInt(number ?? "0", 10); @@ -28,7 +28,7 @@ export function AntiMoneyLaunderingForm({ number }: { number?: string }) { {}} @@ -37,7 +37,7 @@ export function AntiMoneyLaunderingForm({ number }: { number?: string }) { } export interface State { - state: AmlState; + state: AmlExchangeBackend.AmlState; threshold: AmountJson; } diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx index e5fb8eaba..d02d8b395 100644 --- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx +++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -1,10 +1,4 @@ import { Fragment, VNode, h } from "preact"; -import { - AmlDecisionDetail, - AmlDecisionDetails, - AmlState, - KycDetail, -} from "../types.js"; import { AbsoluteTime, AmountJson, @@ -18,8 +12,9 @@ import { NiceForm } from "../NiceForm.js"; import { FlexibleForm } from "../forms/index.js"; import { UIFormField } from "../handlers/forms.js"; import { Pages } from "../pages.js"; +import { AmlExchangeBackend } from "../types.js"; -const response: AmlDecisionDetails = { +const response: AmlExchangeBackend.AmlDecisionDetails = { aml_history: [ { justification: "Lack of documentation", @@ -81,7 +76,7 @@ type AmlFormEvent = { type: "aml-form"; when: AbsoluteTime; title: TranslatedString; - state: AmlState; + state: AmlExchangeBackend.AmlState; threshold: AmountJson; }; type KycCollectionEvent = { @@ -105,8 +100,8 @@ function selectSooner(a: WithTime, b: WithTime) { } function getEventsFromAmlHistory( - aml: AmlDecisionDetail[], - kyc: KycDetail[], + aml: AmlExchangeBackend.AmlDecisionDetail[], + kyc: AmlExchangeBackend.KycDetail[], ): AmlEvent[] { const ae: AmlEvent[] = aml.map((a) => { return { @@ -187,7 +182,7 @@ export function CaseDetails({ account }: { account?: string }) { switch (e.type) { case "aml-form": { switch (e.state) { - case AmlState.normal: { + case AmlExchangeBackend.AmlState.normal: { return (
@@ -200,7 +195,7 @@ export function CaseDetails({ account }: { account?: string }) {
); } - case AmlState.pending: { + case AmlExchangeBackend.AmlState.pending: { return (
@@ -213,7 +208,7 @@ export function CaseDetails({ account }: { account?: string }) {
); } - case AmlState.frozen: { + case AmlExchangeBackend.AmlState.frozen: { return (
@@ -304,15 +299,15 @@ function ShowConsolidated({ choices: [ { label: "Frozen" as TranslatedString, - value: AmlState.frozen, + value: AmlExchangeBackend.AmlState.frozen, }, { label: "Pending" as TranslatedString, - value: AmlState.pending, + value: AmlExchangeBackend.AmlState.pending, }, { label: "Normal" as TranslatedString, - value: AmlState.normal, + value: AmlExchangeBackend.AmlState.normal, }, ], }, @@ -361,7 +356,7 @@ function ShowConsolidated({ interface Consolidated { aml: { - state?: AmlState; + state?: AmlExchangeBackend.AmlState; threshold?: AmountJson; since: AbsoluteTime; }; @@ -421,26 +416,26 @@ export const amlStateConverter = { fromStringUI: parseAmlState, }; -function stringifyAmlState(s: AmlState | undefined): string { +function stringifyAmlState(s: AmlExchangeBackend.AmlState | undefined): string { if (s === undefined) return ""; switch (s) { - case AmlState.normal: + case AmlExchangeBackend.AmlState.normal: return "normal"; - case AmlState.pending: + case AmlExchangeBackend.AmlState.pending: return "pending"; - case AmlState.frozen: + case AmlExchangeBackend.AmlState.frozen: return "frozen"; } } -function parseAmlState(s: string | undefined): AmlState { +function parseAmlState(s: string | undefined): AmlExchangeBackend.AmlState { switch (s) { case "normal": - return AmlState.normal; + return AmlExchangeBackend.AmlState.normal; case "pending": - return AmlState.pending; + return AmlExchangeBackend.AmlState.pending; case "frozen": - return AmlState.frozen; + return AmlExchangeBackend.AmlState.frozen; default: throw Error(`unknown AML state: ${s}`); } diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx index 28b9d2a88..d96e7a90c 100644 --- a/packages/aml-backoffice-ui/src/pages/Cases.tsx +++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx @@ -1,15 +1,18 @@ -import { VNode, h } from "preact"; -import { Pages } from "../pages.js"; -import { AmlRecords, AmlState } from "../types.js"; -import { InputChoiceHorizontal } from "../handlers/InputChoiceHorizontal.js"; -import { createNewForm } from "../handlers/forms.js"; import { TranslatedString } from "@gnu-taler/taler-util"; -import { amlStateConverter as amlStateConverter } from "./CaseDetails.js"; +import { VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { HandleAccountNotReady } from "./HandleAccountNotReady.js"; +import { createNewForm } from "../handlers/forms.js"; +import { useCases } from "../hooks/useCases.js"; import { useOfficer } from "../hooks/useOfficer.js"; +import { Pages } from "../pages.js"; +import { AmlExchangeBackend } from "../types.js"; +import { amlStateConverter } from "./CaseDetails.js"; +import { HandleAccountNotReady } from "./HandleAccountNotReady.js"; +import { buildQuerySignature } from "../account.js"; +import { handleNotOkResult } from "../utils/errors.js"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; -const response: AmlRecords = { +const response: AmlExchangeBackend.AmlRecords = { records: [ { current_state: 0, @@ -56,7 +59,7 @@ const response: AmlRecords = { function doFilter( list: typeof response.records, - filter: AmlState | undefined, + filter: AmlExchangeBackend.AmlState | undefined, ): typeof response.records { if (filter === undefined) return list; return list.filter((r) => r.current_state === filter); @@ -64,14 +67,27 @@ function doFilter( export function Cases() { const officer = useOfficer(); + const { i18n } = useTranslationContext(); if (officer.state !== "ready") { return ; } const form = createNewForm<{ - state: AmlState; + state: AmlExchangeBackend.AmlState; }>(); - const initial = { state: AmlState.pending }; - const [list, setList] = useState(doFilter(response.records, initial.state)); + + const signature = + officer.state === "ready" + ? buildQuerySignature(officer.account.signingKey) + : undefined; + + const initial = AmlExchangeBackend.AmlState.pending; + const [stateFilter, setStateFilter] = useState(initial); + const list = useCases(officer.account.accountId, stateFilter, signature); + + if (!list.ok && !list.loading) { + return handleNotOkResult(i18n)(list); + } + return (
@@ -85,9 +101,9 @@ export function Cases() {

{ - setList(doFilter(response.records, v.state)); + setStateFilter(v.state ?? initial); }} onSubmit={(v) => {}} > @@ -98,15 +114,15 @@ export function Cases() { choices={[ { label: "Pending" as TranslatedString, - value: AmlState.pending, + value: AmlExchangeBackend.AmlState.pending, }, { label: "Frozen" as TranslatedString, - value: AmlState.frozen, + value: AmlExchangeBackend.AmlState.frozen, }, { label: "Normal" as TranslatedString, - value: AmlState.normal, + value: AmlExchangeBackend.AmlState.normal, }, ]} /> @@ -114,82 +130,86 @@ export function Cases() {
-
- - - - - - - - - - - {list.map((r) => { - return ( - - - + + + ); + })} + +
- Account Id - - Status - - Threshold -
- - - {((state: AmlState): VNode => { - switch (state) { - case AmlState.normal: { - return ( - - Normal - - ); - } - case AmlState.pending: { - return ( - - Pending - - ); - } - case AmlState.frozen: { - return ( - - Frozen - - ); + {!list.data.records.length ? ( +
empty result
+ ) : ( +
+ + + + + + + + + + + {list.data.records.map((r) => { + return ( + + + - - - ); - })} - -
+ Account Id + + Status + + Threshold +
+ + + {((state: AmlExchangeBackend.AmlState): VNode => { + switch (state) { + case AmlExchangeBackend.AmlState.normal: { + return ( + + Normal + + ); + } + case AmlExchangeBackend.AmlState.pending: { + return ( + + Pending + + ); + } + case AmlExchangeBackend.AmlState.frozen: { + return ( + + Frozen + + ); + } } - } - })(r.current_state)} - - {r.threshold} -
- -
+ })(r.current_state)} +
+ {r.threshold} +
+ +
+ )}
diff --git a/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx b/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx index fdb255701..bbd04daee 100644 --- a/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx +++ b/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx @@ -2,8 +2,8 @@ import { VNode, h } from "preact"; import { allForms } from "./AntiMoneyLaunderingForm.js"; import { Pages } from "../pages.js"; import { NiceForm } from "../NiceForm.js"; -import { AmlState } from "../types.js"; import { AbsoluteTime, Amounts } from "@gnu-taler/taler-util"; +import { AmlExchangeBackend } from "../types.js"; export function NewFormEntry({ account, @@ -27,7 +27,7 @@ export function NewFormEntry({ const initial = { fullName: "loggedIn_user_fullname", when: AbsoluteTime.now(), - state: AmlState.pending, + state: AmlExchangeBackend.AmlState.pending, threshold: Amounts.parseOrThrow("USD:10"), }; return ( diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx index 2ebac0718..39f8addd3 100644 --- a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx +++ b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx @@ -30,6 +30,9 @@ export function UnlockAccount({
{ try { await onAccountUnlocked(v.password!); diff --git a/packages/aml-backoffice-ui/src/settings.ts b/packages/aml-backoffice-ui/src/settings.ts new file mode 100644 index 000000000..2897874a2 --- /dev/null +++ b/packages/aml-backoffice-ui/src/settings.ts @@ -0,0 +1,35 @@ +/* + This file is part of GNU Taler + (C) 2022 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 + */ + +export interface UiSettings { + backendBaseURL: string; + allowRegistrations: boolean; + uiName: string; +} + +/** + * Global settings for the UI. + */ +const defaultSettings: UiSettings = { + backendBaseURL: "https://exchange.demo.taler.net/", + allowRegistrations: true, + uiName: "Taler Bank", +}; + +export const uiSettings: UiSettings = + "talerExchangeAmlSettings" in globalThis + ? (globalThis as any).talerExchangeAmlSettings + : defaultSettings; diff --git a/packages/aml-backoffice-ui/src/types.ts b/packages/aml-backoffice-ui/src/types.ts index 1197b6b35..104d938b3 100644 --- a/packages/aml-backoffice-ui/src/types.ts +++ b/packages/aml-backoffice-ui/src/types.ts @@ -1,81 +1,88 @@ -export interface AmlDecisionDetails { - // Array of AML decisions made for this account. Possibly - // contains only the most recent decision if "history" was - // not set to 'true'. - aml_history: AmlDecisionDetail[]; - - // Array of KYC attributes obtained for this account. - kyc_attributes: KycDetail[]; -} - -type AmlOfficerPublicKeyP = string; - -export interface AmlDecisionDetail { - // What was the justification given? - justification: string; - - // What is the new AML state. - new_state: Integer; - - // When was this decision made? - decision_time: Timestamp; - - // What is the new AML decision threshold (in monthly transaction volume)? - new_threshold: Amount; - - // Who made the decision? - decider_pub: AmlOfficerPublicKeyP; -} -export interface KycDetail { - // Name of the configuration section that specifies the provider - // which was used to collect the KYC details - provider_section: string; - - // The collected KYC data. NULL if the attribute data could not - // be decrypted (internal error of the exchange, likely the - // attribute key was changed). - attributes?: Object; - - // Time when the KYC data was collected - collection_time: Timestamp; - - // Time when the validity of the KYC data will expire - expiration_time: Timestamp; -} - -interface Timestamp { - // Seconds since epoch, or the special - // value "never" to represent an event that will - // never happen. - t_s: number | "never"; -} - -type PaytoHash = string; -type Integer = number; -type Amount = string; - -export interface AmlRecords { - // Array of AML records matching the query. - records: AmlRecord[]; -} - -interface AmlRecord { - // Which payto-address is this record about. - // Identifies a GNU Taler wallet or an affected bank account. - h_payto: PaytoHash; - - // What is the current AML state. - current_state: AmlState; - - // Monthly transaction threshold before a review will be triggered - threshold: Amount; - - // RowID of the record. - rowid: Integer; -} - -export enum AmlState { - normal = 0, - pending = 1, - frozen = 2, +export namespace AmlExchangeBackend { + // FIXME: placeholder + export interface AmlError { + code: number; + hint: string; + } + export interface AmlDecisionDetails { + // Array of AML decisions made for this account. Possibly + // contains only the most recent decision if "history" was + // not set to 'true'. + aml_history: AmlDecisionDetail[]; + + // Array of KYC attributes obtained for this account. + kyc_attributes: KycDetail[]; + } + + type AmlOfficerPublicKeyP = string; + + export interface AmlDecisionDetail { + // What was the justification given? + justification: string; + + // What is the new AML state. + new_state: Integer; + + // When was this decision made? + decision_time: Timestamp; + + // What is the new AML decision threshold (in monthly transaction volume)? + new_threshold: Amount; + + // Who made the decision? + decider_pub: AmlOfficerPublicKeyP; + } + export interface KycDetail { + // Name of the configuration section that specifies the provider + // which was used to collect the KYC details + provider_section: string; + + // The collected KYC data. NULL if the attribute data could not + // be decrypted (internal error of the exchange, likely the + // attribute key was changed). + attributes?: Object; + + // Time when the KYC data was collected + collection_time: Timestamp; + + // Time when the validity of the KYC data will expire + expiration_time: Timestamp; + } + + interface Timestamp { + // Seconds since epoch, or the special + // value "never" to represent an event that will + // never happen. + t_s: number | "never"; + } + + type PaytoHash = string; + type Integer = number; + type Amount = string; + + export interface AmlRecords { + // Array of AML records matching the query. + records: AmlRecord[]; + } + + interface AmlRecord { + // Which payto-address is this record about. + // Identifies a GNU Taler wallet or an affected bank account. + h_payto: PaytoHash; + + // What is the current AML state. + current_state: AmlState; + + // Monthly transaction threshold before a review will be triggered + threshold: Amount; + + // RowID of the record. + rowid: Integer; + } + + export enum AmlState { + normal = 0, + pending = 1, + frozen = 2, + } } diff --git a/packages/aml-backoffice-ui/src/utils/Loading.tsx b/packages/aml-backoffice-ui/src/utils/Loading.tsx new file mode 100644 index 000000000..7cbdad681 --- /dev/null +++ b/packages/aml-backoffice-ui/src/utils/Loading.tsx @@ -0,0 +1,43 @@ +/* + This file is part of GNU Taler + (C) 2022 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 + */ + +import { h, VNode } from "preact"; + +export function Loading(): VNode { + return ( +
+ +
+ ); +} + +export function Spinner(): VNode { + return ( +
+
+
+
+
+
+ ); +} diff --git a/packages/aml-backoffice-ui/src/utils/QR.tsx b/packages/aml-backoffice-ui/src/utils/QR.tsx new file mode 100644 index 000000000..1dc1712b7 --- /dev/null +++ b/packages/aml-backoffice-ui/src/utils/QR.tsx @@ -0,0 +1,54 @@ +/* + This file is part of GNU Taler + (C) 2022 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 + */ + +import { h, VNode } from "preact"; +import { useEffect, useRef } from "preact/hooks"; +// import qrcode from "qrcode-generator"; + +export function QR({ text }: { text: string }): VNode { + const divRef = useRef(null); + useEffect(() => { + // const qr = qrcode(0, "L"); + // qr.addData(text); + // qr.make(); + // if (divRef.current) + // divRef.current.innerHTML = qr.createSvgTag({ + // scalable: true, + // }); + }); + + return ( +
+
+
+ ); +} diff --git a/packages/aml-backoffice-ui/src/utils/errors.tsx b/packages/aml-backoffice-ui/src/utils/errors.tsx new file mode 100644 index 000000000..b67d61a5f --- /dev/null +++ b/packages/aml-backoffice-ui/src/utils/errors.tsx @@ -0,0 +1,77 @@ +import { + ErrorType, + HttpResponse, + HttpResponsePaginated, + notifyError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { VNode, h } from "preact"; +import { Loading } from "./Loading.js"; +import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util"; +import { AmlExchangeBackend } from "../types.js"; + +export function handleNotOkResult( + i18n: ReturnType["i18n"], +): ( + result: HttpResponsePaginated | HttpResponse, +) => VNode { + return function handleNotOkResult2( + result: HttpResponsePaginated | HttpResponse, + ): VNode { + if (result.loading) return ; + if (!result.ok) { + switch (result.type) { + case ErrorType.TIMEOUT: { + notifyError(i18n.str`Request timeout, try again later.`, undefined); + break; + } + case ErrorType.CLIENT: { + if (result.status === HttpStatusCode.Unauthorized) { + notifyError(i18n.str`Wrong credentials`, undefined); + return
not authorized
; + } + const errorData = result.payload; + notifyError( + i18n.str`Could not load due to a client error`, + errorData.hint as TranslatedString, + JSON.stringify(result), + ); + break; + } + case ErrorType.SERVER: { + notifyError( + i18n.str`Server returned with error`, + result.payload.hint as TranslatedString, + JSON.stringify(result.payload), + ); + break; + } + case ErrorType.UNREADABLE: { + notifyError( + i18n.str`Unexpected error.`, + `Response from ${result.info?.url} is unreadable, http status: ${result.status}` as TranslatedString, + JSON.stringify(result), + ); + break; + } + case ErrorType.UNEXPECTED: { + notifyError( + i18n.str`Unexpected error.`, + `Diagnostic from ${result.info?.url} is "${result.message}"` as TranslatedString, + JSON.stringify(result), + ); + break; + } + default: { + assertUnreachable(result); + } + } + + return
error
; + } + return
; + }; +} +export function assertUnreachable(x: never): never { + throw new Error("Didn't expect to get here"); +} -- cgit v1.2.3