2022-12-09 13:09:20 +01:00
|
|
|
/*
|
|
|
|
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/>
|
|
|
|
*/
|
|
|
|
|
2023-05-29 19:53:06 +02:00
|
|
|
import {
|
|
|
|
Codec,
|
|
|
|
buildCodecForObject,
|
|
|
|
buildCodecForUnion,
|
|
|
|
canonicalizeBaseUrl,
|
|
|
|
codecForBoolean,
|
|
|
|
codecForConstString,
|
|
|
|
codecForString,
|
|
|
|
} from "@gnu-taler/taler-util";
|
2023-02-10 13:51:37 +01:00
|
|
|
import {
|
2023-03-15 13:25:23 +01:00
|
|
|
ErrorType,
|
2023-04-07 22:30:01 +02:00
|
|
|
HttpError,
|
2023-02-10 13:51:37 +01:00
|
|
|
RequestError,
|
2023-05-29 19:53:06 +02:00
|
|
|
buildStorageKey,
|
2023-02-10 13:51:37 +01:00
|
|
|
useLocalStorage,
|
2023-05-05 13:36:48 +02:00
|
|
|
} from "@gnu-taler/web-util/browser";
|
2023-02-08 21:41:19 +01:00
|
|
|
import {
|
|
|
|
HttpResponse,
|
|
|
|
HttpResponseOk,
|
|
|
|
RequestOptions,
|
2023-05-05 13:36:48 +02:00
|
|
|
} from "@gnu-taler/web-util/browser";
|
|
|
|
import { useApiContext } from "@gnu-taler/web-util/browser";
|
2023-02-08 21:41:19 +01:00
|
|
|
import { useCallback, useEffect, useState } from "preact/hooks";
|
|
|
|
import { useSWRConfig } from "swr";
|
|
|
|
import { useBackendContext } from "../context/backend.js";
|
2023-02-26 00:06:17 +01:00
|
|
|
import { bankUiSettings } from "../settings.js";
|
2022-12-07 13:29:36 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Has the information to reach and
|
|
|
|
* authenticate at the bank's backend.
|
|
|
|
*/
|
2022-12-07 22:45:49 +01:00
|
|
|
export type BackendState = LoggedIn | LoggedOut;
|
2022-12-07 19:44:16 +01:00
|
|
|
|
2023-02-08 21:41:19 +01:00
|
|
|
export interface BackendCredentials {
|
2022-12-07 19:44:16 +01:00
|
|
|
username: string;
|
|
|
|
password: string;
|
|
|
|
}
|
|
|
|
|
2023-02-08 21:41:19 +01:00
|
|
|
interface LoggedIn extends BackendCredentials {
|
2022-12-07 22:45:49 +01:00
|
|
|
status: "loggedIn";
|
2023-02-08 21:41:19 +01:00
|
|
|
isUserAdministrator: boolean;
|
2022-12-07 19:44:16 +01:00
|
|
|
}
|
|
|
|
interface LoggedOut {
|
2022-12-07 22:45:49 +01:00
|
|
|
status: "loggedOut";
|
2022-12-07 13:29:36 +01:00
|
|
|
}
|
|
|
|
|
2023-05-29 19:53:06 +02:00
|
|
|
export const codecForBackendStateLoggedIn = (): Codec<LoggedIn> =>
|
|
|
|
buildCodecForObject<LoggedIn>()
|
|
|
|
.property("status", codecForConstString("loggedIn"))
|
|
|
|
.property("username", codecForString())
|
|
|
|
.property("password", codecForString())
|
|
|
|
.property("isUserAdministrator", codecForBoolean())
|
|
|
|
.build("BackendState.LoggedIn");
|
|
|
|
|
|
|
|
export const codecForBackendStateLoggedOut = (): Codec<LoggedOut> =>
|
|
|
|
buildCodecForObject<LoggedOut>()
|
|
|
|
.property("status", codecForConstString("loggedOut"))
|
|
|
|
.build("BackendState.LoggedOut");
|
|
|
|
|
|
|
|
export const codecForBackendState = (): Codec<BackendState> =>
|
|
|
|
buildCodecForUnion<BackendState>()
|
|
|
|
.discriminateOn("status")
|
|
|
|
.alternative("loggedIn", codecForBackendStateLoggedIn())
|
|
|
|
.alternative("loggedOut", codecForBackendStateLoggedOut())
|
|
|
|
.build("BackendState");
|
|
|
|
|
2023-02-08 21:41:19 +01:00
|
|
|
export function getInitialBackendBaseURL(): string {
|
2023-04-21 15:49:02 +02:00
|
|
|
const overrideUrl =
|
|
|
|
typeof localStorage !== "undefined"
|
|
|
|
? localStorage.getItem("bank-base-url")
|
|
|
|
: undefined;
|
2023-04-13 17:17:50 +02:00
|
|
|
if (!overrideUrl) {
|
|
|
|
//normal path
|
|
|
|
if (!bankUiSettings.backendBaseURL) {
|
|
|
|
console.error(
|
|
|
|
"ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'",
|
|
|
|
);
|
|
|
|
return canonicalizeBaseUrl(window.origin);
|
|
|
|
}
|
|
|
|
return canonicalizeBaseUrl(bankUiSettings.backendBaseURL);
|
|
|
|
}
|
|
|
|
// testing/development path
|
|
|
|
return canonicalizeBaseUrl(overrideUrl);
|
2023-02-08 21:41:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export const defaultState: BackendState = {
|
|
|
|
status: "loggedOut",
|
|
|
|
};
|
2022-12-07 19:44:16 +01:00
|
|
|
|
|
|
|
export interface BackendStateHandler {
|
2022-12-07 22:45:49 +01:00
|
|
|
state: BackendState;
|
2023-02-08 21:41:19 +01:00
|
|
|
logOut(): void;
|
|
|
|
logIn(info: BackendCredentials): void;
|
2022-12-07 19:44:16 +01:00
|
|
|
}
|
2023-05-29 19:53:06 +02:00
|
|
|
|
|
|
|
const BACKEND_STATE_KEY = buildStorageKey(
|
|
|
|
"backend-state",
|
|
|
|
codecForBackendState(),
|
|
|
|
);
|
|
|
|
|
2022-12-07 13:29:36 +01:00
|
|
|
/**
|
|
|
|
* Return getters and setters for
|
|
|
|
* login credentials and backend's
|
|
|
|
* base URL.
|
|
|
|
*/
|
2022-12-07 19:44:16 +01:00
|
|
|
export function useBackendState(): BackendStateHandler {
|
2023-05-29 19:53:06 +02:00
|
|
|
const { value: state, update } = useLocalStorage(
|
|
|
|
BACKEND_STATE_KEY,
|
|
|
|
defaultState,
|
2022-12-07 22:45:49 +01:00
|
|
|
);
|
2023-02-08 21:41:19 +01:00
|
|
|
|
2022-12-07 19:44:16 +01:00
|
|
|
return {
|
|
|
|
state,
|
2023-02-08 21:41:19 +01:00
|
|
|
logOut() {
|
2023-05-29 19:53:06 +02:00
|
|
|
update(defaultState);
|
2022-12-07 19:44:16 +01:00
|
|
|
},
|
2023-02-08 21:41:19 +01:00
|
|
|
logIn(info) {
|
|
|
|
//admin is defined by the username
|
2023-02-10 13:51:37 +01:00
|
|
|
const nextState: BackendState = {
|
|
|
|
status: "loggedIn",
|
|
|
|
...info,
|
|
|
|
isUserAdministrator: info.username === "admin",
|
|
|
|
};
|
2023-05-29 19:53:06 +02:00
|
|
|
update(nextState);
|
2022-12-07 19:44:16 +01:00
|
|
|
},
|
2022-12-07 22:45:49 +01:00
|
|
|
};
|
2022-12-07 13:29:36 +01:00
|
|
|
}
|
2023-02-08 21:41:19 +01:00
|
|
|
|
|
|
|
interface useBackendType {
|
|
|
|
request: <T>(
|
|
|
|
path: string,
|
|
|
|
options?: RequestOptions,
|
|
|
|
) => Promise<HttpResponseOk<T>>;
|
|
|
|
fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
|
2023-02-10 13:51:37 +01:00
|
|
|
multiFetcher: <T>(endpoint: string[][]) => Promise<HttpResponseOk<T>[]>;
|
|
|
|
paginatedFetcher: <T>(
|
|
|
|
args: [string, number, number],
|
|
|
|
) => Promise<HttpResponseOk<T>>;
|
|
|
|
sandboxAccountsFetcher: <T>(
|
|
|
|
args: [string, number, number, string],
|
|
|
|
) => Promise<HttpResponseOk<T>>;
|
2023-02-17 20:23:37 +01:00
|
|
|
sandboxCashoutFetcher: <T>(endpoint: string[]) => Promise<HttpResponseOk<T>>;
|
2023-02-08 21:41:19 +01:00
|
|
|
}
|
|
|
|
export function usePublicBackend(): useBackendType {
|
|
|
|
const { state } = useBackendContext();
|
|
|
|
const { request: requestHandler } = useApiContext();
|
|
|
|
|
2023-03-05 19:21:12 +01:00
|
|
|
const baseUrl = getInitialBackendBaseURL();
|
2023-02-08 21:41:19 +01:00
|
|
|
|
|
|
|
const request = useCallback(
|
|
|
|
function requestImpl<T>(
|
|
|
|
path: string,
|
|
|
|
options: RequestOptions = {},
|
|
|
|
): Promise<HttpResponseOk<T>> {
|
|
|
|
return requestHandler<T>(baseUrl, path, options);
|
|
|
|
},
|
|
|
|
[baseUrl],
|
|
|
|
);
|
|
|
|
|
|
|
|
const fetcher = useCallback(
|
|
|
|
function fetcherImpl<T>(endpoint: string): Promise<HttpResponseOk<T>> {
|
|
|
|
return requestHandler<T>(baseUrl, endpoint);
|
|
|
|
},
|
|
|
|
[baseUrl],
|
|
|
|
);
|
|
|
|
const paginatedFetcher = useCallback(
|
2023-02-10 13:51:37 +01:00
|
|
|
function fetcherImpl<T>([endpoint, page, size]: [
|
|
|
|
string,
|
|
|
|
number,
|
|
|
|
number,
|
|
|
|
]): Promise<HttpResponseOk<T>> {
|
|
|
|
return requestHandler<T>(baseUrl, endpoint, {
|
|
|
|
params: { page: page || 1, size },
|
|
|
|
});
|
2023-02-08 21:41:19 +01:00
|
|
|
},
|
|
|
|
[baseUrl],
|
|
|
|
);
|
|
|
|
const multiFetcher = useCallback(
|
2023-02-10 13:51:37 +01:00
|
|
|
function multiFetcherImpl<T>([endpoints]: string[][]): Promise<
|
|
|
|
HttpResponseOk<T>[]
|
|
|
|
> {
|
2023-02-08 21:41:19 +01:00
|
|
|
return Promise.all(
|
|
|
|
endpoints.map((endpoint) => requestHandler<T>(baseUrl, endpoint)),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
[baseUrl],
|
|
|
|
);
|
|
|
|
const sandboxAccountsFetcher = useCallback(
|
2023-02-10 13:51:37 +01:00
|
|
|
function fetcherImpl<T>([endpoint, page, size, account]: [
|
|
|
|
string,
|
|
|
|
number,
|
|
|
|
number,
|
|
|
|
string,
|
|
|
|
]): Promise<HttpResponseOk<T>> {
|
|
|
|
return requestHandler<T>(baseUrl, endpoint, {
|
|
|
|
params: { page: page || 1, size },
|
|
|
|
});
|
2023-02-08 21:41:19 +01:00
|
|
|
},
|
|
|
|
[baseUrl],
|
|
|
|
);
|
2023-02-17 20:23:37 +01:00
|
|
|
const sandboxCashoutFetcher = useCallback(
|
|
|
|
function fetcherImpl<T>([endpoint, account]: string[]): Promise<
|
|
|
|
HttpResponseOk<T>
|
|
|
|
> {
|
|
|
|
return requestHandler<T>(baseUrl, endpoint);
|
|
|
|
},
|
|
|
|
[baseUrl],
|
|
|
|
);
|
2023-02-10 13:51:37 +01:00
|
|
|
return {
|
|
|
|
request,
|
|
|
|
fetcher,
|
|
|
|
paginatedFetcher,
|
|
|
|
multiFetcher,
|
|
|
|
sandboxAccountsFetcher,
|
2023-02-17 20:23:37 +01:00
|
|
|
sandboxCashoutFetcher,
|
2023-02-10 13:51:37 +01:00
|
|
|
};
|
2023-02-08 21:41:19 +01:00
|
|
|
}
|
|
|
|
|
2023-04-07 22:30:01 +02:00
|
|
|
type CheckResult = ValidResult | RequestInvalidResult | InvalidationResult;
|
|
|
|
|
|
|
|
interface ValidResult {
|
|
|
|
valid: true;
|
|
|
|
}
|
|
|
|
interface RequestInvalidResult {
|
|
|
|
valid: false;
|
|
|
|
requestError: true;
|
|
|
|
cause: RequestError<any>["cause"];
|
|
|
|
}
|
|
|
|
interface InvalidationResult {
|
|
|
|
valid: false;
|
|
|
|
requestError: false;
|
|
|
|
error: unknown;
|
|
|
|
}
|
|
|
|
|
2023-03-15 13:25:23 +01:00
|
|
|
export function useCredentialsChecker() {
|
|
|
|
const { request } = useApiContext();
|
|
|
|
const baseUrl = getInitialBackendBaseURL();
|
|
|
|
//check against account details endpoint
|
|
|
|
//while sandbox backend doesn't have a login endpoint
|
|
|
|
return async function testLogin(
|
|
|
|
username: string,
|
|
|
|
password: string,
|
2023-04-07 22:30:01 +02:00
|
|
|
): Promise<CheckResult> {
|
2023-03-15 13:25:23 +01:00
|
|
|
try {
|
|
|
|
await request(baseUrl, `access-api/accounts/${username}/`, {
|
|
|
|
basicAuth: { username, password },
|
|
|
|
preventCache: true,
|
|
|
|
});
|
|
|
|
return { valid: true };
|
|
|
|
} catch (error) {
|
|
|
|
if (error instanceof RequestError) {
|
2023-04-07 22:30:01 +02:00
|
|
|
return { valid: false, requestError: true, cause: error.cause };
|
2023-03-15 13:25:23 +01:00
|
|
|
}
|
2023-04-07 22:30:01 +02:00
|
|
|
return { valid: false, requestError: false, error };
|
2023-03-15 13:25:23 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-02-08 21:41:19 +01:00
|
|
|
export function useAuthenticatedBackend(): useBackendType {
|
|
|
|
const { state } = useBackendContext();
|
|
|
|
const { request: requestHandler } = useApiContext();
|
|
|
|
|
2023-02-10 13:51:37 +01:00
|
|
|
const creds = state.status === "loggedIn" ? state : undefined;
|
2023-03-05 19:21:12 +01:00
|
|
|
const baseUrl = getInitialBackendBaseURL();
|
2023-02-08 21:41:19 +01:00
|
|
|
|
|
|
|
const request = useCallback(
|
|
|
|
function requestImpl<T>(
|
|
|
|
path: string,
|
|
|
|
options: RequestOptions = {},
|
|
|
|
): Promise<HttpResponseOk<T>> {
|
|
|
|
return requestHandler<T>(baseUrl, path, { basicAuth: creds, ...options });
|
|
|
|
},
|
|
|
|
[baseUrl, creds],
|
|
|
|
);
|
|
|
|
|
|
|
|
const fetcher = useCallback(
|
|
|
|
function fetcherImpl<T>(endpoint: string): Promise<HttpResponseOk<T>> {
|
|
|
|
return requestHandler<T>(baseUrl, endpoint, { basicAuth: creds });
|
|
|
|
},
|
|
|
|
[baseUrl, creds],
|
|
|
|
);
|
|
|
|
const paginatedFetcher = useCallback(
|
2023-03-31 19:18:39 +02:00
|
|
|
function fetcherImpl<T>([endpoint, page = 1, size]: [
|
2023-02-10 13:51:37 +01:00
|
|
|
string,
|
|
|
|
number,
|
|
|
|
number,
|
|
|
|
]): Promise<HttpResponseOk<T>> {
|
|
|
|
return requestHandler<T>(baseUrl, endpoint, {
|
|
|
|
basicAuth: creds,
|
|
|
|
params: { page, size },
|
|
|
|
});
|
2023-02-08 21:41:19 +01:00
|
|
|
},
|
|
|
|
[baseUrl, creds],
|
|
|
|
);
|
|
|
|
const multiFetcher = useCallback(
|
2023-02-10 13:51:37 +01:00
|
|
|
function multiFetcherImpl<T>([endpoints]: string[][]): Promise<
|
|
|
|
HttpResponseOk<T>[]
|
|
|
|
> {
|
2023-02-08 21:41:19 +01:00
|
|
|
return Promise.all(
|
2023-02-10 13:51:37 +01:00
|
|
|
endpoints.map((endpoint) =>
|
|
|
|
requestHandler<T>(baseUrl, endpoint, { basicAuth: creds }),
|
|
|
|
),
|
2023-02-08 21:41:19 +01:00
|
|
|
);
|
|
|
|
},
|
|
|
|
[baseUrl, creds],
|
|
|
|
);
|
|
|
|
const sandboxAccountsFetcher = useCallback(
|
2023-02-10 13:51:37 +01:00
|
|
|
function fetcherImpl<T>([endpoint, page, size, account]: [
|
|
|
|
string,
|
|
|
|
number,
|
|
|
|
number,
|
|
|
|
string,
|
|
|
|
]): Promise<HttpResponseOk<T>> {
|
|
|
|
return requestHandler<T>(baseUrl, endpoint, {
|
|
|
|
basicAuth: creds,
|
|
|
|
params: { page: page || 1, size },
|
|
|
|
});
|
2023-02-08 21:41:19 +01:00
|
|
|
},
|
|
|
|
[baseUrl],
|
|
|
|
);
|
2023-02-10 13:51:37 +01:00
|
|
|
|
2023-02-17 20:23:37 +01:00
|
|
|
const sandboxCashoutFetcher = useCallback(
|
|
|
|
function fetcherImpl<T>([endpoint, account]: string[]): Promise<
|
|
|
|
HttpResponseOk<T>
|
|
|
|
> {
|
|
|
|
return requestHandler<T>(baseUrl, endpoint, {
|
|
|
|
basicAuth: creds,
|
|
|
|
params: { account },
|
|
|
|
});
|
|
|
|
},
|
|
|
|
[baseUrl, creds],
|
|
|
|
);
|
2023-02-10 13:51:37 +01:00
|
|
|
return {
|
|
|
|
request,
|
|
|
|
fetcher,
|
|
|
|
paginatedFetcher,
|
|
|
|
multiFetcher,
|
|
|
|
sandboxAccountsFetcher,
|
2023-02-17 20:23:37 +01:00
|
|
|
sandboxCashoutFetcher,
|
2023-02-10 13:51:37 +01:00
|
|
|
};
|
2023-02-08 21:41:19 +01:00
|
|
|
}
|
2023-02-28 23:03:43 +01:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @deprecated
|
|
|
|
*/
|
2023-02-10 13:51:37 +01:00
|
|
|
export function useBackendConfig(): HttpResponse<
|
|
|
|
SandboxBackend.Config,
|
|
|
|
SandboxBackend.SandboxError
|
|
|
|
> {
|
2023-02-08 21:41:19 +01:00
|
|
|
const { request } = usePublicBackend();
|
|
|
|
|
|
|
|
type Type = SandboxBackend.Config;
|
|
|
|
|
2023-02-10 13:51:37 +01:00
|
|
|
const [result, setResult] = useState<
|
|
|
|
HttpResponse<Type, SandboxBackend.SandboxError>
|
|
|
|
>({ loading: true });
|
2023-02-08 21:41:19 +01:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
request<Type>(`/config`)
|
|
|
|
.then((data) => setResult(data))
|
2023-05-12 16:23:38 +02:00
|
|
|
.catch((error: RequestError<SandboxBackend.SandboxError>) =>
|
|
|
|
setResult(error.cause),
|
|
|
|
);
|
2023-02-08 21:41:19 +01:00
|
|
|
}, [request]);
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
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, value?: unknown) {
|
|
|
|
const allKeys = Array.from(cache.keys());
|
|
|
|
const keys = allKeys.filter((key) => re.test(key));
|
|
|
|
const mutations = keys.map((key) => {
|
2023-02-10 13:51:37 +01:00
|
|
|
return mutate(key, value, true);
|
2023-02-08 21:41:19 +01:00
|
|
|
});
|
|
|
|
return Promise.all(mutations);
|
|
|
|
};
|
|
|
|
}
|