/* 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 */ /** * * @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 { 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 >({ loading: true }); useEffect(() => { request(`/management/instances`) .then((data) => setResult(data)) .catch((error: RequestError) => 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 > { const { request } = useBackendBaseRequest(); type Type = MerchantBackend.VersionResponse; type State = { data: HttpResponse>, timer: number } const [result, setResult] = useState({ data: { loading: true }, timer: 0 }); useEffect(() => { if (result.timer) { clearTimeout(result.timer) } function tryConfig(): void { request(`/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: ( endpoint: string, options?: RequestOptions, ) => Promise>; fetcher: (endpoint: string) => Promise>; multiFetcher: (params: [url: string[]]) => Promise[]>; orderFetcher: ( params: [endpoint: string, paid?: YesOrNo, refunded?: YesOrNo, wired?: YesOrNo, searchDate?: Date, delta?: number,] ) => Promise>; transferFetcher: ( params: [endpoint: string, payto_uri?: string, verified?: string, position?: string, delta?: number,] ) => Promise>; templateFetcher: ( params: [endpoint: string, position?: string, delta?: number] ) => Promise>; webhookFetcher: ( params: [endpoint: string, position?: string, delta?: number] ) => Promise>; } interface useBackendBaseRequestType { request: ( endpoint: string, options?: RequestOptions, ) => Promise>; } 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 { const data: MerchantBackend.Instances.LoginTokenRequest = { scope: "write", duration: { d_us: "forever" }, refreshable: true, } try { const response = await request(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 { 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( endpoint: string, options: RequestOptions = {}, ): Promise> { return requestHandler(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, admin } = useInstanceContext(); const { request: requestHandler } = useApiContext(); const { baseUrl, token: loginToken } = !admin ? { baseUrl: rootBackendUrl, token: rootToken } : { baseUrl: rootBackendUrl, token: instanceToken }; const token = loginToken?.token; const request = useCallback( function requestImpl( endpoint: string, options: RequestOptions = {}, ): Promise> { return requestHandler(baseUrl, endpoint, { token, ...options }); }, [baseUrl, token], ); const multiFetcher = useCallback( function multiFetcherImpl( args: [endpoints: string[]], ): Promise[]> { const [endpoints] = args return Promise.all( endpoints.map((endpoint) => requestHandler(baseUrl, endpoint, { token }), ), ); }, [baseUrl, token], ); const fetcher = useCallback( function fetcherImpl(endpoint: string): Promise> { return requestHandler(baseUrl, endpoint, { token }); }, [baseUrl, token], ); const orderFetcher = useCallback( function orderFetcherImpl( args: [endpoint: string, paid?: YesOrNo, refunded?: YesOrNo, wired?: YesOrNo, searchDate?: Date, delta?: number,] ): Promise> { 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(baseUrl, endpoint, { params, token }); }, [baseUrl, token], ); const transferFetcher = useCallback( function transferFetcherImpl( args: [endpoint: string, payto_uri?: string, verified?: string, position?: string, delta?: number,] ): Promise> { 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(baseUrl, endpoint, { params, token }); }, [baseUrl, token], ); const templateFetcher = useCallback( function templateFetcherImpl( args: [endpoint: string, position?: string, delta?: number,] ): Promise> { 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(baseUrl, endpoint, { params, token }); }, [baseUrl, token], ); const webhookFetcher = useCallback( function webhookFetcherImpl( args: [endpoint: string, position?: string, delta?: number,] ): Promise> { 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(baseUrl, endpoint, { params, token }); }, [baseUrl, token], ); return { request, fetcher, multiFetcher, orderFetcher, transferFetcher, templateFetcher, webhookFetcher, }; }