diff options
Diffstat (limited to 'packages/aml-backoffice-ui')
-rw-r--r-- | packages/aml-backoffice-ui/src/App.tsx | 11 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/Dashboard.tsx | 26 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/account.ts | 128 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/context/config.ts | 89 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/handlers/Caption.tsx | 7 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/hooks/useBackend.ts | 27 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts | 103 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/hooks/useCases.ts | 126 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/hooks/useOfficer.ts | 20 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/CaseDetails.tsx | 83 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/Cases.tsx | 46 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx | 19 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/Officer.tsx | 4 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx | 3 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/utils/errors.tsx | 77 |
15 files changed, 297 insertions, 472 deletions
diff --git a/packages/aml-backoffice-ui/src/App.tsx b/packages/aml-backoffice-ui/src/App.tsx index 600131219..0e29279ff 100644 --- a/packages/aml-backoffice-ui/src/App.tsx +++ b/packages/aml-backoffice-ui/src/App.tsx @@ -1,12 +1,19 @@ import { TranslationProvider } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; -import { Dashboard } from "./Dashboard.js"; +import { ExchangeAmlFrame, Main } from "./Dashboard.js"; import "./scss/main.css"; +import { ExchangeApiProvider } from "./context/config.js"; +import { getInitialBackendBaseURL } from "./hooks/useBackend.js"; export function App(): VNode { + const baseUrl = getInitialBackendBaseURL(); return ( <TranslationProvider source={{}}> - <Dashboard /> + <ExchangeApiProvider baseUrl={baseUrl} frameOnError={ExchangeAmlFrame}> + <ExchangeAmlFrame> + <Main /> + </ExchangeAmlFrame> + </ExchangeApiProvider> </TranslationProvider> ); } diff --git a/packages/aml-backoffice-ui/src/Dashboard.tsx b/packages/aml-backoffice-ui/src/Dashboard.tsx index 6794ca1f8..bd8a48c45 100644 --- a/packages/aml-backoffice-ui/src/Dashboard.tsx +++ b/packages/aml-backoffice-ui/src/Dashboard.tsx @@ -182,7 +182,7 @@ function LeftMenu() { ); } -export function Dashboard({ +export function ExchangeAmlFrame({ children, }: { children?: ComponentChildren; @@ -211,21 +211,25 @@ export function Dashboard({ }} /> <Notifications /> - <main class="py-10 px-4 sm:px-6 lg:px-8"> - <div class="mx-auto max-w-3xl"> - <Router - pageList={pageList} - onNotFound={() => { - return <div>not found</div>; - }} - /> - </div> - </main> + {children} </div> </Fragment> ); } +export function Main(): VNode { + return <main class="py-10 px-4 sm:px-6 lg:px-8"> + <div class="mx-auto max-w-3xl"> + <Router + pageList={pageList} + onNotFound={() => { + return <div>not found</div>; + }} + /> + </div> + </main> +} + const pageList = Object.values(Pages); function NavigationBar({ diff --git a/packages/aml-backoffice-ui/src/account.ts b/packages/aml-backoffice-ui/src/account.ts deleted file mode 100644 index 615d843c4..000000000 --- a/packages/aml-backoffice-ui/src/account.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { - Amounts, - TalerSignaturePurpose, - amountToBuffer, - bufferForUint32, - buildSigPS, - createEddsaKeyPair, - decodeCrock, - decryptWithDerivedKey, - eddsaGetPublic, - eddsaSign, - encodeCrock, - encryptWithDerivedKey, - getRandomBytesF, - hash, - hashTruncate32, - stringToBytes, - timestampRoundedToBuffer -} from "@gnu-taler/taler-util"; -import { AmlExchangeBackend } from "./types.js"; - -export interface Account { - accountId: AccountId; - signingKey: SigningKey; -} - -/** - * Restore previous session and unlock account with password - * - * @param salt string from which crypto params will be derived - * @param key secured private key - * @param password password for the private key - * @returns - */ -export async function unlockAccount( - account: LockedAccount, - password: string, -): Promise<Account> { - const rawKey = decodeCrock(account); - const rawPassword = stringToBytes(password); - - const signingKey = (await decryptWithDerivedKey( - rawKey, - rawPassword, - password, - ).catch((e: Error) => { - throw new UnwrapKeyError(e.message); - })) as SigningKey; - - const publicKey = eddsaGetPublic(signingKey); - - const accountId = encodeCrock(publicKey) as AccountId; - - 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, - decision: AmlExchangeBackend.AmlDecision, -): string { - const zero = new Uint8Array(new ArrayBuffer(64)) - - const sigBlob = buildSigPS(TalerSignaturePurpose.TALER_SIGNATURE_AML_DECISION) - //TODO: new need the null terminator, also in the exchange - .put(hash(stringToBytes(decision.justification)))//check null - .put(timestampRoundedToBuffer(decision.decision_time)) - .put(amountToBuffer(decision.new_threshold)) - .put(decodeCrock(decision.h_payto)) - .put(zero) //kyc_requirement - .put(bufferForUint32(decision.new_state)) - .build(); - - return encodeCrock(eddsaSign(sigBlob, key)); -} - -declare const opaque_Account: unique symbol; -export type LockedAccount = string & { [opaque_Account]: true }; - -declare const opaque_AccountId: unique symbol; -export type AccountId = string & { [opaque_AccountId]: true }; - -declare const opaque_SigningKey: unique symbol; -export type SigningKey = Uint8Array & { [opaque_SigningKey]: true }; - -/** - * Create new account (secured private key) - * secured with the given password - * - * @param sessionId - * @param password - * @returns - */ -export async function createNewAccount( - password: string, -): Promise<Account & { safe: LockedAccount }> { - const { eddsaPriv, eddsaPub } = createEddsaKeyPair(); - - const key = stringToBytes(password); - - const protectedPrivKey = await encryptWithDerivedKey( - getRandomBytesF(24), - key, - eddsaPriv, - password, - ); - - const signingKey = eddsaPriv as SigningKey; - const accountId = encodeCrock(eddsaPub) as AccountId; - const safe = encodeCrock(protectedPrivKey) as LockedAccount; - - return { accountId, signingKey, safe }; -} - -export class UnwrapKeyError extends Error { - public cause: string; - constructor(cause: string) { - super(`Recovering private key failed on: ${cause}`); - this.cause = cause; - } -} diff --git a/packages/aml-backoffice-ui/src/context/config.ts b/packages/aml-backoffice-ui/src/context/config.ts new file mode 100644 index 000000000..2866717de --- /dev/null +++ b/packages/aml-backoffice-ui/src/context/config.ts @@ -0,0 +1,89 @@ +/* + 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 <http://www.gnu.org/licenses/> + */ + +import { TalerExchangeApi, TalerExchangeHttpClient, TalerError } from "@gnu-taler/taler-util"; +import { BrowserHttpLib, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ComponentChildren, createContext, FunctionComponent, h, VNode } from "preact"; +import { useContext, useEffect, useState } from "preact/hooks"; +import { ErrorLoading } from "@gnu-taler/web-util/browser"; + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +export type Type = { + url: URL, + config: TalerExchangeApi.ExchangeVersionResponse, + api: TalerExchangeHttpClient, +}; + +const Context = createContext<Type>(undefined as any); + +export const useExchangeApiContext = (): Type => useContext(Context); + +export type ConfigResult = undefined + | { type: "ok", config: TalerExchangeApi.ExchangeVersionResponse } + | { type: "incompatible", result: TalerExchangeApi.ExchangeVersionResponse, supported: string } + | { type: "error", error: TalerError } + +export const ExchangeApiProvider = ({ + baseUrl, + children, + frameOnError, +}: { + baseUrl: string, + children: ComponentChildren; + frameOnError: FunctionComponent<{ children: ComponentChildren }>, +}): VNode => { + const [checked, setChecked] = useState<ConfigResult>() + const { i18n } = useTranslationContext(); + const url = new URL(baseUrl) + const api = new TalerExchangeHttpClient(url.href, new BrowserHttpLib()) + useEffect(() => { + api.getConfig() + .then((resp) => { + if (api.isCompatible(resp.body.version)) { + setChecked({ type: "ok", config: resp.body }); + } else { + setChecked({ type: "incompatible", result: resp.body, supported: api.PROTOCOL_VERSION }) + } + }) + .catch((error: unknown) => { + if (error instanceof TalerError) { + setChecked({ type: "error", error }); + } + }); + }, []); + + if (checked === undefined) { + return h(frameOnError, { children: h("div", {}, "loading...") }) + } + if (checked.type === "error") { + return h(frameOnError, { children: h(ErrorLoading, { error: checked.error, showDetail: true }) }) + } + if (checked.type === "incompatible") { + return h(frameOnError, { children: h("div", {}, i18n.str`the bank backend is not supported. supported version "${checked.supported}", server version "${checked.result.version}"`) }) + } + const value: Type = { + url, config: checked.config, api + } + return h(Context.Provider, { + value, + children, + }); +}; + diff --git a/packages/aml-backoffice-ui/src/handlers/Caption.tsx b/packages/aml-backoffice-ui/src/handlers/Caption.tsx index fbf154d89..8facddec3 100644 --- a/packages/aml-backoffice-ui/src/handlers/Caption.tsx +++ b/packages/aml-backoffice-ui/src/handlers/Caption.tsx @@ -1,11 +1,8 @@ +import { TranslatedString } from "@gnu-taler/taler-util"; import { VNode, h } from "preact"; import { - IconAddon, - InputLine, - LabelWithTooltipMaybeRequired, - UIFormProps, + LabelWithTooltipMaybeRequired } from "./InputLine.js"; -import { TranslatedString } from "@gnu-taler/taler-util"; interface Props { label: TranslatedString; diff --git a/packages/aml-backoffice-ui/src/hooks/useBackend.ts b/packages/aml-backoffice-ui/src/hooks/useBackend.ts index b9d66fca6..95277a915 100644 --- a/packages/aml-backoffice-ui/src/hooks/useBackend.ts +++ b/packages/aml-backoffice-ui/src/hooks/useBackend.ts @@ -1,3 +1,4 @@ +import { canonicalizeBaseUrl } from "@gnu-taler/taler-util"; import { HttpResponseOk, RequestOptions, @@ -5,9 +6,6 @@ import { } 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: <T>( @@ -35,7 +33,7 @@ export function usePublicBackend(): useBackendType { ); const fetcher = useCallback( - function fetcherImpl<T>([endpoint, talerAmlOfficerSignature]: [string,string]): Promise<HttpResponseOk<T>> { + function fetcherImpl<T>([endpoint, talerAmlOfficerSignature]: [string, string]): Promise<HttpResponseOk<T>> { return requestHandler<T>(baseUrl, endpoint, { talerAmlOfficerSignature }); @@ -66,18 +64,29 @@ export function usePublicBackend(): useBackendType { export function getInitialBackendBaseURL(): string { const overrideUrl = typeof localStorage !== "undefined" - ? localStorage.getItem("exchange-aml-base-url") + ? localStorage.getItem("exchange-base-url") : undefined; + + let result: string; + 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); + result = window.origin + } else { + result = uiSettings.backendBaseURL; } - return canonicalizeBaseUrl(uiSettings.backendBaseURL); + } else { + // testing/development path + result = overrideUrl + } + try { + return canonicalizeBaseUrl(result) + } catch (e) { + //fall back + return canonicalizeBaseUrl(window.origin) } - // testing/development path - return canonicalizeBaseUrl(overrideUrl); } diff --git a/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts b/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts index 980a35f21..9db1e2aec 100644 --- a/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts +++ b/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts @@ -1,34 +1,29 @@ import { HttpResponse, - HttpResponseOk, - RequestError + HttpResponseOk } from "@gnu-taler/web-util/browser"; import { AmlExchangeBackend } from "../types.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import { AmountString, OfficerAccount, PaytoString, TalerExchangeApi, TalerExchangeResultByMethod, TalerHttpError } from "@gnu-taler/taler-util"; import _useSWR, { SWRHook, useSWRConfig } from "swr"; -import { AccountId } from "../account.js"; +import { useExchangeApiContext } from "../context/config.js"; import { usePublicBackend } from "./useBackend.js"; +import { useOfficer } from "./useOfficer.js"; const useSWR = _useSWR as unknown as SWRHook; -export function useCaseDetails( - account: AccountId, - paytoHash: string, - signature: string | undefined, -): HttpResponse< - AmlExchangeBackend.AmlDecisionDetails, - AmlExchangeBackend.AmlError -> { - const { fetcher } = usePublicBackend(); +export function useCaseDetails(paytoHash: string) { + const officer = useOfficer(); + const session = officer.state === "ready" ? officer.account : undefined; - const { data, error } = useSWR< - HttpResponseOk<AmlExchangeBackend.AmlDecisionDetails>, - RequestError<AmlExchangeBackend.AmlError> ->( [ - `aml/${account}/decision/${(paytoHash)}`, - signature, -], -fetcher, { + const { api } = useExchangeApiContext(); + + async function fetcher([officer, account]: [OfficerAccount, PaytoString]) { + return await api.getDecisionDetails(officer, account) + } + + const { data, error } = useSWR<TalerExchangeResultByMethod<"getDecisionDetails">, TalerHttpError>( + !session ? undefined : [session, paytoHash], fetcher, { refreshInterval: 0, refreshWhenHidden: false, revalidateOnFocus: false, @@ -41,11 +36,11 @@ fetcher, { }); if (data) return data; - if (error) return error.cause; - return { loading: true }; + if (error) return error; + return undefined; } -const example1: AmlExchangeBackend.AmlDecisionDetails = { +const example1: TalerExchangeApi.AmlDecisionDetails = { aml_history: [ { justification: "Lack of documentation", @@ -54,7 +49,7 @@ const example1: AmlExchangeBackend.AmlDecisionDetails = { t_s: Date.now() / 1000, }, new_state: 2, - new_threshold: "USD:0", + new_threshold: "USD:0" as AmountString, }, { justification: "Doing a transfer of high amount", @@ -63,7 +58,7 @@ const example1: AmlExchangeBackend.AmlDecisionDetails = { t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 6, }, new_state: 1, - new_threshold: "USD:2000", + new_threshold: "USD:2000" as AmountString, }, { justification: "Account is known to the system", @@ -72,7 +67,7 @@ const example1: AmlExchangeBackend.AmlDecisionDetails = { t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 9, }, new_state: 0, - new_threshold: "USD:100", + new_threshold: "USD:100" as AmountString, }, ], kyc_attributes: [ @@ -103,60 +98,4 @@ const example1: AmlExchangeBackend.AmlDecisionDetails = { ], }; -export const exampleResponse: HttpResponse<AmlExchangeBackend.AmlDecisionDetails,AmlExchangeBackend.AmlError> = { - ok: true, - data: example1, -} - - -export function useAmlCasesAPI(): AmlCaseAPI { - const { request } = usePublicBackend(); - const mutateAll = useMatchMutate(); - - const updateDecision = async ( - officer: AccountId, - data: AmlExchangeBackend.AmlDecision, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`aml/${officer}/decision`, { - method: "POST", - data, - contentType: "json", - }); - await mutateAll(/.*aml.*/); - return res; - }; - - return { - updateDecision, - }; -} - -export interface AmlCaseAPI { - updateDecision: ( - officer: AccountId, - data: AmlExchangeBackend.AmlDecision, - ) => Promise<HttpResponseOk<void>>; -} - -function useMatchMutate(): ( - re: RegExp, - value?: unknown, -) => Promise<any> { - const { cache, mutate } = useSWRConfig(); - - if (!(cache instanceof Map)) { - throw new Error( - "matchMutate requires the cache provider to be a Map instance", - ); - } - - return function matchRegexMutate(re: RegExp, value?: unknown) { - const allKeys = Array.from(cache.keys()); - const keys = allKeys.filter((key) => re.test(key)); - const mutations = keys.map((key) => { - return mutate(key, value, true); - }); - return Promise.all(mutations); - }; -} diff --git a/packages/aml-backoffice-ui/src/hooks/useCases.ts b/packages/aml-backoffice-ui/src/hooks/useCases.ts index c07bd5f18..2a133f46d 100644 --- a/packages/aml-backoffice-ui/src/hooks/useCases.ts +++ b/packages/aml-backoffice-ui/src/hooks/useCases.ts @@ -1,16 +1,13 @@ -import { useEffect, useState } from "preact/hooks"; +import { useState } from "preact/hooks"; -import { AmlExchangeBackend } from "../types.js"; import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, + HttpResponsePaginated } from "@gnu-taler/web-util/browser"; +import { AmlExchangeBackend } from "../types.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import { AmountString, OfficerAccount, TalerExchangeApi, TalerExchangeResultByMethod, TalerHttpError } from "@gnu-taler/taler-util"; import _useSWR, { SWRHook } from "swr"; -import { usePublicBackend } from "./useBackend.js"; -import { AccountId, buildQuerySignature } from "../account.js"; +import { useExchangeApiContext } from "../context/config.js"; import { useOfficer } from "./useOfficer.js"; const useSWR = _useSWR as unknown as SWRHook; @@ -22,59 +19,49 @@ const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1; * @param args * @returns */ -export function useCases( - account: AccountId, - state: AmlExchangeBackend.AmlState, - signature: string | undefined, -): HttpResponsePaginated< - AmlExchangeBackend.AmlRecords, - AmlExchangeBackend.AmlError -> { - const { paginatedFetcher } = usePublicBackend(); +export function useCases(state: AmlExchangeBackend.AmlState) { + const officer = useOfficer(); + const session = officer.state === "ready" ? officer.account : undefined; + const { api } = useExchangeApiContext(); - const [page, setPage] = useState(1); + const [offset, setOffet] = useState<string>(); + + async function fetcher([officer, state, offset]: [OfficerAccount, AmlExchangeBackend.AmlState, string | undefined]) { + return await api.getDecisionsByState(officer, state, { + order: "asc", offset, limit: MAX_RESULT_SIZE + }) + } - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<AmlExchangeBackend.AmlRecords>, - RequestError<AmlExchangeBackend.AmlError> - >( - [ - `aml/${account}/decisions/${AmlExchangeBackend.AmlState[state]}`, - page, - PAGE_SIZE, - signature, - ], - paginatedFetcher, + const { data, error } = useSWR<TalerExchangeResultByMethod<"getDecisionsByState">, TalerHttpError>( + !session ? undefined : [session, state, offset], + fetcher, ); - const [lastAfter, setLastAfter] = useState< - HttpResponse<AmlExchangeBackend.AmlRecords, AmlExchangeBackend.AmlError> - >({ loading: true }); + // const [lastAfter, setLastAfter] = useState< + // HttpResponse<AmlExchangeBackend.AmlRecords, AmlExchangeBackend.AmlError> + // >({ loading: true }); - useEffect(() => { - if (afterData) setLastAfter(afterData); - }, [afterData]); + // useEffect(() => { + // if (afterData) setLastAfter(afterData); + // }, [afterData]); - if (afterError) { - return afterError.cause; - } + // 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 isLastPage = + data && data.type === "ok" && data.body.records.length < PAGE_SIZE; + const isFirstPage = !offset; const pagination = { - isReachingEnd, - isReachingStart, + isLastPage, + isFirstPage, loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data && afterData.data.records.length < MAX_RESULT_SIZE) { - setPage(page + 1); + if (isLastPage || data?.type !== "ok") return; + const list = data.body.records + if (list.length < MAX_RESULT_SIZE) { + // setOffset(list[list.length-1].account_name); } }, loadMorePrev: () => { @@ -82,65 +69,62 @@ export function useCases( }, }; - const records = !afterData - ? [] - : ((afterData ?? lastAfter).data ?? { records: [] }).records; - if (loadingAfter) return { loading: true, data: { records } }; - if (afterData) { - return { ok: true, data: { records }, ...pagination }; + // const public_accountslist = data?.type !== "ok" ? [] : data.body.public_accounts; + if (data) { + if (data.type === "fail") { + return { data } + } + return { data, pagination } + } + if (error) { + return error; } - return { loading: true }; + return undefined; } -const example1: AmlExchangeBackend.AmlRecords = { +const example1: TalerExchangeApi.AmlRecords = { records: [ { current_state: 0, h_payto: "QWEQWEQWEQWEWQE", rowid: 1, - threshold: "USD 100", + threshold: "USD 100" as AmountString, }, { current_state: 1, h_payto: "ASDASDASD", rowid: 1, - threshold: "USD 100", + threshold: "USD 100" as AmountString, }, { current_state: 2, h_payto: "ZXCZXCZXCXZC", rowid: 1, - threshold: "USD 1000", + threshold: "USD 1000" as AmountString, }, { current_state: 0, h_payto: "QWEQWEQWEQWEWQE", rowid: 1, - threshold: "USD 100", + threshold: "USD 100" as AmountString, }, { current_state: 1, h_payto: "ASDASDASD", rowid: 1, - threshold: "USD 100", + threshold: "USD 100" as AmountString, }, { current_state: 2, h_payto: "ZXCZXCZXCXZC", rowid: 1, - threshold: "USD 1000", + threshold: "USD 1000" as AmountString, }, ].map((e, idx) => { e.rowid = idx; - e.threshold = `${e.threshold}${idx}`; + e.threshold = `${e.threshold}${idx}` as AmountString; return e; }), }; -export const exampleResponse: HttpResponsePaginated<AmlExchangeBackend.AmlRecords,AmlExchangeBackend.AmlError> = { - ok: true, - data: example1, - loadMore: () => {}, - loadMorePrev: () => {}, -} diff --git a/packages/aml-backoffice-ui/src/hooks/useOfficer.ts b/packages/aml-backoffice-ui/src/hooks/useOfficer.ts index 4ec43569b..0747170e8 100644 --- a/packages/aml-backoffice-ui/src/hooks/useOfficer.ts +++ b/packages/aml-backoffice-ui/src/hooks/useOfficer.ts @@ -1,17 +1,15 @@ import { AbsoluteTime, Codec, + LockedAccount, + OfficerAccount, buildCodecForObject, codecForAbsoluteTime, codecForString, + createNewOfficerAccount, + unlockOfficerAccount, } from "@gnu-taler/taler-util"; import { - Account, - LockedAccount, - createNewAccount, - unlockAccount, -} from "../account.js"; -import { buildStorageKey, useLocalStorage, useMemoryStorage, @@ -43,7 +41,7 @@ interface OfficerLocked { } interface OfficerReady { state: "ready"; - account: Account; + account: OfficerAccount; forget: () => void; lock: () => void; } @@ -52,7 +50,7 @@ const OFFICER_KEY = buildStorageKey("officer", codecForOfficer()); const ACCOUNT_KEY = "account"; export function useOfficer(): OfficerState { - const accountStorage = useMemoryStorage<Account>(ACCOUNT_KEY); + const accountStorage = useMemoryStorage<OfficerAccount>(ACCOUNT_KEY); const officerStorage = useLocalStorage(OFFICER_KEY); const officer = officerStorage.value; @@ -62,13 +60,13 @@ export function useOfficer(): OfficerState { return { state: "not-found", create: async (pwd: string) => { - const { accountId, safe, signingKey } = await createNewAccount(pwd); + const { id, safe, signingKey } = await createNewOfficerAccount(pwd); officerStorage.update({ account: safe, when: AbsoluteTime.now(), }); - accountStorage.update({ accountId, signingKey }); + accountStorage.update({ id, signingKey }); }, }; } @@ -80,7 +78,7 @@ export function useOfficer(): OfficerState { officerStorage.reset(); }, tryUnlock: async (pwd: string) => { - const ac = await unlockAccount(officer.account, pwd); + const ac = await unlockOfficerAccount(officer.account, pwd); accountStorage.update(ac); }, }; diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx index ce820d612..f618a3592 100644 --- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx +++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -1,24 +1,23 @@ -import { Fragment, VNode, h } from "preact"; import { AbsoluteTime, AmountJson, Amounts, + PaytoString, + TalerError, TranslatedString, + assertUnreachable, } from "@gnu-taler/taler-util"; -import { format } from "date-fns"; +import { ErrorLoading, Loading, useTranslationContext } from "@gnu-taler/web-util/browser"; import { ArrowDownCircleIcon, ClockIcon } from "@heroicons/react/20/solid"; +import { format } from "date-fns"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { NiceForm } from "../NiceForm.js"; import { FlexibleForm } from "../forms/index.js"; import { UIFormField } from "../handlers/forms.js"; +import { useCaseDetails } from "../hooks/useCaseDetails.js"; import { Pages } from "../pages.js"; import { AmlExchangeBackend } from "../types.js"; -import { HandleAccountNotReady } from "./HandleAccountNotReady.js"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { useOfficer } from "../hooks/useOfficer.js"; -import { buildQuerySignature } from "../account.js"; -import { useCaseDetails } from "../hooks/useCaseDetails.js"; -import { handleNotOkResult } from "../utils/errors.js"; type AmlEvent = AmlFormEvent | KycCollectionEvent | KycExpirationEvent; type AmlFormEvent = { @@ -85,30 +84,33 @@ function getEventsFromAmlHistory( return ae.concat(ke).sort(selectSooner); } -export function CaseDetails({ account: paytoHash }: { account: string }) { +export function CaseDetails({ account }: { account: string }) { const [selected, setSelected] = useState<AmlEvent | undefined>(undefined); - const officer = useOfficer(); const { i18n } = useTranslationContext(); - if (officer.state !== "ready") { - return <HandleAccountNotReady officer={officer} />; + const details = useCaseDetails(account) + if (!details) { + return <Loading /> } - const signature = - officer.state === "ready" - ? buildQuerySignature(officer.account.signingKey) - : undefined; - const details = useCaseDetails(officer.account.accountId, paytoHash, signature) - if (!details.ok && !details.loading) { - return handleNotOkResult(i18n)(details); + if (details instanceof TalerError) { + return <ErrorLoading error={details} /> } - const aml_history = details.loading ? [] : details.data.aml_history - const kyc_attributes = details.loading ? [] : details.data.kyc_attributes - const events = getEventsFromAmlHistory(aml_history,kyc_attributes); - + if (details.type === "fail") { + switch (details.case) { + case "unauthorized": + case "officer-not-found": + case "officer-disabled": return <div /> + default: assertUnreachable(details) + } + } + const { aml_history, kyc_attributes } = details.body + + const events = getEventsFromAmlHistory(aml_history, kyc_attributes); + return ( <div> <a - href={Pages.newFormEntry.url({ account: paytoHash })} + href={Pages.newFormEntry.url({ account })} class="m-4 block rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700" > New AML form @@ -287,23 +289,22 @@ function ShowConsolidated({ }, Object.entries(cons.kyc).length > 0 ? { - title: "KYC" as TranslatedString, - fields: Object.entries(cons.kyc).map(([key, field]) => { - const result: UIFormField = { - type: "text", - props: { - label: key as TranslatedString, - name: `kyc.${key}.value`, - help: `${field.provider} since ${ - field.since.t_ms === "never" - ? "never" - : format(field.since.t_ms, "dd/MM/yyyy") + title: "KYC" as TranslatedString, + fields: Object.entries(cons.kyc).map(([key, field]) => { + const result: UIFormField = { + type: "text", + props: { + label: key as TranslatedString, + name: `kyc.${key}.value`, + help: `${field.provider} since ${field.since.t_ms === "never" + ? "never" + : format(field.since.t_ms, "dd/MM/yyyy") }` as TranslatedString, - }, - }; - return result; - }), - } + }, + }; + return result; + }), + } : undefined, ], }; @@ -319,7 +320,7 @@ function ShowConsolidated({ key={`${String(Date.now())}`} form={form} initial={cons} - onUpdate={() => {}} + onUpdate={() => { }} /> </Fragment> ); diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx index 990c0d2d4..5f79db71e 100644 --- a/packages/aml-backoffice-ui/src/pages/Cases.tsx +++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx @@ -1,4 +1,5 @@ -import { TranslatedString } from "@gnu-taler/taler-util"; +import { TalerError, TranslatedString, assertUnreachable } from "@gnu-taler/taler-util"; +import { ErrorLoading, Loading, useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { createNewForm } from "../handlers/forms.js"; @@ -7,34 +8,37 @@ 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"; export function Cases() { - const officer = useOfficer(); const { i18n } = useTranslationContext(); - if (officer.state !== "ready") { - return <HandleAccountNotReady officer={officer} />; - } - const form = createNewForm<{ - state: AmlExchangeBackend.AmlState; - }>(); - const signature = - officer.state === "ready" - ? buildQuerySignature(officer.account.signingKey) - : undefined; + const form = createNewForm<{ state: AmlExchangeBackend.AmlState }>(); + 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); + const list = useCases(stateFilter); + + if (!list) { + return <Loading /> + } + + if (list instanceof TalerError) { + return <ErrorLoading error={list} /> } - const records = list.loading ? [] : list.data.records + + if (list.data.type === "fail") { + switch (list.data.case) { + case "unauthorized": + case "officer-not-found": + case "officer-disabled": return <div /> + default: assertUnreachable(list.data) + } + } + + const { records } = list.data.body + return ( <div> <div class="px-4 sm:px-6 lg:px-8"> @@ -52,7 +56,7 @@ export function Cases() { onUpdate={(v) => { setStateFilter(v.state ?? initial); }} - onSubmit={(v) => {}} + onSubmit={(v) => { }} > <form.InputChoiceHorizontal name="state" diff --git a/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx b/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx index 429cfb9ca..fa79bb476 100644 --- a/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx +++ b/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx @@ -2,12 +2,11 @@ import { VNode, h } from "preact"; import { allForms } from "./AntiMoneyLaunderingForm.js"; import { Pages } from "../pages.js"; import { NiceForm } from "../NiceForm.js"; -import { AbsoluteTime, Amounts, TalerProtocolTimestamp } from "@gnu-taler/taler-util"; +import { AbsoluteTime, Amounts, TalerExchangeApi, TalerProtocolTimestamp } from "@gnu-taler/taler-util"; import { AmlExchangeBackend } from "../types.js"; -import { useAmlCasesAPI } from "../hooks/useCaseDetails.js"; import { useOfficer } from "../hooks/useOfficer.js"; import { HandleAccountNotReady } from "./HandleAccountNotReady.js"; -import { buildDecisionSignature, buildQuerySignature } from "../account.js"; +import { useExchangeApiContext } from "../context/config.js"; export function NewFormEntry({ account, @@ -40,21 +39,21 @@ export function NewFormEntry({ state: AmlExchangeBackend.AmlState.pending, threshold: Amounts.parseOrThrow("KUDOS:1000"), }; - const api = useAmlCasesAPI() - + const { api } = useExchangeApiContext() + return ( <NiceForm initial={initial} form={showingFrom(initial)} onSubmit={(formValue) => { if (formValue.state === undefined || formValue.threshold === undefined) return; - + const justification = { index: selectedForm, name: formName, value: formValue } - const decision: AmlExchangeBackend.AmlDecision = { + const decision: TalerExchangeApi.AmlDecision = { justification: JSON.stringify(justification), decision_time: TalerProtocolTimestamp.now(), h_payto: account, @@ -63,9 +62,9 @@ export function NewFormEntry({ officer_sig: "", kyc_requirements: undefined } - const signature = buildDecisionSignature(officer.account.signingKey, decision); - decision.officer_sig = signature - api.updateDecision(officer.account.accountId, decision); + // const signature = buildDecisionSignature(officer.account.signingKey, decision); + // decision.officer_sig = signature + api.addDecisionDetails(officer.account, decision); // alert(JSON.stringify(formValue)); }} diff --git a/packages/aml-backoffice-ui/src/pages/Officer.tsx b/packages/aml-backoffice-ui/src/pages/Officer.tsx index 5320369e4..4af34805a 100644 --- a/packages/aml-backoffice-ui/src/pages/Officer.tsx +++ b/packages/aml-backoffice-ui/src/pages/Officer.tsx @@ -14,12 +14,12 @@ export function Officer() { Public key </h1> <div class="max-w-xl text-base leading-7 text-gray-700 lg:max-w-lg"> - <p class="mt-6 font-mono break-all">{officer.account.accountId}</p> + <p class="mt-6 font-mono break-all">{officer.account.id}</p> </div> <p> <a href={`mailto:aml@exchange.taler.net?body=${encodeURIComponent( - `I want my AML account\n\n\nPubKey: ${officer.account.accountId}`, + `I want my AML account\n\n\nPubKey: ${officer.account.id}`, )}`} target="_blank" rel="noreferrer" diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx index 39f8addd3..83d8767fb 100644 --- a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx +++ b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx @@ -1,7 +1,6 @@ -import { TranslatedString } from "@gnu-taler/taler-util"; +import { TranslatedString, UnwrapKeyError } from "@gnu-taler/taler-util"; import { notifyError, notifyInfo } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; -import { UnwrapKeyError } from "../account.js"; import { createNewForm } from "../handlers/forms.js"; export function UnlockAccount({ diff --git a/packages/aml-backoffice-ui/src/utils/errors.tsx b/packages/aml-backoffice-ui/src/utils/errors.tsx deleted file mode 100644 index b67d61a5f..000000000 --- a/packages/aml-backoffice-ui/src/utils/errors.tsx +++ /dev/null @@ -1,77 +0,0 @@ -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<Error extends AmlExchangeBackend.AmlError>( - i18n: ReturnType<typeof useTranslationContext>["i18n"], -): <T>( - result: HttpResponsePaginated<T, Error> | HttpResponse<T, Error>, -) => VNode { - return function handleNotOkResult2<T>( - result: HttpResponsePaginated<T, Error> | HttpResponse<T, Error>, - ): VNode { - if (result.loading) return <Loading />; - 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 <div> not authorized</div>; - } - 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 <div>error</div>; - } - return <div />; - }; -} -export function assertUnreachable(x: never): never { - throw new Error("Didn't expect to get here"); -} |