diff options
Diffstat (limited to 'packages/merchant-backoffice-ui/src/context')
4 files changed, 252 insertions, 261 deletions
diff --git a/packages/merchant-backoffice-ui/src/context/backend.test.ts b/packages/merchant-backoffice-ui/src/context/backend.test.ts deleted file mode 100644 index 74530e750..000000000 --- a/packages/merchant-backoffice-ui/src/context/backend.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2024 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 * as tests from "@gnu-taler/web-util/testing"; -import { ComponentChildren, h, VNode } from "preact"; -import { AccessToken, MerchantBackend } from "../declaration.js"; -import { - useAdminAPI, - useInstanceAPI, - useManagementAPI, -} from "../hooks/instance.js"; -import { expect } from "chai"; -import { ApiMockEnvironment } from "../hooks/testing.js"; -import { - API_CREATE_INSTANCE, - API_NEW_LOGIN, - API_UPDATE_CURRENT_INSTANCE_AUTH, - API_UPDATE_INSTANCE_AUTH_BY_ID, -} from "../hooks/urls.js"; - -interface TestingContextProps { - children?: ComponentChildren; -} - -describe("backend context api ", () => { - it("should use new token after updating the instance token in the settings as user", async () => { - const env = new ApiMockEnvironment(); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const instance = useInstanceAPI(); - const management = useManagementAPI("default"); - const admin = useAdminAPI(); - - return { instance, management, admin }; - }, - {}, - [ - ({ instance, management, admin }) => { - env.addRequestExpectation(API_UPDATE_INSTANCE_AUTH_BY_ID("default"), { - request: { - method: "token", - token: "another_token", - }, - response: { - name: "instance_name", - } as MerchantBackend.Instances.QueryInstancesResponse, - }); - env.addRequestExpectation(API_NEW_LOGIN, { - auth: "another_token", - request: { - scope: "write", - duration: { - "d_us": "forever", - }, - refreshable: true, - }, - - }); - - management.setNewAccessToken(undefined,"another_token" as AccessToken); - }, - ({ instance, management, admin }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - - env.addRequestExpectation(API_CREATE_INSTANCE, { - // auth: "another_token", - request: { - id: "new_instance_id", - } as MerchantBackend.Instances.InstanceConfigurationMessage, - }); - - admin.createInstance({ - id: "new_instance_id", - } as MerchantBackend.Instances.InstanceConfigurationMessage); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should use new token after updating the instance token in the settings as admin", async () => { - const env = new ApiMockEnvironment(); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const instance = useInstanceAPI(); - const management = useManagementAPI("default"); - const admin = useAdminAPI(); - - return { instance, management, admin }; - }, - {}, - [ - ({ instance, management, admin }) => { - env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, { - request: { - method: "token", - token: "another_token", - }, - response: { - name: "instance_name", - } as MerchantBackend.Instances.QueryInstancesResponse, - }); - env.addRequestExpectation(API_NEW_LOGIN, { - auth: "another_token", - request: { - scope: "write", - duration: { - "d_us": "forever", - }, - refreshable: true, - }, - }); - instance.setNewAccessToken(undefined, "another_token" as AccessToken); - }, - ({ instance, management, admin }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - - env.addRequestExpectation(API_CREATE_INSTANCE, { - // auth: "another_token", - request: { - id: "new_instance_id", - } as MerchantBackend.Instances.InstanceConfigurationMessage, - }); - - admin.createInstance({ - id: "new_instance_id", - } as MerchantBackend.Instances.InstanceConfigurationMessage); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); diff --git a/packages/merchant-backoffice-ui/src/context/backend.ts b/packages/merchant-backoffice-ui/src/context/backend.ts deleted file mode 100644 index f78236216..000000000 --- a/packages/merchant-backoffice-ui/src/context/backend.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2024 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 { useMemoryStorage } from "@gnu-taler/web-util/browser"; -import { createContext, h, VNode } from "preact"; -import { useContext } from "preact/hooks"; -import { LoginToken } from "../declaration.js"; -import { useBackendDefaultToken, useBackendURL } from "../hooks/index.js"; - -interface BackendContextType { - url: string, - alreadyTriedLogin: boolean; - token?: LoginToken; - updateToken: (token: LoginToken | undefined) => void; -} - -const BackendContext = createContext<BackendContextType>({ - url: "", - alreadyTriedLogin: false, - token: undefined, - updateToken: () => null, -}); - -function useBackendContextState( - defaultUrl?: string, -): BackendContextType { - const [url] = useBackendURL(defaultUrl); - const [token, updateToken] = useBackendDefaultToken(); - - return { - url, - token, - alreadyTriedLogin: token !== undefined, - updateToken, - }; -} - -const BackendContextProvider = ({ - children, - defaultUrl, -}: { - children: any; - defaultUrl?: string; -}): VNode => { - const value = useBackendContextState(defaultUrl); - - return h(BackendContext.Provider, { value, children }); -}; - -const useBackendContext = (): BackendContextType => - useContext(BackendContext); diff --git a/packages/merchant-backoffice-ui/src/context/config.ts b/packages/merchant-backoffice-ui/src/context/config.ts deleted file mode 100644 index 8c562b3c1..000000000 --- a/packages/merchant-backoffice-ui/src/context/config.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2024 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 { createContext } from "preact"; -import { useContext } from "preact/hooks"; -import { MerchantBackend } from "../declaration.js"; - -const Context = createContext<MerchantBackend.VersionResponse>(null!); - -export const ConfigContextProvider = Context.Provider; -export const useConfigContext = (): MerchantBackend.VersionResponse => useContext(Context); diff --git a/packages/merchant-backoffice-ui/src/context/session.ts b/packages/merchant-backoffice-ui/src/context/session.ts new file mode 100644 index 000000000..83f3f113a --- /dev/null +++ b/packages/merchant-backoffice-ui/src/context/session.ts @@ -0,0 +1,252 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 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/> + */ + +import { + AccessToken, + Codec, + buildCodecForObject, + buildCodecForUnion, + codecForBoolean, + codecForConstString, + codecForString, + codecOptional, +} from "@gnu-taler/taler-util"; +import { + buildStorageKey, + useLocalStorage, + useMerchantApiContext, +} from "@gnu-taler/web-util/browser"; +import { mutate } from "swr"; + +/** + * Has the information to reach and + * authenticate at the bank's backend. + */ +export type SessionState = LoggedIn | LoggedOut | Expired; + +interface LoggedIn { + status: "loggedIn"; + backendUrl: string; + isAdmin: boolean; + instance: string; + token: AccessToken | undefined; + impersonate: Impersonate | undefined; +} +interface Impersonate { + originalInstance: string; + originalToken: AccessToken | undefined; + originalBackendUrl: string; +} +interface Expired { + status: "expired"; + backendUrl: string; + isAdmin: boolean; + instance: string; + impersonate: Impersonate | undefined; +} +interface LoggedOut { + status: "loggedOut"; + backendUrl: string; + instance: string; + isAdmin: boolean; +} + +export const codecForSessionStateLoggedIn = (): Codec<LoggedIn> => + buildCodecForObject<LoggedIn>() + .property("status", codecForConstString("loggedIn")) + .property("backendUrl", codecForString()) + .property("instance", codecForString()) + .property("impersonate", codecOptional(codecForImpresonate())) + .property("token", codecOptional(codecForString() as Codec<AccessToken>)) + .property("isAdmin", codecForBoolean()) + .build("SessionState.LoggedIn"); + +export const codecForSessionStateExpired = (): Codec<Expired> => + buildCodecForObject<Expired>() + .property("status", codecForConstString("expired")) + .property("backendUrl", codecForString()) + .property("instance", codecForString()) + .property("impersonate", codecOptional(codecForImpresonate())) + .property("isAdmin", codecForBoolean()) + .build("SessionState.Expired"); + +export const codecForSessionStateLoggedOut = (): Codec<LoggedOut> => + buildCodecForObject<LoggedOut>() + .property("status", codecForConstString("loggedOut")) + .property("backendUrl", codecForString()) + .property("instance", codecForString()) + .property("isAdmin", codecForBoolean()) + .build("SessionState.LoggedOut"); + +export const codecForImpresonate = (): Codec<Impersonate> => + buildCodecForObject<Impersonate>() + .property("originalInstance", codecForString()) + .property( + "originalToken", + codecOptional(codecForString() as Codec<AccessToken>), + ) + .property("originalBackendUrl", codecForString()) + .build("SessionState.Impersonate"); + +export const codecForSessionState = (): Codec<SessionState> => + buildCodecForUnion<SessionState>() + .discriminateOn("status") + .alternative("loggedIn", codecForSessionStateLoggedIn()) + .alternative("loggedOut", codecForSessionStateLoggedOut()) + .alternative("expired", codecForSessionStateExpired()) + .build("SessionState"); + +function inferInstanceName(url: URL) { + const match = INSTANCE_ID_LOOKUP.exec(url.href); + return !match || !match[1] ? DEFAULT_ADMIN_USERNAME : match[1]; +} + +export const defaultState = (url: URL): SessionState => { + const instance = inferInstanceName(url); + return { + status: "loggedIn", + instance, + backendUrl: url.href, + isAdmin: instance === DEFAULT_ADMIN_USERNAME, + token: undefined, + impersonate: undefined, + }; +}; + +export interface SessionStateHandler { + state: SessionState; + /** + * from every state to logout state + */ + logOut(): void; + /** + * from impersonate to loggedIn + */ + deImpersonate(): void; + /** + * from non-loggedOut state to expired + */ + expired(): void; + /** + * from any to loggedIn + * @param info + */ + logIn(info: { token?: AccessToken }): void; + /** + * from loggedIn to impersonate + * @param info + */ + impersonate(info: { instance: string; token?: AccessToken }): void; +} + +const SESSION_STATE_KEY = buildStorageKey( + "merchant-session", + codecForSessionState(), +); + +export const DEFAULT_ADMIN_USERNAME = "default"; + +export const INSTANCE_ID_LOOKUP = /\/instances\/([^/]*)\/?$/; + +/** + * Return getters and setters for + * login credentials and backend's + * base URL. + */ +export function useSessionContext(): SessionStateHandler { + const { url } = useMerchantApiContext(); + + const { value: state, update } = useLocalStorage( + SESSION_STATE_KEY, + defaultState(url), + ); + + return { + state, + logOut() { + const instance = inferInstanceName(url); + const nextState: SessionState = { + status: "loggedOut", + backendUrl: url.href, + instance, + isAdmin: instance === DEFAULT_ADMIN_USERNAME, + }; + update(nextState); + }, + deImpersonate() { + if (state.status === "loggedOut" || state.status === "expired") { + // can't impersonate if not loggedin + return; + } + if (state.impersonate === undefined) { + return; + } + const nextState: SessionState = { + status: "loggedIn", + backendUrl: state.impersonate.originalBackendUrl, + isAdmin: state.impersonate.originalInstance === DEFAULT_ADMIN_USERNAME, + instance: state.impersonate.originalInstance, + token: state.impersonate.originalToken, + impersonate: undefined, + }; + update(nextState); + }, + impersonate(info) { + if (state.status === "loggedOut" || state.status === "expired") { + // can't impersonate if not loggedin + return; + } + const nextState: SessionState = { + status: "loggedIn", + backendUrl: new URL(`instances/${info.instance}`, state.backendUrl) + .href, + isAdmin: info.instance === DEFAULT_ADMIN_USERNAME, + instance: info.instance, + token: info.token, + impersonate: { + originalBackendUrl: state.backendUrl, + originalToken: state.token, + originalInstance: state.instance, + }, + }; + update(nextState); + }, + expired() { + if (state.status === "loggedOut") return; + + const nextState: SessionState = { + ...state, + status: "expired", + }; + update(nextState); + }, + logIn(info) { + // admin is defined by the username + const nextState: SessionState = { + impersonate: undefined, + ...state, + status: "loggedIn", + token: info.token, + }; + update(nextState); + cleanAllCache(); + }, + }; +} + +function cleanAllCache(): void { + mutate(() => true, undefined, { revalidate: false }); +} |