taler-typescript-core

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

commit 428d18b067b903e63313a4a4fe20e858b90bf329
parent b2f3334d5f6065ac2f0c0fbf98f58cce6561471a
Author: Sebastian <sebasjm@gmail.com>
Date:   Tue,  8 Jul 2025 12:11:30 -0300

fix #10108

Diffstat:
Mpackages/merchant-backoffice-ui/src/AdminRoutes.tsx | 3+--
Mpackages/merchant-backoffice-ui/src/Routing.tsx | 39++++++++++++++++++++++++++++++++-------
Mpackages/merchant-backoffice-ui/src/components/menu/SideBar.tsx | 12+++++++++++-
Apackages/merchant-backoffice-ui/src/hooks/access-tokens.ts | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/merchant-backoffice-ui/src/hooks/order.ts | 43+++++++++++++++++++++++++++++++++++--------
Mpackages/merchant-backoffice-ui/src/hooks/product.ts | 3++-
Mpackages/merchant-backoffice-ui/src/hooks/transfer.ts | 37++++++++++++++++++++++++++++++-------
Mpackages/merchant-backoffice-ui/src/hooks/webhooks.ts | 37-------------------------------------
Apackages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/Create.stories.tsx | 27+++++++++++++++++++++++++++
Apackages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/CreatePage.tsx | 170+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/index.tsx | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/merchant-backoffice-ui/src/paths/instance/accessTokens/list/Table.tsx | 179+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/merchant-backoffice-ui/src/paths/instance/accessTokens/list/index.tsx | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx | 4++--
Apackages/merchant-backoffice-ui/src/paths/instance/password/DetailPage.tsx | 154+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/merchant-backoffice-ui/src/paths/instance/password/index.tsx | 210+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rpackages/merchant-backoffice-ui/src/paths/instance/token/stories.tsx -> packages/merchant-backoffice-ui/src/paths/instance/password/stories.tsx | 0
Mpackages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx | 4++--
Dpackages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx | 167-------------------------------------------------------------------------------
Dpackages/merchant-backoffice-ui/src/paths/instance/token/index.tsx | 208-------------------------------------------------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx | 14++++++++++----
Mpackages/taler-util/src/types-taler-common.ts | 11+----------
Mpackages/taler-util/src/types-taler-merchant.ts | 6++++++
23 files changed, 1100 insertions(+), 456 deletions(-)

diff --git a/packages/merchant-backoffice-ui/src/AdminRoutes.tsx b/packages/merchant-backoffice-ui/src/AdminRoutes.tsx @@ -31,8 +31,7 @@ export function AdminRoutes(): VNode { path={AdminPaths.list_instances} component={InstanceListPage} onChangePassword={(id) => { - console.log("ASDASD") - route(`/instance/${id}/token`); + route(`/instance/${id}/password`); }} onCreate={() => { route(AdminPaths.new_instance); diff --git a/packages/merchant-backoffice-ui/src/Routing.tsx b/packages/merchant-backoffice-ui/src/Routing.tsx @@ -42,6 +42,8 @@ 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 AccessTokenCreatePage from "./paths/instance/accessTokens/create/index.js"; +import AccessTokenListPage from "./paths/instance/accessTokens/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"; @@ -55,6 +57,10 @@ import OrderListPage from "./paths/instance/orders/list/index.js"; import ValidatorCreatePage from "./paths/instance/otp_devices/create/index.js"; import ValidatorListPage from "./paths/instance/otp_devices/list/index.js"; import ValidatorUpdatePage from "./paths/instance/otp_devices/update/index.js"; +import PasswordPage, { + AdminPassword as InstanceAdminTokenPage, + Props as InstanceAdminTokenProps, +} from "./paths/instance/password/index.js"; import ProductCreatePage from "./paths/instance/products/create/index.js"; import ProductListPage from "./paths/instance/products/list/index.js"; import ProductUpdatePage from "./paths/instance/products/update/index.js"; @@ -63,10 +69,6 @@ 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, { - 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"; @@ -86,7 +88,10 @@ import { Notification } from "./utils/types.js"; export enum InstancePaths { error = "/error", settings = "/settings", - token = "/token", + password = "/password", + + access_token_list = "/access-token", + access_token_new = "/access-token/new", bank_list = "/bank", bank_update = "/bank/:bid/update", @@ -329,8 +334,8 @@ export function Routing(_p: Props): VNode { * Update instance page */} <Route - path={InstancePaths.token} - component={TokenPage} + path={InstancePaths.password} + component={PasswordPage} onChange={() => { route(`/`); }} @@ -438,6 +443,26 @@ export function Routing(_p: Props): VNode { }} /> {/** + * Sessions pages + */} + <Route + path={InstancePaths.access_token_list} + component={AccessTokenListPage} + onCreate={() => { + route(InstancePaths.access_token_new); + }} + /> + <Route + path={InstancePaths.access_token_new} + component={AccessTokenCreatePage} + onConfirm={() => { + route(InstancePaths.access_token_list); + }} + onBack={() => { + route(InstancePaths.access_token_list); + }} + /> + {/** * Order pages */} <Route diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx @@ -248,7 +248,7 @@ export function Sidebar({ mobile }: Props): VNode { </a> </li> <li> - <a href={"#/token"} class="has-icon"> + <a href={"#/password"} class="has-icon"> <span class="icon"> <i class="mdi mdi-security" /> </span> @@ -257,6 +257,16 @@ export function Sidebar({ mobile }: Props): VNode { </span> </a> </li> + <li> + <a href={"#/access-token"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-security" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>Access Tokens</i18n.Translate> + </span> + </a> + </li> </ul> </Fragment> ) : undefined} diff --git a/packages/merchant-backoffice-ui/src/hooks/access-tokens.ts b/packages/merchant-backoffice-ui/src/hooks/access-tokens.ts @@ -0,0 +1,69 @@ +/* + 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/> + */ + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import { + AccessToken, + TalerHttpError, + TalerMerchantManagementResultByMethod, +} from "@gnu-taler/taler-util"; +import _useSWR, { SWRHook, mutate } from "swr"; +import { useSessionContext } from "../context/session.js"; +import { buildPaginatedResult } from "@gnu-taler/web-util/browser"; +import { PAGINATED_LIST_REQUEST } from "../utils/constants.js"; +import { useState } from "preact/hooks"; +const useSWR = _useSWR as unknown as SWRHook; + +export interface InstanceBankAccountFilter {} + +export function revalidateInstanceAccessTokens() { + return mutate( + (key) => + Array.isArray(key) && key[key.length - 1] === "useInstanceAccessTokens", + undefined, + { revalidate: true }, + ); +} +export function useInstanceAccessTokens() { + const { state, lib } = useSessionContext(); + + const [offset, setOffset] = useState<number | undefined>(); + + async function fetcher([token, tid]: [AccessToken, string]) { + return await lib.instance.listAccessTokens(token, { + limit: PAGINATED_LIST_REQUEST, + offset: tid, + order: "dec", + }); + } + + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"listAccessTokens">, + TalerHttpError + >([state.token, "offset", "listAccessTokens"], fetcher); + + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; + + return buildPaginatedResult( + data.body.tokens, + offset, + setOffset, + (d) => d.row_id, + PAGINATED_LIST_REQUEST, + ); +} diff --git a/packages/merchant-backoffice-ui/src/hooks/order.ts b/packages/merchant-backoffice-ui/src/hooks/order.ts @@ -16,14 +16,17 @@ import { PAGINATED_LIST_REQUEST } from "../utils/constants.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import { AbsoluteTime, AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; +import { + AbsoluteTime, + AccessToken, + TalerHttpError, + TalerMerchantManagementResultByMethod, +} from "@gnu-taler/taler-util"; import _useSWR, { SWRHook, mutate } from "swr"; import { useSessionContext } from "../context/session.js"; -import { buildPaginatedResult } from "./webhooks.js"; +import { buildPaginatedResult } from "@gnu-taler/web-util/browser"; const useSWR = _useSWR as unknown as SWRHook; - - export function revalidateOrderDetails() { return mutate( (key) => Array.isArray(key) && key[key.length - 1] === "getOrderDetails", @@ -65,13 +68,20 @@ export function revalidateInstanceOrders() { } export function useInstanceOrders( args?: InstanceOrderFilter, - updatePosition: (d: string | undefined) => void = () => { }, + updatePosition: (d: string | undefined) => void = () => {}, ) { const { state, lib } = useSessionContext(); // const [offset, setOffset] = useState<string | undefined>(args?.position); - async function fetcher([token, o, p, r, w, d]: [AccessToken, string, boolean, boolean, boolean, AbsoluteTime]) { + async function fetcher([token, o, p, r, w, d]: [ + AccessToken, + string, + boolean, + boolean, + boolean, + AbsoluteTime, + ]) { return await lib.instance.listOrders(token, { limit: PAGINATED_LIST_REQUEST, offset: o, @@ -86,11 +96,28 @@ export function useInstanceOrders( const { data, error } = useSWR< TalerMerchantManagementResultByMethod<"listOrders">, TalerHttpError - >([state.token, args?.position, args?.paid, args?.refunded, args?.wired, args?.date, "listOrders"], fetcher); + >( + [ + state.token, + args?.position, + args?.paid, + args?.refunded, + args?.wired, + args?.date, + "listOrders", + ], + fetcher, + ); if (error) return error; if (data === undefined) return undefined; if (data.type !== "ok") return data; - return buildPaginatedResult(data.body.orders, args?.position, updatePosition, (d) => String(d.row_id)) + return buildPaginatedResult( + data.body.orders, + args?.position, + updatePosition, + (d) => String(d.row_id), + PAGINATED_LIST_REQUEST, + ); } diff --git a/packages/merchant-backoffice-ui/src/hooks/product.ts b/packages/merchant-backoffice-ui/src/hooks/product.ts @@ -28,7 +28,7 @@ import { useState } from "preact/hooks"; import _useSWR, { SWRHook, mutate } from "swr"; import { useSessionContext } from "../context/session.js"; import { PAGINATED_LIST_REQUEST } from "../utils/constants.js"; -import { buildPaginatedResult } from "./webhooks.js"; +import { buildPaginatedResult } from "@gnu-taler/web-util/browser"; const useSWR = _useSWR as unknown as SWRHook; export type ProductWithId = TalerMerchantApi.ProductDetail & { @@ -89,6 +89,7 @@ export function useInstanceProducts() { offset, setOffset, (d) => d.serial, + PAGINATED_LIST_REQUEST ); } diff --git a/packages/merchant-backoffice-ui/src/hooks/transfer.ts b/packages/merchant-backoffice-ui/src/hooks/transfer.ts @@ -16,10 +16,14 @@ import { PAGINATED_LIST_REQUEST } from "../utils/constants.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; +import { + AccessToken, + TalerHttpError, + TalerMerchantManagementResultByMethod, +} from "@gnu-taler/taler-util"; import _useSWR, { SWRHook, mutate } from "swr"; import { useSessionContext } from "../context/session.js"; -import { buildPaginatedResult } from "./webhooks.js"; +import { buildPaginatedResult } from "@gnu-taler/web-util/browser"; const useSWR = _useSWR as unknown as SWRHook; export interface InstanceTransferFilter { @@ -37,13 +41,18 @@ export function revalidateInstanceTransfers() { } export function useInstanceTransfers( args?: InstanceTransferFilter, - updatePosition: (id: string | undefined) => void = (() => { }), + updatePosition: (id: string | undefined) => void = () => {}, ) { const { state, lib } = useSessionContext(); // const [offset, setOffset] = useState<string | undefined>(args?.position); - async function fetcher([token, o, p, v]: [AccessToken, string, string, boolean]) { + async function fetcher([token, o, p, v]: [ + AccessToken, + string, + string, + boolean, + ]) { return await lib.instance.listWireTransfers(token, { paytoURI: p, verified: v, @@ -56,12 +65,26 @@ export function useInstanceTransfers( const { data, error } = useSWR< TalerMerchantManagementResultByMethod<"listWireTransfers">, TalerHttpError - >([state.token, args?.position, args?.payto_uri, args?.verified, "listWireTransfers"], fetcher); + >( + [ + state.token, + args?.position, + args?.payto_uri, + args?.verified, + "listWireTransfers", + ], + fetcher, + ); if (error) return error; if (data === undefined) return undefined; if (data.type !== "ok") return data; - return buildPaginatedResult(data.body.transfers, args?.position, updatePosition, (d) => String(d.transfer_serial_id)) - + return buildPaginatedResult( + data.body.transfers, + args?.position, + updatePosition, + (d) => String(d.transfer_serial_id), + PAGINATED_LIST_REQUEST, + ); } diff --git a/packages/merchant-backoffice-ui/src/hooks/webhooks.ts b/packages/merchant-backoffice-ui/src/hooks/webhooks.ts @@ -61,43 +61,6 @@ export function useInstanceWebhooks() { return data; } -type PaginatedResult<T> = OperationOk<T> & { - isLastPage: boolean; - isFirstPage: boolean; - loadNext(): void; - loadFirst(): void; -}; - -//TODO: consider sending this to web-util -export function buildPaginatedResult<R, OffId>( - data: R[], - offset: OffId | undefined, - setOffset: (o: OffId | undefined) => void, - getId: (r: R) => OffId, -): PaginatedResult<R[]> { - const isLastPage = data.length < PAGINATED_LIST_REQUEST; - const isFirstPage = offset === undefined; - - const result = structuredClone(data); - if (result.length == PAGINATED_LIST_REQUEST) { - result.pop(); - } - return { - type: "ok", - case: "ok", - body: result, - isLastPage, - isFirstPage, - loadNext: () => { - if (!result.length) return; - const id = getId(result[result.length - 1]); - setOffset(id); - }, - loadFirst: () => { - setOffset(undefined); - }, - }; -} export function revalidateWebhookDetails() { return mutate( diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/Create.stories.tsx @@ -0,0 +1,27 @@ +/* + 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/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { CreatePage as TestedComponent } from "./CreatePage.js"; + +export default { + title: "Pages/AcessToken/Create", + component: TestedComponent, +}; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/CreatePage.tsx @@ -0,0 +1,170 @@ +/* + 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/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + assertUnreachable, + LoginTokenScope, + TalerMerchantApi, +} from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; +import { + FormErrors, + FormProvider, + TalerForm, +} from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { InputDuration } from "../../../../components/form/InputDuration.js"; +import { InputSelector } from "../../../../components/form/InputSelector.js"; +import { undefinedIfEmpty } from "../../../../utils/table.js"; + +type Entity = TalerMerchantApi.LoginTokenRequest & { + password: string; +} & TalerForm; + +interface Props { + onCreate: ( + pwd: string, + d: TalerMerchantApi.LoginTokenRequest, + ) => Promise<void>; + onBack?: () => void; +} + +const VALID_TOKEN_SCOPE = [ + LoginTokenScope.All, + LoginTokenScope.OrderFull, + LoginTokenScope.OrderManagement, + LoginTokenScope.OrderPos, + LoginTokenScope.OrderSimple, + LoginTokenScope.ReadOnly, +]; + +export function CreatePage({ onCreate, onBack }: Props): VNode { + const { i18n } = useTranslationContext(); + + const [state, setState] = useState<Partial<Entity>>({}); + + const errors = undefinedIfEmpty<FormErrors<Entity>>({ + password: !state.password ? i18n.str`Required` : undefined, + duration: !state.duration ? i18n.str`Required` : undefined, + scope: !state.scope ? i18n.str`Required` : undefined, + }); + + const hasErrors = errors !== undefined; + + const submitForm = () => { + if (hasErrors) return Promise.reject(); + + return onCreate(state.password!, { + scope: state.scope!, + duration: state.duration!, + }); + }; + + return ( + <Fragment> + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <FormProvider + object={state} + valueHandler={setState} + errors={errors} + > + <Input<Entity> + name="password" + inputType="password" + label={i18n.str`Password`} + /> + + <InputDuration<Entity> + name="duration" + label={i18n.str`Duration`} + withForever + tooltip={i18n.str`Time the acess token will be valid.`} + /> + <InputSelector + name="scope" + label={i18n.str`Scope`} + tooltip={i18n.str`Choose the authentication type for the account info URL`} + values={VALID_TOKEN_SCOPE} + toStr={(str) => { + const s = str as LoginTokenScope; + switch (s) { + case LoginTokenScope.ReadOnly: + return i18n.str`Read only`; + case LoginTokenScope.All: + return i18n.str`All`; + case LoginTokenScope.OrderSimple: + return i18n.str`Order Simple`; + case LoginTokenScope.OrderPos: + return i18n.str`Order Point-of-Sale`; + case LoginTokenScope.OrderManagement: + return i18n.str`Order Management`; + case LoginTokenScope.OrderFull: + return i18n.str`Order Full`; + case LoginTokenScope.ReadOnly_Refreshable: + return i18n.str`Read only (refreshable)`; + case LoginTokenScope.All_Refreshable: + return i18n.str`All (refreshable)`; + case LoginTokenScope.OrderSimple_Refreshable: + return i18n.str`Order Simple (refreshable)`; + case LoginTokenScope.OrderPos_Refreshable: + return i18n.str`Order Point-of-Sale (refreshable)`; + case LoginTokenScope.OrderManagement_Refreshable: + return i18n.str`Order Management (refreshable)`; + case LoginTokenScope.OrderFull_Refreshable: + return i18n.str`Order Full (refreshable)`; + default: { + assertUnreachable(s); + } + } + }} + /> + </FormProvider> + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + disabled={hasErrors} + data-tooltip={ + hasErrors + ? i18n.str`Need to complete marked fields` + : i18n.str`Confirm operation` + } + onClick={submitForm} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </div> + <div class="column" /> + </div> + </section> + </Fragment> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/index.tsx @@ -0,0 +1,78 @@ +/* + 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/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + TalerMerchantApi +} from "@gnu-taler/taler-util"; +import { + useTranslationContext +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { Notification } from "../../../../utils/types.js"; +import { CreatePage } from "./CreatePage.js"; + +export type Entity = TalerMerchantApi.LoginTokenRequest; +interface Props { + onBack?: () => void; + onConfirm: () => void; +} + +export default function AccessTokenCreatePage({ onConfirm, onBack }: Props): VNode { + const { state, lib } = useSessionContext(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); + + return ( + <> + <NotificationCard notification={notif} /> + <CreatePage + onBack={onBack} + onCreate={async (pwd: string, request: Entity) => { + return lib.instance + .createAccessToken(state.instance, pwd, request) + .then((resp) => { + if (resp.type === "fail") { + setNotif({ + message: i18n.str`Could not create access token`, + type: "ERROR", + description: resp.detail?.hint, + }); + return; + } + onConfirm(); + }) + .catch((error) => { + setNotif({ + message: i18n.str`Could not create access token`, + type: "ERROR", + description: + error instanceof Error ? error.message : String(error), + }); + }); + }} + /> + </> + ); +} + diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/list/Table.tsx @@ -0,0 +1,179 @@ +/* + 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/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + parsePaytoUri, + PaytoType, + PaytoUri, + TokenInfo, + TokenInfos +} from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { StateUpdater, useState } from "preact/hooks"; + +type Entity = TokenInfo; + +interface Props { + tokens: Entity[]; + onSelect: (e: Entity) => void; + onCreate: () => void; +} + +export function CardTable({ + tokens, + onCreate, + onSelect, +}: Props): VNode { + const [rowSelection, rowSelectionHandler] = useState<string[]>([]); + + const { i18n } = useTranslationContext(); + + return ( + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-bank" /> + </span> + <i18n.Translate>Active sessions</i18n.Translate> + </p> + <div class="card-header-icon" aria-label="more options"> + <span + class="has-tooltip-left" + data-tooltip={i18n.str`Create access token`} + > + <button class="button is-info" type="button" onClick={onCreate}> + <span class="icon is-small"> + <i class="mdi mdi-plus mdi-36px" /> + </span> + </button> + </span> + </div> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {tokens.length > 0 ? ( + <Table + tokens={tokens} + onSelect={onSelect} + rowSelection={rowSelection} + rowSelectionHandler={rowSelectionHandler} + /> + ) : ( + <EmptyTable /> + )} + </div> + </div> + </div> + </div> + ); +} +interface TableProps { + rowSelection: string[]; + tokens: Entity[]; + onSelect: (e: Entity) => void; + rowSelectionHandler: StateUpdater<string[]>; +} + +function Table({ tokens, onSelect }: TableProps): VNode { + const { i18n } = useTranslationContext(); + return ( + <Fragment> + <div class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>Scope</i18n.Translate> + </th> + <th> + <i18n.Translate>Created at</i18n.Translate> + </th> + <th> + <i18n.Translate>Last access</i18n.Translate> + </th> + <th> + <i18n.Translate>Valid until</i18n.Translate> + </th> + <th> + <i18n.Translate>Description</i18n.Translate> + </th> + <th /> + </tr> + </thead> + <tbody> + {tokens.map((t, idx) => { + return ( + <tr key={idx}> + <td + onClick={(): void => onSelect(t)} + style={{ cursor: "pointer" }} + > + {t.scope} + </td> + <td + onClick={(): void => onSelect(t)} + style={{ cursor: "pointer" }} + > + {t.creation_time} + </td> + <td + onClick={(): void => onSelect(t)} + style={{ cursor: "pointer" }} + > + {t.last_access} + </td> + <td + onClick={(): void => onSelect(t)} + style={{ cursor: "pointer" }} + > + {t.expiration} + </td> + </tr> + ); + })} + + </tbody> + </table> + </div> + </Fragment> + ); +} + +function EmptyTable(): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="content has-text-grey has-text-centered"> + <p> + <span class="icon is-large"> + <i class="mdi mdi-magnify mdi-48px" /> + </span> + </p> + <p> + <i18n.Translate> + There are no acitve sessions yet, add one by pressing the + sign + </i18n.Translate> + </p> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/list/index.tsx @@ -0,0 +1,81 @@ +/* + 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/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + HttpStatusCode, + TalerError, + assertUnreachable, +} from "@gnu-taler/taler-util"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; +import { Loading } from "../../../../components/exception/loading.js"; +import { useInstanceAccessTokens } from "../../../../hooks/access-tokens.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; +import { CardTable } from "./Table.js"; + +interface Props { + onCreate: () => void; +} + +export default function AccessTokenListPage({ onCreate }: Props): VNode { + const result = useInstanceAccessTokens(); + const [selected, setSelected] = useState<number>(); + if (!result) return <Loading />; + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} />; + } + if (result.type === "fail") { + switch (result.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + case HttpStatusCode.Unauthorized: { + return <LoginPage />; + } + case HttpStatusCode.Forbidden: { + return <LoginPage />; + } + default: { + assertUnreachable(result); + } + } + } + + return ( + <Fragment> + <section class="section is-main-section"> + {!selected ? undefined : <div>selected</div>} + <CardTable + tokens={result.body.map((o) => ({ + ...o, + id: String(o.row_id), + }))} + onCreate={onCreate} + onSelect={(e) => { + setSelected(e.row_id); + }} + /> + </section> + </Fragment> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx @@ -117,8 +117,8 @@ export default function OrderList({ onCreate, onSelect }: Props): VNode { <ListPage orders={result.body.map((o) => ({ ...o, id: o.order_id }))} - onLoadMoreBefore={result.isFirstPage ? undefined : result.loadFirst} - onLoadMoreAfter={result.isLastPage ? undefined : result.loadNext} + onLoadMoreBefore={result.loadFirst} + onLoadMoreAfter={result.loadNext} onSelectOrder={(order) => onSelect(order.id)} onRefundOrder={(value) => setOrderToBeRefunded(value)} isAllActive={isAllActive} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/password/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/password/DetailPage.tsx @@ -0,0 +1,154 @@ +/* + 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/> + */ + +/** + * + * @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"; +import { AsyncButton } from "../../../components/exception/AsyncButton.js"; +import { FormProvider } from "../../../components/form/FormProvider.js"; +import { Input } from "../../../components/form/Input.js"; +import { undefinedIfEmpty } from "../../../utils/table.js"; + +interface Props { + instanceId: string; + hasPassword: boolean | undefined; + onNewPassword: (c: string | undefined, s: string) => void; + onBack?: () => void; +} + +export function DetailPage({ + instanceId, + hasPassword, + onBack, + onNewPassword, +}: Props): VNode { + type State = { old_token: string; new_token: string; repeat_token: string }; + const [form, setValue] = useState<Partial<State>>({ + old_token: "", + new_token: "", + repeat_token: "", + }); + const { i18n } = useTranslationContext(); + + const errors = undefinedIfEmpty({ + old_token: + hasPassword && !form.old_token + ? i18n.str`You need your password to perform the operation` + : undefined, + new_token: !form.new_token + ? i18n.str`Required` + : form.new_token === form.old_token + ? i18n.str`Can't be the same as the old password` + : undefined, + repeat_token: + form.new_token !== form.repeat_token + ? i18n.str`Is not the same` + : undefined, + }); + + const hasErrors = errors !== undefined; + + const text = i18n.str`You are updating the password for the instance with ID "${instanceId}"`; + + async function submitForm() { + if (hasErrors) return; + const oldToken = + form.old_token !== undefined && hasPassword ? form.old_token : undefined; + const newToken = form.new_token!; + onNewPassword(oldToken, newToken); + } + + return ( + <div> + <section class="section"> + <section class="hero is-hero-bar"> + <div class="hero-body"> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <span class="is-size-4">{text}</span> + </div> + </div> + </div> + </div> + </section> + <hr /> + + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <FormProvider errors={errors} object={form} valueHandler={setValue}> + <Fragment> + {hasPassword && ( + <Fragment> + <Input<State> + name="old_token" + label={i18n.str`Current password`} + tooltip={i18n.str`Password currently in use`} + inputType="password" + /> + </Fragment> + )} + + <Input<State> + name="new_token" + label={i18n.str`New password`} + tooltip={i18n.str`Next password to be used`} + inputType="password" + /> + <Input<State> + name="repeat_token" + label={i18n.str`Repeat password`} + tooltip={i18n.str`Confirm the same password`} + inputType="password" + /> + </Fragment> + <div class="buttons is-right mt-5"> + {onBack && ( + <a class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </a> + )} + <AsyncButton + type="submit" + disabled={hasErrors} + data-tooltip={ + hasErrors + ? i18n.str`Need to complete marked fields` + : i18n.str`Confirm operation` + } + onClick={submitForm} + > + <i18n.Translate>Confirm change</i18n.Translate> + </AsyncButton> + </div> + </FormProvider> + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/password/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/password/index.tsx @@ -0,0 +1,210 @@ +/* + 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 { + HttpStatusCode, + MerchantAuthMethod, + TalerError, + TalerMerchantManagementResultByMethod, + assertUnreachable +} from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../components/ErrorLoadingMerchant.js"; +import { Loading } from "../../../components/exception/loading.js"; +import { NotificationCard } from "../../../components/menu/index.js"; +import { useSessionContext } from "../../../context/session.js"; +import { + useInstanceDetails, + useManagedInstanceDetails, +} from "../../../hooks/instance.js"; +import { Notification } from "../../../utils/types.js"; +import { + FOREVER_REFRESHABLE_TOKEN, + LoginPage, + TEMP_TEST_TOKEN, +} from "../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../notfound/index.js"; +import { DetailPage } from "./DetailPage.js"; + +export interface Props { + onChange: () => void; + onCancel: () => void; +} + +export default function PasswordPage(props: Props): VNode { + const { lib, state, logIn } = useSessionContext(); + const result = useInstanceDetails(); + const instanceId = state.instance; + + async function changePassword( + currentPassword: string | undefined, + newPassword: string, + ) { + if (currentPassword) { + const resp = await lib.instance.createAccessToken( + instanceId, + currentPassword, + TEMP_TEST_TOKEN, + ); + if (resp.type !== "ok") { + throw Error(resp.detail?.hint ?? "The current password is wrong"); + } + } + + { + const resp = await lib.instance.updateCurrentInstanceAuthentication( + state.token, + { + password: newPassword, + method: MerchantAuthMethod.TOKEN, + }, + ); + if (resp.type === "fail") { + throw Error(resp.detail?.hint ?? "The request failed"); + } + } + + const resp = await lib.instance.createAccessToken( + instanceId, + newPassword, + FOREVER_REFRESHABLE_TOKEN, + ); + if (resp.type === "ok") { + logIn(state.instance, resp.body.access_token); + return; + } else { + throw Error(resp.detail?.hint ?? "The new login failed"); + } + } + + return CommonPassword({ ...props, instanceId }, result, changePassword); +} + +export function AdminPassword(props: Props & { instanceId: string }): VNode { + const { lib, state } = useSessionContext(); + + const subInstanceLib = lib.subInstanceApi(props.instanceId).instance; + const result = useManagedInstanceDetails(props.instanceId); + + const instanceId = props.instanceId; + + async function changePassword( + currentPassword: string | undefined, + newPassword: string, + ) { + if (currentPassword) { + const resp = await lib.instance.createAccessToken( + instanceId, + currentPassword, + TEMP_TEST_TOKEN, + ); + if (resp.type !== "ok") { + throw Error(resp.detail?.hint ?? "The current password is wrong"); + } + } + + { + const resp = await lib.instance.updateInstanceAuthentication( + state.token, + props.instanceId, + { + password: newPassword, + method: MerchantAuthMethod.TOKEN, + }, + ); + if (resp.type === "fail") { + throw Error(resp.detail?.hint ?? "The request failed"); + } + } + const resp = await subInstanceLib.createAccessToken( + instanceId, + newPassword, + FOREVER_REFRESHABLE_TOKEN, + ); + if (resp.type === "ok") { + return; + } else { + throw Error(resp.detail?.hint ?? "The new login failed"); + } + } + return CommonPassword(props, result, changePassword); +} + +function CommonPassword( + { onChange, onCancel, instanceId }: Props & { instanceId: string }, + result: + | TalerMerchantManagementResultByMethod<"getInstanceDetails"> + | TalerError + | undefined, + onNewPassword: ( + oldToken: string | undefined, + newToken: string, + ) => Promise<void>, +): VNode { + const { i18n } = useTranslationContext(); + const { state } = useSessionContext(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + + if (!result) return <Loading />; + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} />; + } + if (result.type === "fail") { + switch (result.case) { + case HttpStatusCode.Unauthorized: { + return <LoginPage />; + } + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + default: { + assertUnreachable(result); + } + } + } + + const adminChangingPwdForAnotherInstance = + state.isAdmin && state.instance !== instanceId; + const hasToken = + result.body.auth.method === MerchantAuthMethod.TOKEN && + !adminChangingPwdForAnotherInstance; + + const id = result.body.name; + return ( + <Fragment> + <NotificationCard notification={notif} /> + <DetailPage + onBack={onCancel} + instanceId={result.body.name} + hasPassword={hasToken} + onNewPassword={async (currentPassword, newPassword): Promise<void> => { + try { + await onNewPassword(currentPassword, newPassword); + return onChange(); + } catch (error) { + return setNotif({ + message: i18n.str`Failed to set new password`, + type: "ERROR", + description: + error instanceof Error ? error.message : String(error), + }); + } + }} + /> + </Fragment> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/password/stories.tsx diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx @@ -89,8 +89,8 @@ export default function ProductList({ onCreate, onSelect }: Props): VNode { <CardTable instances={result.body} - onLoadMoreBefore={result.isFirstPage ? undefined : result.loadFirst} - onLoadMoreAfter={result.isLastPage ? undefined : result.loadNext} + onLoadMoreBefore={result.loadFirst} + onLoadMoreAfter={result.loadNext} onCreate={onCreate} onUpdate={async (id, prod) => { try { diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx @@ -1,167 +0,0 @@ -/* - 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/> - */ - -/** - * - * @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"; -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 { undefinedIfEmpty } from "../../../utils/table.js"; - -interface Props { - instanceId: string; - hasToken: boolean | undefined; - onNewToken: (c: AccessToken | undefined, s: AccessToken) => void; - onBack?: () => void; -} - -export function DetailPage({ - instanceId, - hasToken, - onBack, - onNewToken, -}: Props): VNode { - type State = { old_token: string; new_token: string; repeat_token: string }; - const [form, setValue] = useState<Partial<State>>({ - old_token: "", - new_token: "", - repeat_token: "", - }); - const { i18n } = useTranslationContext(); - - const errors = undefinedIfEmpty({ - old_token: - hasToken && !form.old_token - ? i18n.str`You need your password to perform the operation` - : undefined, - new_token: !form.new_token - ? i18n.str`Required` - : form.new_token === form.old_token - ? i18n.str`Can't be the same as the old password` - : undefined, - repeat_token: - form.new_token !== form.repeat_token - ? i18n.str`Is not the same` - : undefined, - }); - - const hasErrors = errors !== undefined; - - const text = i18n.str`You are updating the password for the instance with ID "${instanceId}"`; - - async function submitForm() { - if (hasErrors) return; - const oldToken = - form.old_token !== undefined && hasToken - ? createRFC8959AccessTokenPlain(form.old_token) - : undefined; - const newToken = createRFC8959AccessTokenPlain(form.new_token!); - onNewToken(oldToken, newToken); - } - - return ( - <div> - <section class="section"> - <section class="hero is-hero-bar"> - <div class="hero-body"> - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <span class="is-size-4">{text}</span> - </div> - </div> - </div> - </div> - </section> - <hr /> - - {/* {!hasToken && ( - <NotificationCard - notification={{ - message: i18n.str`This instance does not have authentication password.`, - description: i18n.str`You can leave it empty if there is another layer of security.`, - type: "WARN", - }} - /> - )} */} - - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - <FormProvider errors={errors} object={form} valueHandler={setValue}> - <Fragment> - {hasToken && ( - <Fragment> - <Input<State> - name="old_token" - label={i18n.str`Current password`} - tooltip={i18n.str`Password currently in use`} - inputType="password" - /> - </Fragment> - )} - - <Input<State> - name="new_token" - label={i18n.str`New password`} - tooltip={i18n.str`Next password to be used`} - inputType="password" - /> - <Input<State> - name="repeat_token" - label={i18n.str`Repeat password`} - tooltip={i18n.str`Confirm the same password`} - inputType="password" - /> - </Fragment> - <div class="buttons is-right mt-5"> - {onBack && ( - <a class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </a> - )} - <AsyncButton - type="submit" - disabled={hasErrors} - data-tooltip={ - hasErrors - ? i18n.str`Need to complete marked fields` - : i18n.str`Confirm operation` - } - onClick={submitForm} - > - <i18n.Translate>Confirm change</i18n.Translate> - </AsyncButton> - </div> - </FormProvider> - </div> - <div class="column" /> - </div> - </section> - </div> - ); -} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx @@ -1,208 +0,0 @@ -/* - 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, - HttpStatusCode, - MerchantAuthMethod, - TalerError, - TalerMerchantInstanceHttpClient, - TalerMerchantManagementResultByMethod, - assertUnreachable, -} from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; -import { useState } from "preact/hooks"; -import { ErrorLoadingMerchant } from "../../../components/ErrorLoadingMerchant.js"; -import { Loading } from "../../../components/exception/loading.js"; -import { NotificationCard } from "../../../components/menu/index.js"; -import { useSessionContext } from "../../../context/session.js"; -import { - useInstanceDetails, - useManagedInstanceDetails, -} from "../../../hooks/instance.js"; -import { Notification } from "../../../utils/types.js"; -import { FOREVER_REFRESHABLE_TOKEN, LoginPage, TEMP_TEST_TOKEN } from "../../login/index.js"; -import { NotFoundPageOrAdminCreate } from "../../notfound/index.js"; -import { DetailPage } from "./DetailPage.js"; - -export interface Props { - onChange: () => void; - onCancel: () => void; -} - -export default function Token(props: Props): VNode { - const { lib, state, logIn } = useSessionContext(); - const result = useInstanceDetails(); - const instanceId = state.instance; - - async function changeToken( - currentPassword: AccessToken | undefined, - newPassword: AccessToken, - ) { - if (currentPassword) { - const resp = await lib.instance.createAccessToken( - instanceId, - currentPassword, - TEMP_TEST_TOKEN, - ); - if (resp.type !== "ok") { - throw Error(resp.detail?.hint ?? "The current password is wrong"); - } - } - - { - const resp = await lib.instance.updateCurrentInstanceAuthentication( - state.token, - { - password: newPassword, - method: MerchantAuthMethod.TOKEN, - }, - ); - if (resp.type === "fail") { - throw Error(resp.detail?.hint ?? "The request failed"); - } - } - - const resp = await lib.instance.createAccessToken( - instanceId, - newPassword, - FOREVER_REFRESHABLE_TOKEN, - ); - if (resp.type === "ok") { - logIn(state.instance, resp.body.access_token); - return; - } else { - throw Error(resp.detail?.hint ?? "The new login failed"); - } - } - - return CommonToken({ ...props, instanceId }, result, changeToken); -} - -export function AdminToken(props: Props & { instanceId: string }): VNode { - const { lib, state } = useSessionContext(); - - const subInstanceLib = lib.subInstanceApi(props.instanceId).instance; - const result = useManagedInstanceDetails(props.instanceId); - - const instanceId = props.instanceId; - - async function changeToken( - currentPassword: AccessToken | undefined, - newPassword: AccessToken, - ) { - if (currentPassword) { - const resp = await lib.instance.createAccessToken( - instanceId, - currentPassword, - TEMP_TEST_TOKEN, - ); - if (resp.type !== "ok") { - throw Error(resp.detail?.hint ?? "The current password is wrong"); - } - } - - { - const resp = await lib.instance.updateInstanceAuthentication( - state.token, - props.instanceId, - { - password: newPassword, - method: MerchantAuthMethod.TOKEN, - }, - ); - if (resp.type === "fail") { - throw Error(resp.detail?.hint ?? "The request failed"); - } - } - const resp = await subInstanceLib.createAccessToken( - instanceId, - newPassword, - FOREVER_REFRESHABLE_TOKEN, - ); - if (resp.type === "ok") { - return; - } else { - throw Error(resp.detail?.hint ?? "The new login failed"); - } - } - return CommonToken(props, result, changeToken); -} - -function CommonToken( - { onChange, onCancel, instanceId }: Props & { instanceId: string }, - result: - | TalerMerchantManagementResultByMethod<"getInstanceDetails"> - | TalerError - | undefined, - onNewToken: ( - oldToken: AccessToken | undefined, - newToken: AccessToken, - ) => Promise<void>, -): VNode { - const { i18n } = useTranslationContext(); - const { state } = useSessionContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - - if (!result) return <Loading />; - if (result instanceof TalerError) { - return <ErrorLoadingMerchant error={result} />; - } - if (result.type === "fail") { - switch (result.case) { - case HttpStatusCode.Unauthorized: { - return <LoginPage />; - } - case HttpStatusCode.NotFound: { - return <NotFoundPageOrAdminCreate />; - } - default: { - assertUnreachable(result); - } - } - } - - const adminChangingPwdForAnotherInstance = - state.isAdmin && state.instance !== instanceId; - const hasToken = - result.body.auth.method === MerchantAuthMethod.TOKEN && - !adminChangingPwdForAnotherInstance; - - const id = result.body.name; - return ( - <Fragment> - <NotificationCard notification={notif} /> - <DetailPage - onBack={onCancel} - instanceId={result.body.name} - hasToken={hasToken} - onNewToken={async (currentToken, newToken): Promise<void> => { - try { - await onNewToken(currentToken, newToken); - return onChange(); - } catch (error) { - return setNotif({ - message: i18n.str`Failed to set new password`, - type: "ERROR", - description: - error instanceof Error ? error.message : String(error), - }); - } - }} - /> - </Fragment> - ); -} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx @@ -107,15 +107,21 @@ export default function ListTransfer({ onCreate }: Props): VNode { <ListPage accounts={accounts} transfers={result.body} - onLoadMoreBefore={result.isFirstPage ? undefined : result.loadFirst} - onLoadMoreAfter={result.isLastPage ? undefined : result.loadNext} + onLoadMoreBefore={result.loadFirst} + onLoadMoreAfter={result.loadNext} onCreate={onCreate} onDelete={async (transfer) => { try { - const resp = await lib.instance.deleteWireTransfer(state.token, String(transfer.transfer_serial_id)); + const resp = await lib.instance.deleteWireTransfer( + state.token, + String(transfer.transfer_serial_id), + ); if (resp.type === "ok") { setNotif({ - message: i18n.str`Wire transfer "${transfer.wtid.substring(0,16)}..." has been deleted`, + message: i18n.str`Wire transfer "${transfer.wtid.substring( + 0, + 16, + )}..." has been deleted`, type: "SUCCESS", }); } else { diff --git a/packages/taler-util/src/types-taler-common.ts b/packages/taler-util/src/types-taler-common.ts @@ -429,15 +429,7 @@ export const codecForTokenInfo = (): Codec<TokenInfo> => buildCodecForObject<TokenInfo>() .property("creation_time", codecForTimestamp) .property("expiration", codecForTimestamp) - .property( - "scope", - codecForEither( - codecForConstString("readonly"), - codecForConstString("readwrite"), - codecForConstString("revenue"), - codecForConstString("wiregateway"), - ), - ) + .property("scope", codecForString()) .property("refreshable", codecForBoolean()) .property("description", codecOptional(codecForString())) .property("last_access", codecForTimestamp) @@ -457,7 +449,6 @@ export const codecForTokenSuccessResponse = (): Codec<TokenSuccessResponse> => .property("expiration", codecForTimestamp) .build("TalerAuthentication.TokenSuccessResponse"); - // FIXME: implement this codec export const codecForURN = codecForString; diff --git a/packages/taler-util/src/types-taler-merchant.ts b/packages/taler-util/src/types-taler-merchant.ts @@ -3750,6 +3750,12 @@ export const codecForTokenScope = codecForEither( codecForConstString(LoginTokenScope.OrderPos), codecForConstString(LoginTokenScope.OrderSimple), codecForConstString(LoginTokenScope.ReadOnly), + codecForConstString(LoginTokenScope.All_Refreshable), + codecForConstString(LoginTokenScope.OrderFull_Refreshable), + codecForConstString(LoginTokenScope.OrderManagement_Refreshable), + codecForConstString(LoginTokenScope.OrderPos_Refreshable), + codecForConstString(LoginTokenScope.OrderSimple_Refreshable), + codecForConstString(LoginTokenScope.ReadOnly_Refreshable), ); export const codecForLoginTokenSuccessResponse =