aboutsummaryrefslogtreecommitdiff
path: root/packages/merchant-backoffice-ui/src/hooks
diff options
context:
space:
mode:
Diffstat (limited to 'packages/merchant-backoffice-ui/src/hooks')
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/backend.ts139
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/index.ts84
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/instance.test.ts4
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/instance.ts38
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/testing.tsx5
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/useSettings.ts9
6 files changed, 173 insertions, 106 deletions
diff --git a/packages/merchant-backoffice-ui/src/hooks/backend.ts b/packages/merchant-backoffice-ui/src/hooks/backend.ts
index ecd34df6d..fe4155788 100644
--- a/packages/merchant-backoffice-ui/src/hooks/backend.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/backend.ts
@@ -19,19 +19,21 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { useSWRConfig } from "swr";
-import { MerchantBackend } from "../declaration.js";
-import { useBackendContext } from "../context/backend.js";
-import { useCallback, useEffect, useState } from "preact/hooks";
-import { useInstanceContext } from "../context/instance.js";
+import { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util";
import {
ErrorType,
+ HttpError,
HttpResponse,
HttpResponseOk,
RequestError,
RequestOptions,
+ useApiContext,
} from "@gnu-taler/web-util/browser";
-import { 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(): (
@@ -85,6 +87,9 @@ export function useBackendInstancesTestForAdmin(): HttpResponse<
return result;
}
+const CHECK_CONFIG_INTERVAL_OK = 5 * 60 * 1000;
+const CHECK_CONFIG_INTERVAL_FAIL = 2 * 1000;
+
export function useBackendConfig(): HttpResponse<
MerchantBackend.VersionResponse,
RequestError<MerchantBackend.ErrorDetail>
@@ -92,18 +97,33 @@ export function useBackendConfig(): HttpResponse<
const { request } = useBackendBaseRequest();
type Type = MerchantBackend.VersionResponse;
-
- const [result, setResult] = useState<
- HttpResponse<Type, RequestError<MerchantBackend.ErrorDetail>>
- >({ loading: true });
+ type State = { data: HttpResponse<Type, RequestError<MerchantBackend.ErrorDetail>>, timer: number }
+ const [result, setResult] = useState<State>({ data: { loading: true }, timer: 0 });
useEffect(() => {
- request<Type>(`/config`)
- .then((data) => setResult(data))
- .catch((error) => setResult(error));
+ 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;
+ return result.data;
}
interface useBackendInstanceRequestType {
@@ -149,32 +169,86 @@ interface useBackendBaseRequestType {
}
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 testLogin(
- instance: string,
- token: string,
- ): Promise<{
- valid: boolean;
- cause?: ErrorType;
- }> {
+ 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(instance, `/private/`, {
+ const response = await request<MerchantBackend.Instances.LoginTokenSuccessResponse>(baseUrl, `/private/token`, {
+ method: "POST",
token,
+ data
});
- return { valid: true };
+ return { valid: true, token: response.data.token, expiration: response.data.expiration };
} catch (error) {
if (error instanceof RequestError) {
- return { valid: false, cause: error.cause.type };
+ return { valid: false, cause: error.cause };
}
- return { valid: false, cause: ErrorType.UNEXPECTED };
+ 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")
+ }
+ };
}
};
- return testLogin
+
+ 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 }
}
/**
@@ -183,15 +257,20 @@ export function useCredentialsChecker() {
* @returns request handler to
*/
export function useBackendBaseRequest(): useBackendBaseRequestType {
- const { url: backend, token } = useBackendContext();
+ 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, { token, ...options });
+ return requestHandler<T>(backend, endpoint, { token, ...options }).then(res => {
+ return res
+ }).catch(err => {
+ throw err
+ });
},
[backend, token],
);
@@ -204,10 +283,12 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
const { token: instanceToken, id, admin } = useInstanceContext();
const { request: requestHandler } = useApiContext();
- const { baseUrl, token } = !admin
+ 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,
diff --git a/packages/merchant-backoffice-ui/src/hooks/index.ts b/packages/merchant-backoffice-ui/src/hooks/index.ts
index 79b22304a..ee696779f 100644
--- a/packages/merchant-backoffice-ui/src/hooks/index.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/index.ts
@@ -19,9 +19,11 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { StateUpdater, useCallback, useEffect, useState } from "preact/hooks";
+import { buildCodecForObject, codecForMap, codecForString, codecForTimestamp } from "@gnu-taler/taler-util";
+import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
+import { StateUpdater, useEffect, useState } from "preact/hooks";
+import { LoginToken } from "../declaration.js";
import { ValueOrFunction } from "../utils/types.js";
-import { useMemoryStorage } from "@gnu-taler/web-util/browser";
import { useMatchMutate } from "./backend.js";
const calculateRootPath = () => {
@@ -32,53 +34,55 @@ const calculateRootPath = () => {
return rootPath;
};
+const loginTokenCodec = buildCodecForObject<LoginToken>()
+ .property("token", codecForString())
+ .property("expiration", codecForTimestamp)
+ .build("loginToken")
+const TOKENS_KEY = buildStorageKey("backend-token", codecForMap(loginTokenCodec));
+
+
export function useBackendURL(
url?: string,
-): [string, boolean, StateUpdater<string>, () => void] {
- const [value, setter] = useNotNullLocalStorage(
+): [string, StateUpdater<string>] {
+ const [value, setter] = useSimpleLocalStorage(
"backend-url",
url || calculateRootPath(),
);
- const [triedToLog, setTriedToLog] = useLocalStorage("tried-login");
const checkedSetter = (v: ValueOrFunction<string>) => {
- setTriedToLog("yes");
- return setter((p) => (v instanceof Function ? v(p) : v).replace(/\/$/, ""));
+ return setter((p) => (v instanceof Function ? v(p ?? "") : v).replace(/\/$/, ""));
};
- const resetBackend = () => {
- setTriedToLog(undefined);
- };
- return [value, !!triedToLog, checkedSetter, resetBackend];
+ return [value!, checkedSetter];
}
export function useBackendDefaultToken(
- initialValue?: string,
-): [string | undefined, ((d: string | undefined) => void)] {
- // uncomment for testing
- initialValue = "secret-token:secret" as string | undefined
- const { update: setToken, value: token, reset } = useMemoryStorage(`backend-token`, initialValue)
+): [LoginToken | undefined, ((d: LoginToken | undefined) => void)] {
+ const { update: setToken, value: tokenMap, reset } = useLocalStorage(TOKENS_KEY, {})
+
+ const tokenOfDefaultInstance = tokenMap["default"]
const clearCache = useMatchMutate()
useEffect(() => {
clearCache()
- }, [token])
+ }, [tokenOfDefaultInstance])
function updateToken(
- value: (string | undefined)
+ value: (LoginToken | undefined)
): void {
if (value === undefined) {
reset()
} else {
- setToken(value)
+ const res = { ...tokenMap, "default": value }
+ setToken(res)
}
}
- return [token, updateToken];
+ return [tokenMap["default"], updateToken];
}
export function useBackendInstanceToken(
id: string,
-): [string | undefined, ((d: string | undefined) => void)] {
- const { update: setToken, value: token, reset } = useMemoryStorage(`backend-token-${id}`)
+): [LoginToken | undefined, ((d: LoginToken | undefined) => void)] {
+ const { update: setToken, value: tokenMap, reset } = useLocalStorage(TOKENS_KEY, {})
const [defaultToken, defaultSetToken] = useBackendDefaultToken();
// instance named 'default' use the default token
@@ -86,16 +90,17 @@ export function useBackendInstanceToken(
return [defaultToken, defaultSetToken];
}
function updateToken(
- value: (string | undefined)
+ value: (LoginToken | undefined)
): void {
if (value === undefined) {
reset()
} else {
- setToken(value)
+ const res = { ...tokenMap, [id]: value }
+ setToken(res)
}
}
- return [token, updateToken];
+ return [tokenMap[id], updateToken];
}
export function useLang(initial?: string): [string, StateUpdater<string>] {
@@ -104,10 +109,10 @@ export function useLang(initial?: string): [string, StateUpdater<string>] {
? navigator.language || (navigator as any).userLanguage
: undefined;
const defaultLang = (browserLang || initial || "en").substring(0, 2);
- return useNotNullLocalStorage("lang-preference", defaultLang);
+ return useSimpleLocalStorage("lang-preference", defaultLang) as [string, StateUpdater<string>];
}
-export function useLocalStorage(
+export function useSimpleLocalStorage(
key: string,
initialValue?: string,
): [string | undefined, StateUpdater<string | undefined>] {
@@ -137,28 +142,3 @@ export function useLocalStorage(
return [storedValue, setValue];
}
-
-export function useNotNullLocalStorage(
- key: string,
- initialValue: string,
-): [string, StateUpdater<string>] {
- const [storedValue, setStoredValue] = useState<string>((): string => {
- return typeof window !== "undefined"
- ? window.localStorage.getItem(key) || initialValue
- : initialValue;
- });
-
- const setValue = (value: string | ((val: string) => string)) => {
- const valueToStore = value instanceof Function ? value(storedValue) : value;
- setStoredValue(valueToStore);
- if (typeof window !== "undefined") {
- if (!valueToStore) {
- window.localStorage.removeItem(key);
- } else {
- window.localStorage.setItem(key, valueToStore);
- }
- }
- };
-
- return [storedValue, setValue];
-}
diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.test.ts b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts
index d15b3f6d7..a7b8d047c 100644
--- a/packages/merchant-backoffice-ui/src/hooks/instance.test.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts
@@ -21,7 +21,7 @@
import * as tests from "@gnu-taler/web-util/testing";
import { expect } from "chai";
-import { MerchantBackend } from "../declaration.js";
+import { AccessToken, MerchantBackend } from "../declaration.js";
import {
useAdminAPI,
useBackendInstances,
@@ -158,7 +158,7 @@ describe("instance api interaction with details", () => {
},
} as MerchantBackend.Instances.QueryInstancesResponse,
});
- api.setNewToken("secret");
+ api.setNewToken("secret" as AccessToken);
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.ts b/packages/merchant-backoffice-ui/src/hooks/instance.ts
index 32ed30c6f..50f9487a3 100644
--- a/packages/merchant-backoffice-ui/src/hooks/instance.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/instance.ts
@@ -19,10 +19,11 @@ import {
RequestError,
} from "@gnu-taler/web-util/browser";
import { useBackendContext } from "../context/backend.js";
-import { MerchantBackend } from "../declaration.js";
+import { AccessToken, MerchantBackend } from "../declaration.js";
import {
useBackendBaseRequest,
useBackendInstanceRequest,
+ useCredentialsChecker,
useMatchMutate,
} from "./backend.js";
@@ -36,7 +37,7 @@ interface InstanceAPI {
) => Promise<void>;
deleteInstance: () => Promise<void>;
clearToken: () => Promise<void>;
- setNewToken: (token: string) => Promise<void>;
+ setNewToken: (token: AccessToken) => Promise<void>;
}
export function useAdminAPI(): AdminAPI {
@@ -86,8 +87,10 @@ export interface AdminAPI {
export function useManagementAPI(instanceId: string): InstanceAPI {
const mutateAll = useMatchMutate();
+ const { url: backendURL } = useBackendContext()
const { updateToken } = useBackendContext();
const { request } = useBackendBaseRequest();
+ const { requestNewLoginToken } = useCredentialsChecker()
const updateInstance = async (
instance: MerchantBackend.Instances.InstanceReconfigurationMessage,
@@ -117,13 +120,20 @@ export function useManagementAPI(instanceId: string): InstanceAPI {
mutateAll(/\/management\/instances/);
};
- const setNewToken = async (newToken: string): Promise<void> => {
+ const setNewToken = async (newToken: AccessToken): Promise<void> => {
await request(`/management/instances/${instanceId}/auth`, {
method: "POST",
data: { method: "token", token: newToken },
});
- updateToken(newToken);
+ const resp = await requestNewLoginToken(backendURL, newToken)
+ if (resp.valid) {
+ const { token, expiration } = resp
+ updateToken({ token, expiration });
+ } else {
+ updateToken(undefined)
+ }
+
mutateAll(/\/management\/instances/);
};
@@ -132,12 +142,13 @@ export function useManagementAPI(instanceId: string): InstanceAPI {
export function useInstanceAPI(): InstanceAPI {
const { mutate } = useSWRConfig();
+ const { url: backendURL, updateToken } = useBackendContext()
+
const {
- url: baseUrl,
token: adminToken,
- updateLoginStatus,
} = useBackendContext();
const { request } = useBackendInstanceRequest();
+ const { requestNewLoginToken } = useCredentialsChecker()
const updateInstance = async (
instance: MerchantBackend.Instances.InstanceReconfigurationMessage,
@@ -147,7 +158,7 @@ export function useInstanceAPI(): InstanceAPI {
data: instance,
});
- if (adminToken) mutate(["/private/instances", adminToken, baseUrl], null);
+ if (adminToken) mutate(["/private/instances", adminToken, backendURL], null);
mutate([`/private/`], null);
};
@@ -157,7 +168,7 @@ export function useInstanceAPI(): InstanceAPI {
// token: adminToken,
});
- if (adminToken) mutate(["/private/instances", adminToken, baseUrl], null);
+ if (adminToken) mutate(["/private/instances", adminToken, backendURL], null);
mutate([`/private/`], null);
};
@@ -170,13 +181,20 @@ export function useInstanceAPI(): InstanceAPI {
mutate([`/private/`], null);
};
- const setNewToken = async (newToken: string): Promise<void> => {
+ const setNewToken = async (newToken: AccessToken): Promise<void> => {
await request(`/private/auth`, {
method: "POST",
data: { method: "token", token: newToken },
});
- updateLoginStatus(baseUrl, newToken);
+ const resp = await requestNewLoginToken(backendURL, newToken)
+ if (resp.valid) {
+ const { token, expiration } = resp
+ updateToken({ token, expiration });
+ } else {
+ updateToken(undefined)
+ }
+
mutate([`/private/`], null);
};
diff --git a/packages/merchant-backoffice-ui/src/hooks/testing.tsx b/packages/merchant-backoffice-ui/src/hooks/testing.tsx
index ebbc6f64a..847d512b0 100644
--- a/packages/merchant-backoffice-ui/src/hooks/testing.tsx
+++ b/packages/merchant-backoffice-ui/src/hooks/testing.tsx
@@ -90,10 +90,7 @@ export class ApiMockEnvironment extends MockEnvironment {
const SC: any = SWRConfig;
return (
- <BackendContextProvider
- defaultUrl="http://backend"
- initialToken={undefined}
- >
+ <BackendContextProvider defaultUrl="http://backend">
<InstanceContextProvider
value={{
token: undefined,
diff --git a/packages/merchant-backoffice-ui/src/hooks/useSettings.ts b/packages/merchant-backoffice-ui/src/hooks/useSettings.ts
index 7dee9f896..8c1ebd9f6 100644
--- a/packages/merchant-backoffice-ui/src/hooks/useSettings.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/useSettings.ts
@@ -24,15 +24,6 @@ import {
codecForString,
} from "@gnu-taler/taler-util";
-function parse_json_or_undefined<T>(str: string | undefined): T | undefined {
- if (str === undefined) return undefined;
- try {
- return JSON.parse(str);
- } catch {
- return undefined;
- }
-}
-
export interface Settings {
advanceOrderMode: boolean;
dateFormat: "ymd" | "dmy" | "mdy";