/* 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 { canonicalizeBaseUrl } from "@gnu-taler/taler-util"; import { RequestError, useLocalStorage, } from "@gnu-taler/web-util/lib/index.browser"; import { HttpResponse, HttpResponseOk, RequestOptions, } from "@gnu-taler/web-util/lib/index.browser"; import { useApiContext } from "@gnu-taler/web-util/lib/index.browser"; import { useCallback, useEffect, useState } from "preact/hooks"; import { useSWRConfig } from "swr"; import { useBackendContext } from "../context/backend.js"; import { bankUiSettings } from "../settings.js"; /** * Has the information to reach and * authenticate at the bank's backend. */ export type BackendState = LoggedIn | LoggedOut; export interface BackendCredentials { username: string; password: string; } interface LoggedIn extends BackendCredentials { url: string; status: "loggedIn"; isUserAdministrator: boolean; } interface LoggedOut { url: string; status: "loggedOut"; } const maybeRootPath = bankUiSettings.backendBaseURL; export function getInitialBackendBaseURL(): string { const overrideUrl = localStorage.getItem("bank-base-url"); return canonicalizeBaseUrl(overrideUrl ? overrideUrl : maybeRootPath); } export const defaultState: BackendState = { status: "loggedOut", url: getInitialBackendBaseURL(), }; export interface BackendStateHandler { state: BackendState; logOut(): void; logIn(info: BackendCredentials): void; } /** * Return getters and setters for * login credentials and backend's * base URL. */ export function useBackendState(): BackendStateHandler { const [value, update] = useLocalStorage( "backend-state", JSON.stringify(defaultState), ); let parsed; try { parsed = JSON.parse(value!); } catch { parsed = undefined; } const state: BackendState = !parsed?.status ? defaultState : parsed; return { state, logOut() { update(JSON.stringify({ ...defaultState, url: state.url })); }, logIn(info) { //admin is defined by the username const nextState: BackendState = { status: "loggedIn", url: state.url, ...info, isUserAdministrator: info.username === "admin", }; update(JSON.stringify(nextState)); }, }; } interface useBackendType { request: ( path: string, options?: RequestOptions, ) => Promise>; fetcher: (endpoint: string) => Promise>; multiFetcher: (endpoint: string[][]) => Promise[]>; paginatedFetcher: ( args: [string, number, number], ) => Promise>; sandboxAccountsFetcher: ( args: [string, number, number, string], ) => Promise>; sandboxCashoutFetcher: (endpoint: string[]) => Promise>; } export function usePublicBackend(): useBackendType { const { state } = useBackendContext(); const { request: requestHandler } = useApiContext(); const baseUrl = state.url; 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]: [ string, number, number, ]): Promise> { return requestHandler(baseUrl, endpoint, { params: { page: page || 1, size }, }); }, [baseUrl], ); const multiFetcher = useCallback( function multiFetcherImpl([endpoints]: string[][]): Promise< HttpResponseOk[] > { return Promise.all( endpoints.map((endpoint) => requestHandler(baseUrl, endpoint)), ); }, [baseUrl], ); const sandboxAccountsFetcher = useCallback( function fetcherImpl([endpoint, page, size, account]: [ string, number, number, string, ]): Promise> { return requestHandler(baseUrl, endpoint, { params: { page: page || 1, size }, }); }, [baseUrl], ); const sandboxCashoutFetcher = useCallback( function fetcherImpl([endpoint, account]: string[]): Promise< HttpResponseOk > { return requestHandler(baseUrl, endpoint); }, [baseUrl], ); return { request, fetcher, paginatedFetcher, multiFetcher, sandboxAccountsFetcher, sandboxCashoutFetcher, }; } export function useAuthenticatedBackend(): useBackendType { const { state } = useBackendContext(); const { request: requestHandler } = useApiContext(); const creds = state.status === "loggedIn" ? state : undefined; const baseUrl = state.url; const request = useCallback( function requestImpl( path: string, options: RequestOptions = {}, ): Promise> { return requestHandler(baseUrl, path, { basicAuth: creds, ...options }); }, [baseUrl, creds], ); const fetcher = useCallback( function fetcherImpl(endpoint: string): Promise> { return requestHandler(baseUrl, endpoint, { basicAuth: creds }); }, [baseUrl, creds], ); const paginatedFetcher = useCallback( function fetcherImpl([endpoint, page = 0, size]: [ string, number, number, ]): Promise> { return requestHandler(baseUrl, endpoint, { basicAuth: creds, params: { page, size }, }); }, [baseUrl, creds], ); const multiFetcher = useCallback( function multiFetcherImpl([endpoints]: string[][]): Promise< HttpResponseOk[] > { return Promise.all( endpoints.map((endpoint) => requestHandler(baseUrl, endpoint, { basicAuth: creds }), ), ); }, [baseUrl, creds], ); const sandboxAccountsFetcher = useCallback( function fetcherImpl([endpoint, page, size, account]: [ string, number, number, string, ]): Promise> { return requestHandler(baseUrl, endpoint, { basicAuth: creds, params: { page: page || 1, size }, }); }, [baseUrl], ); const sandboxCashoutFetcher = useCallback( function fetcherImpl([endpoint, account]: string[]): Promise< HttpResponseOk > { return requestHandler(baseUrl, endpoint, { basicAuth: creds, params: { account }, }); }, [baseUrl, creds], ); return { request, fetcher, paginatedFetcher, multiFetcher, sandboxAccountsFetcher, sandboxCashoutFetcher, }; } export function useBackendConfig(): HttpResponse< SandboxBackend.Config, SandboxBackend.SandboxError > { const { request } = usePublicBackend(); type Type = SandboxBackend.Config; const [result, setResult] = useState< HttpResponse >({ loading: true }); useEffect(() => { request(`/config`) .then((data) => setResult(data)) .catch((error) => setResult(error)); }, [request]); return result; } 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, 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); }; }