taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 4db0f46e53e5676402820907a51a622ccc58ffdd
parent 78fc99d03ccd08f39923345c965982bcfe3bd707
Author: Sebastian <sebasjm@gmail.com>
Date:   Fri, 13 Jun 2025 19:07:21 -0300

fix #9754

Diffstat:
Mpackages/bank-ui/src/Routing.tsx | 77+++++++++--------------------------------------------------------------------
Mpackages/bank-ui/src/hooks/session.ts | 74+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mpackages/bank-ui/src/pages/LoginForm.tsx | 7++++---
Mpackages/merchant-backoffice-ui/src/AdminRoutes.tsx | 4++++
Mpackages/merchant-backoffice-ui/src/Routing.tsx | 38+++++++++++++++++++++++++++++++++-----
Mpackages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx | 23+++++++++++++++++++++--
Mpackages/merchant-backoffice-ui/src/paths/admin/list/View.tsx | 3+++
Mpackages/merchant-backoffice-ui/src/paths/admin/list/index.tsx | 8+++++++-
Mpackages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx | 9++++-----
Mpackages/merchant-backoffice-ui/src/paths/instance/token/index.tsx | 49++++++++++++++++++++++++++++++++++++++++---------
Mpackages/merchant-backoffice-ui/src/paths/instance/update/index.tsx | 5-----
Mpackages/taler-util/src/http-client/bank-core.ts | 40+++++++++++++++++++++++++++++++++++++---
12 files changed, 235 insertions(+), 102 deletions(-)

diff --git a/packages/bank-ui/src/Routing.tsx b/packages/bank-ui/src/Routing.tsx @@ -16,27 +16,28 @@ import { LocalNotificationBanner, - RouteDefinition, urlPattern, useBankCoreApiContext, useCurrentLocation, useLocalNotification, useNavigationContext, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { AbsoluteTime, AccessToken, - Duration, HttpStatusCode, + TalerErrorCode, + TokenRequest, TranslatedString, assertUnreachable, - createRFC8959AccessTokenEncoded, + createRFC8959AccessTokenEncoded } from "@gnu-taler/taler-util"; import { useEffect } from "preact/hooks"; -import { useSessionState } from "./hooks/session.js"; +import { useBankState } from "./hooks/bank-state.js"; +import { useRefreshSessionBeforeExpires, useSessionState } from "./hooks/session.js"; import { AccountPage } from "./pages/AccountPage/index.js"; import { BankFrame } from "./pages/BankFrame.js"; import { LoginForm, SESSION_DURATION } from "./pages/LoginForm.js"; @@ -56,9 +57,6 @@ import { RemoveAccount } from "./pages/admin/RemoveAccount.js"; import { ConversionConfig } from "./pages/regional/ConversionConfig.js"; import { CreateCashout } from "./pages/regional/CreateCashout.js"; import { ShowCashoutDetails } from "./pages/regional/ShowCashoutDetails.js"; -import { useBankState } from "./hooks/bank-state.js"; -import { TalerErrorCode } from "@gnu-taler/taler-util"; -import { TokenRequest } from "@gnu-taler/taler-util"; const TALER_SCREEN_ID = 100; @@ -66,64 +64,7 @@ Routing.SCREEN_ID = TALER_SCREEN_ID; export function Routing(): VNode { const session = useSessionState(); - const refreshSession = - session.state.status !== "loggedIn" || - session.state.expiration.t_ms === "never" - ? undefined - : { - user: session.state.username, - time: session.state.expiration, - auth: session.state.token, - }; - - const { - lib: { bank }, - } = useBankCoreApiContext(); - - useEffect(() => { - if (!refreshSession) return; - /** - * we need to wait before refreshing the session. Waiting too much and the token will - * be expired. So 20% before expiration should be close enough. - */ - const timeLeftBeforeExpiration = Duration.getRemaining(refreshSession.time); - const refreshWindow = Duration.multiply( - Duration.fromTalerProtocolDuration(SESSION_DURATION), - 0.2, - ); - if ( - timeLeftBeforeExpiration.d_ms === "forever" || - refreshWindow.d_ms === "forever" - ) - return; - const remain = Math.max( - timeLeftBeforeExpiration.d_ms - refreshWindow.d_ms, - 0, - ); - const timeoutId = setTimeout(async () => { - const result = await bank.createAccessTokenBasic( - refreshSession.user, - refreshSession.auth, - { - scope: "readwrite", - duration: SESSION_DURATION, - refreshable: true, - }, - ); - if (result.type === "fail") { - console.log(`could not refresh session ${result.case}`); - return; - } - session.logIn({ - username: refreshSession.user, - token: createRFC8959AccessTokenEncoded(result.body.access_token), - expiration: AbsoluteTime.fromProtocolTimestamp(result.body.expiration), - }); - }, remain); - return () => { - clearTimeout(timeoutId); - }; - }, [refreshSession]); + useRefreshSessionBeforeExpires() if (session.state.status === "loggedIn") { const { isUserAdministrator, username } = session.state; @@ -189,9 +130,9 @@ function PublicRounting({ duration: SESSION_DURATION, refreshable: true, } as TokenRequest; - const resp = await lib.bank.createAccessTokenBasic( + const resp = await lib.bank.createAccessToken( username, - password, + { type: "basic", password }, tokenRequest, ); if (resp.type === "ok") { diff --git a/packages/bank-ui/src/hooks/session.ts b/packages/bank-ui/src/hooks/session.ts @@ -14,6 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { Duration } from "@gnu-taler/taler-util"; import { AbsoluteTime, AccessToken, @@ -26,8 +27,11 @@ import { codecForString, codecOptionalDefault, } from "@gnu-taler/taler-util"; -import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; +import { buildStorageKey, useBankCoreApiContext, useLocalStorage } from "@gnu-taler/web-util/browser"; import { mutate } from "swr"; +import { SESSION_DURATION } from "../pages/LoginForm.js"; +import { createRFC8959AccessTokenEncoded } from "@gnu-taler/taler-util"; +import { useEffect } from "preact/hooks"; /** * Has the information to reach and @@ -140,3 +144,71 @@ export function useSessionState(): SessionStateHandler { function cleanAllCache(): void { mutate(() => true, undefined, { revalidate: false }); } + +/** + * Loads the session from local storage + * Sets a timeout before the session expires + * Makes a request to refresh the session + * Saves new session + */ +export function useRefreshSessionBeforeExpires() { + const session = useSessionState(); + + const { + lib: { bank }, + } = useBankCoreApiContext(); + + const refreshSession = + session.state.status !== "loggedIn" || + session.state.expiration.t_ms === "never" + ? undefined + : session.state; + + useEffect(() => { + if (!refreshSession) return; + /** + * we need to wait before refreshing the session. Waiting too much and the token will + * be expired. So 20% before expiration should be close enough. + */ + const timeLeftBeforeExpiration = Duration.getRemaining(refreshSession.expiration); + const refreshWindow = Duration.multiply( + Duration.fromTalerProtocolDuration(SESSION_DURATION), + 0.2, + ); + if ( + timeLeftBeforeExpiration.d_ms === "forever" || + refreshWindow.d_ms === "forever" + ) + return; + const remain = Math.max( + timeLeftBeforeExpiration.d_ms - refreshWindow.d_ms, + 0, + ); + const timeoutId = setTimeout(async () => { + const result = await bank.createAccessToken( + refreshSession.username, + { type: "bearer", accessToken: refreshSession.token }, + { + scope: "readwrite", + duration: SESSION_DURATION, + refreshable: true, + }, + ); + if (result.type === "fail") { + console.log( + `could not refresh session ${result.case}: ${JSON.stringify(result)}`, + ); + return; + } + session.logIn({ + username: refreshSession.username, + token: createRFC8959AccessTokenEncoded(result.body.access_token), + expiration: AbsoluteTime.fromProtocolTimestamp(result.body.expiration), + }); + }, remain); + return () => { + clearTimeout(timeoutId); + }; + }, [refreshSession]); + +} diff --git a/packages/bank-ui/src/pages/LoginForm.tsx b/packages/bank-ui/src/pages/LoginForm.tsx @@ -43,6 +43,7 @@ const TALER_SCREEN_ID = 104; export const SESSION_DURATION = Duration.toTalerProtocolDuration( Duration.fromSpec({ + // seconds: 6, minutes: 30, }), ); @@ -110,9 +111,9 @@ export function LoginForm({ ? undefined : withErrorHandler( async () => - authenticator.createAccessTokenBasic( + authenticator.createAccessToken( username, - password, + { type: "basic", password }, tokenRequest, ), (result) => { @@ -138,7 +139,7 @@ export function LoginForm({ password, }, }); - onAuthorizationRequired() + onAuthorizationRequired(); return i18n.str`Second factor authentication required.`; } case TalerErrorCode.GENERIC_FORBIDDEN: diff --git a/packages/merchant-backoffice-ui/src/AdminRoutes.tsx b/packages/merchant-backoffice-ui/src/AdminRoutes.tsx @@ -30,6 +30,10 @@ export function AdminRoutes(): VNode { <Route path={AdminPaths.list_instances} component={InstanceListPage} + onChangePassword={(id) => { + console.log("ASDASD") + route(`/instance/${id}/token`); + }} onCreate={() => { route(AdminPaths.new_instance); }} diff --git a/packages/merchant-backoffice-ui/src/Routing.tsx b/packages/merchant-backoffice-ui/src/Routing.tsx @@ -38,13 +38,16 @@ import { } from "./components/menu/index.js"; import { useSessionContext } from "./context/session.js"; import { useInstanceBankAccounts } from "./hooks/bank.js"; -import { useInstanceKYCDetails, useInstanceKYCDetailsLongPolling } from "./hooks/instance.js"; +import { useInstanceKYCDetailsLongPolling } from "./hooks/instance.js"; import { usePreference } from "./hooks/preference.js"; import InstanceCreatePage from "./paths/admin/create/index.js"; import InstanceListPage from "./paths/admin/list/index.js"; import BankAccountCreatePage from "./paths/instance/accounts/create/index.js"; import BankAccountListPage from "./paths/instance/accounts/list/index.js"; import BankAccountUpdatePage from "./paths/instance/accounts/update/index.js"; +import CreateCategory from "./paths/instance/categories/create/index.js"; +import ListCategories from "./paths/instance/categories/list/index.js"; +import UpdateCategory from "./paths/instance/categories/update/index.js"; import ListKYCPage from "./paths/instance/kyc/list/index.js"; import OrderCreatePage from "./paths/instance/orders/create/index.js"; import OrderDetailsPage from "./paths/instance/orders/details/index.js"; @@ -60,7 +63,10 @@ import TemplateListPage from "./paths/instance/templates/list/index.js"; import TemplateQrPage from "./paths/instance/templates/qr/index.js"; import TemplateUpdatePage from "./paths/instance/templates/update/index.js"; import TemplateUsePage from "./paths/instance/templates/use/index.js"; -import TokenPage from "./paths/instance/token/index.js"; +import TokenPage, { + AdminToken as InstanceAdminTokenPage, + Props as InstanceAdminTokenProps, +} from "./paths/instance/token/index.js"; import TokenFamilyCreatePage from "./paths/instance/tokenfamilies/create/index.js"; import TokenFamilyListPage from "./paths/instance/tokenfamilies/list/index.js"; import TokenFamilyUpdatePage from "./paths/instance/tokenfamilies/update/index.js"; @@ -76,9 +82,6 @@ import WebhookUpdatePage from "./paths/instance/webhooks/update/index.js"; import { LoginPage } from "./paths/login/index.js"; import { Settings } from "./paths/settings/index.js"; import { Notification } from "./utils/types.js"; -import ListCategories from "./paths/instance/categories/list/index.js"; -import CreateCategory from "./paths/instance/categories/create/index.js"; -import UpdateCategory from "./paths/instance/categories/update/index.js"; export enum InstancePaths { error = "/error", @@ -131,6 +134,7 @@ export enum AdminPaths { list_instances = "/instances", new_instance = "/instance/new", update_instance = "/instance/:id/update", + update_instance_auth = "/instance/:id/token", } export interface Props {} @@ -267,6 +271,9 @@ export function Routing(_p: Props): VNode { <Route path={AdminPaths.list_instances} component={InstanceListPage} + onChangePassword={(id: string): void => { + route(`/instance/${id}/token`); + }} onCreate={() => { route(AdminPaths.new_instance); }} @@ -295,6 +302,16 @@ export function Routing(_p: Props): VNode { }} /> )} + {state.isAdmin && ( + <Route + path={AdminPaths.update_instance_auth} + component={AdminInstanceUpdateTokenPage} + onCancel={() => route(AdminPaths.list_instances)} + onChange={() => { + route(AdminPaths.list_instances); + }} + /> + )} {/** * Update instance page */} @@ -662,6 +679,17 @@ function AdminInstanceUpdatePage({ ); } +function AdminInstanceUpdateTokenPage({ + id, + ...rest +}: { id: string } & InstanceAdminTokenProps): VNode { + return ( + <Fragment> + <InstanceAdminTokenPage {...rest} instanceId={id} /> + </Fragment> + ); +} + function BankAccountBanner(): VNode { const { i18n } = useTranslationContext(); diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx @@ -28,6 +28,7 @@ import { useSessionContext } from "../../../context/session.js"; interface Props { instances: TalerMerchantApi.Instance[]; onUpdate: (id: string) => void; + onChangePassword: (id: string) => void; onDelete: (id: TalerMerchantApi.Instance) => void; onPurge: (id: TalerMerchantApi.Instance) => void; onCreate: () => void; @@ -40,6 +41,7 @@ export function CardTable({ onUpdate, onPurge, onDelete, + onChangePassword, selected, }: Props): VNode { const [actionQueue, actionQueueHandler] = useState<Actions[]>([]); @@ -114,6 +116,7 @@ export function CardTable({ onPurge={onPurge} onUpdate={onUpdate} onDelete={onDelete} + onChangePassword={onChangePassword} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} /> @@ -130,6 +133,7 @@ interface TableProps { rowSelection: string[]; instances: TalerMerchantApi.Instance[]; onUpdate: (id: string) => void; + onChangePassword: (id: string) => void; onDelete: (id: TalerMerchantApi.Instance) => void; onPurge: (id: TalerMerchantApi.Instance) => void; rowSelectionHandler: StateUpdater<string[]>; @@ -140,11 +144,17 @@ function toggleSelected<T>(id: T): (prev: T[]) => T[] { prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id); } +/** + * FIXME: put this on UI settings + */ +const HIDE_EDIT_INSTANCE = true; + function Table({ rowSelection, rowSelectionHandler, instances, onUpdate, + onChangePassword, onDelete, onPurge, }: TableProps): VNode { @@ -212,12 +222,21 @@ function Table({ <td>{i.name}</td> <td class="is-actions-cell right-sticky"> <div class="buttons is-right"> + {HIDE_EDIT_INSTANCE ? undefined : ( + <button + class="button is-small is-success jb-modal" + type="button" + onClick={(): void => onUpdate(i.id)} + > + <i18n.Translate>Edit</i18n.Translate> + </button> + )} <button class="button is-small is-success jb-modal" type="button" - onClick={(): void => onUpdate(i.id)} + onClick={(): void => onChangePassword(i.id)} > - <i18n.Translate>Edit</i18n.Translate> + <i18n.Translate>Change password</i18n.Translate> </button> {!i.deleted && ( <button diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx @@ -29,6 +29,7 @@ interface Props { instances: TalerMerchantApi.Instance[]; onCreate: () => void; onUpdate: (id: string) => void; + onChangePassword: (id: string) => void; onDelete: (id: TalerMerchantApi.Instance) => void; onPurge: (id: TalerMerchantApi.Instance) => void; selected?: boolean; @@ -40,6 +41,7 @@ export function View({ onDelete, onPurge, onUpdate, + onChangePassword, selected, }: Props): VNode { const [show, setShow] = useState<"active" | "deleted" | null>("active"); @@ -98,6 +100,7 @@ export function View({ instances={showingInstances} onDelete={onDelete} onPurge={onPurge} + onChangePassword={onChangePassword} onUpdate={onUpdate} selected={selected} onCreate={onCreate} diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx @@ -41,10 +41,15 @@ import { View } from "./View.js"; interface Props { onCreate: () => void; onUpdate: (id: string) => void; + onChangePassword: (id: string) => void; instances: TalerMerchantApi.Instance[]; } -export default function Instances({ onCreate, onUpdate }: Props): VNode { +export default function Instances({ + onCreate, + onUpdate, + onChangePassword, +}: Props): VNode { const result = useBackendInstances(); const [deleting, setDeleting] = useState<TalerMerchantApi.Instance | null>( null, @@ -80,6 +85,7 @@ export default function Instances({ onCreate, onUpdate }: Props): VNode { onCreate={onCreate} onPurge={setPurging} onUpdate={onUpdate} + onChangePassword={onChangePassword} selected={!!deleting} /> {deleting && ( diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx @@ -19,6 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { AccessToken, createRFC8959AccessTokenPlain } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; @@ -26,11 +27,10 @@ import { AsyncButton } from "../../../components/exception/AsyncButton.js"; import { FormProvider } from "../../../components/form/FormProvider.js"; import { Input } from "../../../components/form/Input.js"; import { NotificationCard } from "../../../components/menu/index.js"; -import { useSessionContext } from "../../../context/session.js"; -import { AccessToken, createRFC8959AccessTokenPlain } from "@gnu-taler/taler-util"; import { undefinedIfEmpty } from "../../../utils/table.js"; interface Props { + instanceId: string; hasToken: boolean | undefined; onClearToken: (c: AccessToken | undefined) => void; onNewToken: (c: AccessToken | undefined, s: AccessToken) => void; @@ -38,6 +38,7 @@ interface Props { } export function DetailPage({ + instanceId, hasToken, onBack, onNewToken, @@ -69,9 +70,7 @@ export function DetailPage({ const hasErrors = errors !== undefined; - const { state } = useSessionContext(); - - const text = i18n.str`You are updating the password from instance with id "${state.instance}"`; + const text = i18n.str`You are updating the password from instance with id "${instanceId}"`; async function submitForm() { if (hasErrors) return; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx @@ -17,6 +17,8 @@ import { HttpStatusCode, MerchantAuthMethod, TalerError, + TalerMerchantInstanceHttpClient, + TalerMerchantManagementResultByMethod, assertUnreachable, } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; @@ -26,22 +28,47 @@ import { ErrorLoadingMerchant } from "../../../components/ErrorLoadingMerchant.j import { Loading } from "../../../components/exception/loading.js"; import { NotificationCard } from "../../../components/menu/index.js"; import { useSessionContext } from "../../../context/session.js"; -import { useInstanceDetails } from "../../../hooks/instance.js"; +import { useInstanceDetails, useManagedInstanceDetails } from "../../../hooks/instance.js"; import { Notification } from "../../../utils/types.js"; import { LoginPage } from "../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../notfound/index.js"; import { DetailPage } from "./DetailPage.js"; -interface Props { +export interface Props { onChange: () => void; onCancel: () => void; } -export default function Token({ onChange, onCancel }: Props): VNode { +export default function Token(props: Props): VNode { + const { lib } = useSessionContext(); + const updateCurrentInstanceAuthentication = lib.instance.updateCurrentInstanceAuthentication.bind(lib.instance); + const createAuthTokenFromToken = lib.instance.createAuthTokenFromToken.bind(lib.instance); + const result = useInstanceDetails(); + return CommonToken(props, result, updateCurrentInstanceAuthentication, createAuthTokenFromToken, true) +} + +export function AdminToken(props: Props& { instanceId: string }): VNode { + const { lib } = useSessionContext(); + const subInstaceLib = lib.subInstanceApi(props.instanceId).instance; + const updateCurrentInstanceAuthentication = subInstaceLib.updateCurrentInstanceAuthentication.bind(subInstaceLib); + const createAuthTokenFromToken = subInstaceLib.createAuthTokenFromToken.bind(subInstaceLib); + const result = useManagedInstanceDetails(props.instanceId); + return CommonToken(props, result, updateCurrentInstanceAuthentication, createAuthTokenFromToken, false) +} + +function CommonToken( + { onChange, onCancel }: Props, + result: + | TalerMerchantManagementResultByMethod<"getInstanceDetails"> + | TalerError + | undefined, + updateCurrentInstanceAuthentication: typeof TalerMerchantInstanceHttpClient.prototype.updateCurrentInstanceAuthentication, + createAuthTokenFromToken: typeof TalerMerchantInstanceHttpClient.prototype.createAuthTokenFromToken, + replaceSession: boolean, +): VNode { const { i18n } = useTranslationContext(); - const { state, logIn, lib } = useSessionContext(); + const { state, logIn } = useSessionContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); - const result = useInstanceDetails(); if (!result) return <Loading />; if (result instanceof TalerError) { @@ -68,10 +95,11 @@ export default function Token({ onChange, onCancel }: Props): VNode { <NotificationCard notification={notif} /> <DetailPage onBack={onCancel} + instanceId={result.body.name} hasToken={hasToken} onClearToken={async (currentToken): Promise<void> => { try { - const resp = await lib.instance.updateCurrentInstanceAuthentication( + const resp = await updateCurrentInstanceAuthentication( currentToken, { method: MerchantAuthMethod.EXTERNAL, @@ -99,7 +127,7 @@ export default function Token({ onChange, onCancel }: Props): VNode { try { { const resp = - await lib.instance.updateCurrentInstanceAuthentication( + await updateCurrentInstanceAuthentication( currentToken, { token: newToken, @@ -114,7 +142,7 @@ export default function Token({ onChange, onCancel }: Props): VNode { }); } } - const resp = await lib.instance.createAuthTokenFromToken(newToken, { + const resp = await createAuthTokenFromToken(newToken, { scope: "write", duration: { d_us: "forever", @@ -122,7 +150,10 @@ export default function Token({ onChange, onCancel }: Props): VNode { refreshable: true, }); if (resp.type === "ok") { - logIn(state.instance, resp.body.token); + if (replaceSession) { + // only renew the session token if we are not the admin + logIn(state.instance, resp.body.token); + } return onChange(); } else { return setNotif({ diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx @@ -35,11 +35,6 @@ import { UpdatePage } from "./UpdatePage.js"; export interface Props { onBack: () => void; onConfirm: () => void; - - // onUnauthorized: () => VNode; - // onNotFound: () => VNode; - // onLoadError: (e: HttpError<TalerErrorDetail>) => VNode; - // onUpdateError: (e: HttpError<TalerErrorDetail>) => void; } export default function Update(props: Props): VNode { diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts @@ -29,6 +29,7 @@ import { TokenRequest, UserAndPassword, UserAndToken, + assertUnreachable, codecForTalerCommonConfigResponse, codecForTokenInfoList, codecForTokenSuccessResponse, @@ -100,6 +101,15 @@ export enum TalerCoreBankCacheEviction { CREATE_CASHOUT, } +export type Credentials = BasicCredentials | BearerCredentials; +export type BasicCredentials = { + type: "basic"; + password: string; +}; +export type BearerCredentials = { + type: "bearer"; + accessToken: AccessToken; +}; /** * Protocol version spoken with the core bank. * @@ -129,11 +139,12 @@ export class TalerCoreBankHttpClient { } /** + * * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-token */ - async createAccessTokenBasic( + async createAccessToken( username: string, - password: string, + cred: Credentials, body: TokenRequest, cid?: string, ) { @@ -141,7 +152,12 @@ export class TalerCoreBankHttpClient { const resp = await this.httpLib.fetch(url.href, { method: "POST", headers: { - Authorization: makeBasicAuthHeader(username, password), + Authorization: + cred.type === "basic" + ? makeBasicAuthHeader(username, cred.password) + : cred.type === "bearer" + ? makeBearerTokenAuthHeader(cred.accessToken) + : assertUnreachable(cred), "X-Challenge-Id": cid, }, body, @@ -176,6 +192,24 @@ export class TalerCoreBankHttpClient { } /** + * @deprecated use createAccessToken + * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-token + */ + async createAccessTokenBasic( + username: string, + password: string, + body: TokenRequest, + cid?: string, + ) { + return this.createAccessToken( + username, + { type: "basic", password }, + body, + cid, + ); + } + + /** * https://docs.taler.net/core/api-corebank.html#delete--accounts-$USERNAME-token */ async deleteAccessToken(user: string, token: AccessToken) {