diff options
Diffstat (limited to 'packages/merchant-backoffice-ui/src/context/session.ts')
-rw-r--r-- | packages/merchant-backoffice-ui/src/context/session.ts | 282 |
1 files changed, 139 insertions, 143 deletions
diff --git a/packages/merchant-backoffice-ui/src/context/session.ts b/packages/merchant-backoffice-ui/src/context/session.ts index 6d01464e0..fb1b7b374 100644 --- a/packages/merchant-backoffice-ui/src/context/session.ts +++ b/packages/merchant-backoffice-ui/src/context/session.ts @@ -17,11 +17,10 @@ import { AccessToken, Codec, + TalerMerchantApi, buildCodecForObject, - buildCodecForUnion, - codecForBoolean, - codecForConstString, codecForString, + codecForURL, codecOptional, } from "@gnu-taler/taler-util"; import { @@ -29,99 +28,79 @@ import { 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 | Expired; +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; + + //instane access token token: AccessToken | undefined; - impersonate: Impersonate | undefined; -} -interface Impersonate { - originalInstance: string; - originalToken: AccessToken | undefined; - originalBackendUrl: string; -} -interface Expired { - status: "expired"; - isAdmin: boolean; - instance: string; - token?: undefined; - impersonate: Impersonate | undefined; } + interface LoggedOut { status: "loggedOut"; + backendUrl: URL; instance: string; isAdmin: boolean; - token?: undefined; + token: AccessToken | undefined; } -export const codecForSessionStateLoggedIn = (): Codec<LoggedIn> => - buildCodecForObject<LoggedIn>() - .property("status", codecForConstString("loggedIn")) - .property("instance", codecForString()) - .property("impersonate", codecOptional(codecForImpresonate())) +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("isAdmin", codecForBoolean()) - .build("SessionState.LoggedIn"); - -export const codecForSessionStateExpired = (): Codec<Expired> => - buildCodecForObject<Expired>() - .property("status", codecForConstString("expired")) - .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("instance", codecForString()) - .property("isAdmin", codecForBoolean()) - .build("SessionState.LoggedOut"); - -export const codecForImpresonate = (): Codec<Impersonate> => - buildCodecForObject<Impersonate>() - .property("originalInstance", codecForString()) .property( - "originalToken", + "prevToken", 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"); + .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): SessionState => { - const instance = inferInstanceName(url); +export const defaultState = (url: URL): SavedSession => { return { - status: "loggedIn", - instance, - isAdmin: instance === DEFAULT_ADMIN_USERNAME, + backendUrl: url, token: undefined, - impersonate: undefined, + prevToken: undefined, }; }; export interface SessionStateHandler { + lib: MerchantLib; + config: TalerMerchantApi.VersionResponse; + state: SessionState; /** * from every state to logout state @@ -132,19 +111,15 @@ export interface SessionStateHandler { */ deImpersonate(): void; /** - * from non-loggedOut state to expired - */ - expired(): void; - /** * from any to loggedIn * @param info */ - logIn(info: { token?: AccessToken }): void; + logIn(token: AccessToken | undefined): void; /** * from loggedIn to impersonate * @param info */ - impersonate(info: { instance: string; baseUrl: URL, token?: AccessToken }): void; + impersonate(baseUrl: URL): void; } const SESSION_STATE_KEY = buildStorageKey( @@ -156,95 +131,116 @@ 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: merchantUrl, changeBackend } = useMerchantApiContext(); +export function cleanAllCache(): void { + mutate(() => true, undefined, { revalidate: false }); +} +const Context = createContext<SessionStateHandler>(undefined!); + +export const useSessionContext = (): SessionStateHandler => useContext(Context); + +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), ); - return { - state, + 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() { - const instance = inferInstanceName(merchantUrl); - const nextState: SessionState = { - status: "loggedOut", - instance, - isAdmin: instance === DEFAULT_ADMIN_USERNAME, - }; - update(nextState); + setStatus("loggedOut"); + update({ + backendUrl: merchantUrl, + token: undefined, + prevToken: undefined, + }); + cleanAllCache(); }, deImpersonate() { - if (state.status === "loggedOut" || state.status === "expired") { - // can't impersonate if not loggedin - return; - } - if (state.impersonate === undefined) { - return; - } - const newURL = new URL(`./`, state.impersonate.originalBackendUrl); - changeBackend(newURL); - const nextState: SessionState = { - status: "loggedIn", - 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; - } - changeBackend(info.baseUrl); - const nextState: SessionState = { - status: "loggedIn", - isAdmin: info.instance === DEFAULT_ADMIN_USERNAME, - instance: info.instance, - // FIXME: bank and merchant should have consistent behavior - token: info.token?.substring("secret-token:".length) as AccessToken, - impersonate: { - originalBackendUrl: merchantUrl.href, - originalToken: state.token, - originalInstance: state.instance, - }, - }; - update(nextState); + cleanAllCache(); + update({ + backendUrl: merchantUrl, + token: state.prevToken, + prevToken: undefined, + }); + setStatus("loggedIn"); }, - expired() { - if (state.status === "loggedOut") return; - - const nextState: SessionState = { - ...state, - status: "expired", + impersonate(baseUrl) { + /** + * FIXME: can't impersonate other than local instances + */ + update({ + backendUrl: baseUrl, token: undefined, - }; - update(nextState); + prevToken: state.token, + }); + setStatus("loggedIn"); + cleanAllCache(); }, - logIn(info) { - // admin is defined by the username - const nextState: SessionState = { - impersonate: undefined, - ...state, - status: "loggedIn", - // FIXME: bank and merchant should have consistent behavior - token: info.token?.substring("secret-token:".length) as AccessToken, - // token: info.token, - }; - update(nextState); + logIn(token) { cleanAllCache(); + setStatus("loggedIn"); + update({ + backendUrl: state.backendUrl, + token: token, + prevToken: state.prevToken, + }); }, }; -} -export function cleanAllCache(): void { - mutate(() => true, undefined, { revalidate: false }); -} + return h(Context.Provider, { + value, + children, + }); +}; |