diff options
Diffstat (limited to 'packages/auditor-backoffice-ui/src/hooks/backend.ts')
-rw-r--r-- | packages/auditor-backoffice-ui/src/hooks/backend.ts | 477 |
1 files changed, 477 insertions, 0 deletions
diff --git a/packages/auditor-backoffice-ui/src/hooks/backend.ts b/packages/auditor-backoffice-ui/src/hooks/backend.ts new file mode 100644 index 000000000..8d99546a8 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/backend.ts @@ -0,0 +1,477 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util"; +import { + ErrorType, + HttpError, + HttpResponse, + HttpResponseOk, + RequestError, + RequestOptions, + useApiContext, +} from "@gnu-taler/web-util/browser"; +import { useCallback, useEffect, useState } from "preact/hooks"; +import { useSWRConfig } from "swr"; +import { useBackendContext } from "../context/backend.js"; +import { useInstanceContext } from "../context/instance.js"; +import { AccessToken, LoginToken, MerchantBackend, Timestamp } from "../declaration.js"; + + +export 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) { + return mutate((key) => { + // evict if no key or regex === all + if (!key || !re) return true + // match string + if (typeof key === 'string' && re.test(key)) return true + // record or object have the path at [0] + if (typeof key === 'object' && re.test(key[0])) return true + //key didn't match regex + return false + }, undefined, { + revalidate: true, + }); + }; +} + +export function useBackendInstancesTestForAdmin(): HttpResponse< + MerchantBackend.Instances.InstancesResponse, + MerchantBackend.ErrorDetail +> { + const { request } = useBackendBaseRequest(); + + type Type = MerchantBackend.Instances.InstancesResponse; + + const [result, setResult] = useState< + HttpResponse<Type, MerchantBackend.ErrorDetail> + >({ loading: true }); + + useEffect(() => { + request<Type>(`/management/instances`) + .then((data) => setResult(data)) + .catch((error: RequestError<MerchantBackend.ErrorDetail>) => + setResult(error.cause), + ); + }, [request]); + + return result; +} + +const CHECK_CONFIG_INTERVAL_OK = 5 * 60 * 1000; +const CHECK_CONFIG_INTERVAL_FAIL = 2 * 1000; + +export function useBackendConfig(): HttpResponse< + MerchantBackend.VersionResponse | undefined, + RequestError<MerchantBackend.ErrorDetail> +> { + const { request } = useBackendBaseRequest(); + + type Type = MerchantBackend.VersionResponse; + type State = { data: HttpResponse<Type, RequestError<MerchantBackend.ErrorDetail>>, timer: number } + const [result, setResult] = useState<State>({ data: { loading: true }, timer: 0 }); + + useEffect(() => { + if (result.timer) { + clearTimeout(result.timer) + } + function tryConfig(): void { + request<Type>(`/config`) + .then((data) => { + const timer: any = setTimeout(() => { + tryConfig() + }, CHECK_CONFIG_INTERVAL_OK) + setResult({ data, timer }) + }) + .catch((error) => { + const timer: any = setTimeout(() => { + tryConfig() + }, CHECK_CONFIG_INTERVAL_FAIL) + const data = error.cause + setResult({ data, timer }) + }); + } + tryConfig() + }, [request]); + + return result.data; +} + +interface useBackendInstanceRequestType { + request: <T>( + endpoint: string, + options?: RequestOptions, + ) => Promise<HttpResponseOk<T>>; + fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; + reserveDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; + rewardsDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; + multiFetcher: <T>(params: [url: string[]]) => Promise<HttpResponseOk<T>[]>; + orderFetcher: <T>( + params: [endpoint: string, + paid?: YesOrNo, + refunded?: YesOrNo, + wired?: YesOrNo, + searchDate?: Date, + delta?: number,] + ) => Promise<HttpResponseOk<T>>; + transferFetcher: <T>( + params: [endpoint: string, + payto_uri?: string, + verified?: string, + position?: string, + delta?: number,] + ) => Promise<HttpResponseOk<T>>; + templateFetcher: <T>( + params: [endpoint: string, + position?: string, + delta?: number] + ) => Promise<HttpResponseOk<T>>; + webhookFetcher: <T>( + params: [endpoint: string, + position?: string, + delta?: number] + ) => Promise<HttpResponseOk<T>>; +} +interface useBackendBaseRequestType { + request: <T>( + endpoint: string, + options?: RequestOptions, + ) => Promise<HttpResponseOk<T>>; +} + +type YesOrNo = "yes" | "no"; +type LoginResult = { + valid: true; + token: string; + expiration: Timestamp; +} | { + valid: false; + cause: HttpError<{}>; +} + +export function useCredentialsChecker() { + const { request } = useApiContext(); + //check against instance details endpoint + //while merchant backend doesn't have a login endpoint + async function requestNewLoginToken( + baseUrl: string, + token: AccessToken, + ): Promise<LoginResult> { + const data: MerchantBackend.Instances.LoginTokenRequest = { + scope: "write", + duration: { + d_us: "forever" + }, + refreshable: true, + } + try { + const response = await request<MerchantBackend.Instances.LoginTokenSuccessResponse>(baseUrl, `/private/token`, { + method: "POST", + token, + data + }); + return { valid: true, token: response.data.token, expiration: response.data.expiration }; + } catch (error) { + if (error instanceof RequestError) { + return { valid: false, cause: error.cause }; + } + + return { + valid: false, cause: { + type: ErrorType.UNEXPECTED, + loading: false, + info: { + hasToken: true, + status: 0, + options: {}, + url: `/private/token`, + payload: {} + }, + exception: error, + message: (error instanceof Error ? error.message : "unpexepected error") + } + }; + } + }; + + async function refreshLoginToken( + baseUrl: string, + token: LoginToken + ): Promise<LoginResult> { + + if (AbsoluteTime.isExpired(AbsoluteTime.fromProtocolTimestamp(token.expiration))) { + return { + valid: false, cause: { + type: ErrorType.CLIENT, + status: HttpStatusCode.Unauthorized, + message: "login token expired, login again.", + info: { + hasToken: true, + status: 401, + options: {}, + url: `/private/token`, + payload: {} + }, + payload: {} + }, + } + } + + return requestNewLoginToken(baseUrl, token.token as AccessToken) + } + return { requestNewLoginToken, refreshLoginToken } +} + +/** + * + * @param root the request is intended to the base URL and no the instance URL + * @returns request handler to + */ +export function useBackendBaseRequest(): useBackendBaseRequestType { + const { url: backend, token: loginToken } = useBackendContext(); + const { request: requestHandler } = useApiContext(); + const token = loginToken?.token; + + const request = useCallback( + function requestImpl<T>( + endpoint: string, + options: RequestOptions = {}, + ): Promise<HttpResponseOk<T>> { + return requestHandler<T>(backend, endpoint, { ...options, token }).then(res => { + return res + }).catch(err => { + throw err + }); + }, + [backend, token], + ); + + return { request }; +} + +export function useBackendInstanceRequest(): useBackendInstanceRequestType { + const { url: rootBackendUrl, token: rootToken } = useBackendContext(); + const { token: instanceToken, id, admin } = useInstanceContext(); + const { request: requestHandler } = useApiContext(); + + const { baseUrl, token: loginToken } = !admin + ? { baseUrl: rootBackendUrl, token: rootToken } + : { baseUrl: `${rootBackendUrl}/instances/${id}`, token: instanceToken }; + + const token = loginToken?.token; + + const request = useCallback( + function requestImpl<T>( + endpoint: string, + options: RequestOptions = {}, + ): Promise<HttpResponseOk<T>> { + return requestHandler<T>(baseUrl, endpoint, { token, ...options }); + }, + [baseUrl, token], + ); + + const multiFetcher = useCallback( + function multiFetcherImpl<T>( + args: [endpoints: string[]], + ): Promise<HttpResponseOk<T>[]> { + const [endpoints] = args + return Promise.all( + endpoints.map((endpoint) => + requestHandler<T>(baseUrl, endpoint, { token }), + ), + ); + }, + [baseUrl, token], + ); + + const fetcher = useCallback( + function fetcherImpl<T>(endpoint: string): Promise<HttpResponseOk<T>> { + return requestHandler<T>(baseUrl, endpoint, { token }); + }, + [baseUrl, token], + ); + + const orderFetcher = useCallback( + function orderFetcherImpl<T>( + args: [endpoint: string, + paid?: YesOrNo, + refunded?: YesOrNo, + wired?: YesOrNo, + searchDate?: Date, + delta?: number,] + ): Promise<HttpResponseOk<T>> { + const [endpoint, paid, refunded, wired, searchDate, delta] = args + const date_s = + delta && delta < 0 && searchDate + ? Math.floor(searchDate.getTime() / 1000) + 1 + : searchDate !== undefined ? Math.floor(searchDate.getTime() / 1000) : undefined; + const params: any = {}; + if (paid !== undefined) params.paid = paid; + if (delta !== undefined) params.delta = delta; + if (refunded !== undefined) params.refunded = refunded; + if (wired !== undefined) params.wired = wired; + if (date_s !== undefined) params.date_s = date_s; + if (delta === 0) { + //in this case we can already assume the response + //and avoid network + return Promise.resolve({ + ok: true, + data: { orders: [] } as T, + }) + } + return requestHandler<T>(baseUrl, endpoint, { params, token }); + }, + [baseUrl, token], + ); + + const reserveDetailFetcher = useCallback( + function reserveDetailFetcherImpl<T>( + endpoint: string, + ): Promise<HttpResponseOk<T>> { + return requestHandler<T>(baseUrl, endpoint, { + params: { + rewards: "yes", + }, + token, + }); + }, + [baseUrl, token], + ); + + const rewardsDetailFetcher = useCallback( + function rewardsDetailFetcherImpl<T>( + endpoint: string, + ): Promise<HttpResponseOk<T>> { + return requestHandler<T>(baseUrl, endpoint, { + params: { + pickups: "yes", + }, + token, + }); + }, + [baseUrl, token], + ); + + const transferFetcher = useCallback( + function transferFetcherImpl<T>( + args: [endpoint: string, + payto_uri?: string, + verified?: string, + position?: string, + delta?: number,] + ): Promise<HttpResponseOk<T>> { + const [endpoint, payto_uri, verified, position, delta] = args + const params: any = {}; + if (payto_uri !== undefined) params.payto_uri = payto_uri; + if (verified !== undefined) params.verified = verified; + if (delta === 0) { + //in this case we can already assume the response + //and avoid network + return Promise.resolve({ + ok: true, + data: { transfers: [] } as T, + }) + } + if (delta !== undefined) { + params.limit = delta; + } + if (position !== undefined) params.offset = position; + + return requestHandler<T>(baseUrl, endpoint, { params, token }); + }, + [baseUrl, token], + ); + + const templateFetcher = useCallback( + function templateFetcherImpl<T>( + args: [endpoint: string, + position?: string, + delta?: number,] + ): Promise<HttpResponseOk<T>> { + const [endpoint, position, delta] = args + const params: any = {}; + if (delta === 0) { + //in this case we can already assume the response + //and avoid network + return Promise.resolve({ + ok: true, + data: { templates: [] } as T, + }) + } + if (delta !== undefined) { + params.limit = delta; + } + if (position !== undefined) params.offset = position; + + return requestHandler<T>(baseUrl, endpoint, { params, token }); + }, + [baseUrl, token], + ); + + const webhookFetcher = useCallback( + function webhookFetcherImpl<T>( + args: [endpoint: string, + position?: string, + delta?: number,] + ): Promise<HttpResponseOk<T>> { + const [endpoint, position, delta] = args + const params: any = {}; + if (delta === 0) { + //in this case we can already assume the response + //and avoid network + return Promise.resolve({ + ok: true, + data: { webhooks: [] } as T, + }) + } + if (delta !== undefined) { + params.limit = delta; + } + if (position !== undefined) params.offset = position; + + return requestHandler<T>(baseUrl, endpoint, { params, token }); + }, + [baseUrl, token], + ); + + return { + request, + fetcher, + multiFetcher, + orderFetcher, + reserveDetailFetcher, + rewardsDetailFetcher, + transferFetcher, + templateFetcher, + webhookFetcher, + }; +} |