/*
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
*/
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 =>
buildCodecForObject()
.property("backendUrl", codecForURL())
.property("token", codecOptional(codecForString() as Codec))
.property(
"prevToken",
codecOptional(codecForString() as Codec),
)
.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(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();
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,
});
};