summaryrefslogtreecommitdiff
path: root/packages/auditor-backoffice-ui/src/hooks/backend.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/auditor-backoffice-ui/src/hooks/backend.ts')
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/backend.ts477
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,
+ };
+}