diff options
Diffstat (limited to 'packages/merchant-backoffice-ui/src/context/session.ts')
-rw-r--r-- | packages/merchant-backoffice-ui/src/context/session.ts | 252 |
1 files changed, 252 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..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 }); +} |