diff options
Diffstat (limited to 'packages/merchant-backoffice-ui/src/context/session.ts')
-rw-r--r-- | packages/merchant-backoffice-ui/src/context/session.ts | 255 |
1 files changed, 255 insertions, 0 deletions
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..fa5e14ab3 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/context/session.ts @@ -0,0 +1,255 @@ +/* + 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, + TalerMerchantApi, + buildCodecForObject, + codecForString, + codecForURL, + codecOptional, +} from "@gnu-taler/taler-util"; +import { + buildStorageKey, + useLocalStorage, + useMerchantApiContext, +} from "@gnu-taler/web-util/browser"; +import { ComponentChildren, VNode, createContext, h } from "preact"; +import { useContext, useEffect, useState } from "preact/hooks"; +import { mutate } from "swr"; +import { MerchantLib } from "../../../web-util/lib/context/activity.js"; + +/** + * Has the information to reach and + * authenticate at the bank's backend. + */ +export type SessionState = LoggedIn | LoggedOut; + +interface LoggedIn { + status: "loggedIn"; + + // is this instance admin? usually "default" name + isAdmin: boolean; + + // url where all the request will be made + // usually this is from where the SPA was loaded + // unless it's using impersonate feature + backendUrl: URL; + + // instance name + instance: string; + + // session is not the same from where it was loaded + impersonated: boolean; + + //instance access token + token: AccessToken | undefined; +} + +interface LoggedOut { + status: "loggedOut"; + backendUrl: URL; + instance: string; + isAdmin: boolean; + token: AccessToken | undefined; +} + +interface SavedSession { + backendUrl: URL; + token: AccessToken | undefined; + prevToken: AccessToken | undefined; +} + +export const codecForSessionState = (): Codec<SavedSession> => + buildCodecForObject<SavedSession>() + .property("backendUrl", codecForURL()) + .property("token", codecOptional(codecForString() as Codec<AccessToken>)) + .property( + "prevToken", + codecOptional(codecForString() as Codec<AccessToken>), + ) + .build("SavedSession"); + +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): SavedSession => { + return { + backendUrl: url, + token: undefined, + prevToken: undefined, + }; +}; + +export interface SessionStateHandler { + lib: MerchantLib; + config: TalerMerchantApi.VersionResponse; + + state: SessionState; + /** + * from every state to logout state + */ + logOut(): void; + /** + * from impersonate to loggedIn + */ + deImpersonate(): void; + /** + * from any to loggedIn + * @param info + */ + logIn(token: AccessToken | undefined): void; + /** + * from loggedIn to impersonate + * @param info + */ + impersonate(baseUrl: URL): void; +} + +const SESSION_STATE_KEY = buildStorageKey( + "merchant-session", + codecForSessionState(), +); + +export const DEFAULT_ADMIN_USERNAME = "default"; + +export const INSTANCE_ID_LOOKUP = /\/instances\/([^/]*)\/?$/; + +export function cleanAllCache(): void { + mutate(() => true, undefined, { revalidate: false }); +} + +const Context = createContext<SessionStateHandler>(undefined!); + +export const useSessionContext = (): SessionStateHandler => useContext(Context); + +/** + * Creates the session in loggedIn state. + * Infer the instance name based on the URL. + * Create the instance of the merchant api http rest. + * Returns API that handle impersonation. + * + * @param param0 + * @returns + */ +export const SessionContextProvider = ({ + children, + // value, +}: { + // value: MerchantUiSettings; + children: ComponentChildren; +}): VNode => { + const { + lib: rootLib, + config: rootConfig, + url: merchantUrl, + } = useMerchantApiContext(); + const [status, setStatus] = useState<"loggedIn" | "loggedOut">("loggedIn"); + const [currentConfig, setCurrentConfig] = + useState<TalerMerchantApi.VersionResponse>(); + const { value: state, update } = useLocalStorage( + SESSION_STATE_KEY, + defaultState(merchantUrl), + ); + + const currentInstance = inferInstanceName(state.backendUrl); + + let lib: MerchantLib; + let config: TalerMerchantApi.VersionResponse; + const doingImpersonation = state.backendUrl.href !== merchantUrl.href; + if (doingImpersonation) { + /** + * FIXME: can't impersonate other than local instances + */ + lib = rootLib.subInstanceApi(inferInstanceName(state.backendUrl)); + + config = currentConfig ?? rootConfig; + } else { + lib = rootLib; + config = rootConfig; + } + + useEffect(() => { + // FIXME: handle what happen if the subinstance /config + // fails + if (!doingImpersonation) return; + lib.instance.getConfig().then((resp) => { + if (resp.type === "ok") { + setCurrentConfig(resp.body); + } + }); + }, [state.backendUrl.href]); + + const value: SessionStateHandler = { + state: { + backendUrl: state.backendUrl, + token: state.token, + impersonated: doingImpersonation, + instance: currentInstance, + isAdmin: currentInstance === DEFAULT_ADMIN_USERNAME, + status: status, + }, + lib, + config, + logOut() { + setStatus("loggedOut"); + update({ + backendUrl: merchantUrl, + token: undefined, + prevToken: undefined, + }); + cleanAllCache(); + }, + deImpersonate() { + cleanAllCache(); + update({ + backendUrl: merchantUrl, + token: state.prevToken, + prevToken: undefined, + }); + setStatus("loggedIn"); + }, + impersonate(baseUrl) { + /** + * FIXME: can't impersonate other than local instances + */ + update({ + backendUrl: baseUrl, + token: undefined, + prevToken: state.token, + }); + setStatus("loggedIn"); + cleanAllCache(); + }, + logIn(token) { + cleanAllCache(); + setStatus("loggedIn"); + update({ + backendUrl: state.backendUrl, + token: token, + prevToken: state.prevToken, + }); + }, + }; + + return h(Context.Provider, { + value, + children, + }); +}; |