summaryrefslogtreecommitdiff
path: root/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/auditor-backoffice-ui/src/InstanceRoutes.tsx')
-rw-r--r--packages/auditor-backoffice-ui/src/InstanceRoutes.tsx485
1 files changed, 485 insertions, 0 deletions
diff --git a/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx b/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx
new file mode 100644
index 000000000..163438654
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx
@@ -0,0 +1,485 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 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)
+ * @author Nic Eigel
+ */
+
+import {
+ useTranslationContext,
+ HttpError,
+ ErrorType,
+} from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { Fragment, FunctionComponent, h, VNode } from "preact";
+import { Route, route, Router } from "preact-router";
+import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
+import { Loading } from "./components/exception/loading.js";
+import { Menu, NotificationCard } from "./components/menu/index.js";
+import { useBackendContext } from "./context/backend.js";
+import { InstanceContextProvider } from "./context/instance.js";
+import {
+ useBackendDefaultToken,
+ useBackendInstanceToken,
+ useSimpleLocalStorage,
+} from "./hooks/index.js";
+import { useInstanceKYCDetails } from "./hooks/instance.js";
+import InstanceCreatePage from "./paths/admin/create/index.js";
+import InstanceListPage from "./paths/admin/list/index.js";
+import TokenPage from "./paths/instance/token/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";
+import OrderListPage from "./paths/instance/orders/list/index.js";
+import DepositConfirmationCreatePage from "./paths/instance/deposit_confirmations/create/index.js";
+import DepositConfirmationListPage from "./paths/instance/deposit_confirmations/list/index.js";
+import DepositConfirmationUpdatePage from "./paths/instance/deposit_confirmations/update/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";
+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 ReservesCreatePage from "./paths/instance/reserves/create/index.js";
+import ReservesDetailsPage from "./paths/instance/reserves/details/index.js";
+import ReservesListPage from "./paths/instance/reserves/list/index.js";
+import TemplateCreatePage from "./paths/instance/templates/create/index.js";
+import TemplateUsePage from "./paths/instance/templates/use/index.js";
+import TemplateQrPage from "./paths/instance/templates/qr/index.js";
+import TemplateListPage from "./paths/instance/templates/list/index.js";
+import TemplateUpdatePage from "./paths/instance/templates/update/index.js";
+import WebhookCreatePage from "./paths/instance/webhooks/create/index.js";
+import WebhookListPage from "./paths/instance/webhooks/list/index.js";
+import WebhookUpdatePage from "./paths/instance/webhooks/update/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 TransferCreatePage from "./paths/instance/transfers/create/index.js";
+import TransferListPage from "./paths/instance/transfers/list/index.js";
+import InstanceUpdatePage, {
+ AdminUpdate as InstanceAdminUpdatePage,
+ Props as InstanceUpdatePageProps,
+} from "./paths/instance/update/index.js";
+import { LoginPage } from "./paths/login/index.js";
+import NotFoundPage from "./paths/notfound/index.js";
+import { Notification } from "./utils/types.js";
+import { LoginToken, MerchantBackend } from "./declaration.js";
+import { Settings } from "./paths/settings/index.js";
+import { dateFormatForSettings, useSettings } from "./hooks/useSettings.js";
+
+export enum InstancePaths {
+ error = "/error",
+ settings = "/settings",
+ token = "/token",
+
+ inventory_list = "/inventory",
+ inventory_update = "/inventory/:pid/update",
+ inventory_new = "/inventory/new",
+
+ deposit_confirmation_list = "/deposit-confirmation",
+ deposit_confirmation_update = "/deposit-confirmation/:pid/update",
+ deposit_confirmation_new = "/deposit-confirmation/new",
+
+ interface = "/interface",
+}
+
+// eslint-disable-next-line @typescript-eslint/no-empty-function
+const noop = () => { };
+
+export enum AdminPaths {
+ list_instances = "/instances",
+ new_instance = "/instance/new",
+ update_instance = "/instance/:id/update",
+}
+
+export interface Props {
+ id: string;
+ admin?: boolean;
+ path: string;
+ onUnauthorized: () => void;
+ onLoginPass: () => void;
+ setInstanceName: (s: string) => void;
+}
+
+export function InstanceRoutes({
+ id,
+ admin,
+ path,
+ // onUnauthorized,
+ onLoginPass,
+ setInstanceName,
+}: Props): VNode {
+ const [defaultToken, updateDefaultToken] = useBackendDefaultToken();
+ const [token, updateToken] = useBackendInstanceToken(id);
+ const { i18n } = useTranslationContext();
+
+ type GlobalNotifState = (Notification & { to: string }) | undefined;
+ const [globalNotification, setGlobalNotification] =
+ useState<GlobalNotifState>(undefined);
+
+ const changeToken = (token?: LoginToken) => {
+ if (admin) {
+ updateToken(token);
+ } else {
+ updateDefaultToken(token);
+ }
+ onLoginPass()
+ };
+ // const updateLoginStatus = (url: string, token?: string) => {
+ // changeToken(token);
+ // };
+
+ const value = useMemo(
+ () => ({ id, token, admin, changeToken }),
+ [id, token, admin],
+ );
+
+ function ServerErrorRedirectTo(to: InstancePaths | AdminPaths) {
+ return function ServerErrorRedirectToImpl(
+ error: HttpError<MerchantBackend.ErrorDetail>,
+ ) {
+ if (error.type === ErrorType.TIMEOUT) {
+ setGlobalNotification({
+ message: i18n.str`The request to the backend take too long and was cancelled`,
+ description: i18n.str`Diagnostic from ${error.info.url} is "${error.message}"`,
+ type: "ERROR",
+ to,
+ });
+ } else {
+ setGlobalNotification({
+ message: i18n.str`The backend reported a problem: HTTP status #${error.status}`,
+ description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`,
+ details:
+ error.type === ErrorType.CLIENT || error.type === ErrorType.SERVER
+ ? error.payload.detail
+ : undefined,
+ type: "ERROR",
+ to,
+ });
+ }
+ return <Redirect to={to} />;
+ };
+ }
+
+ // const LoginPageAccessDeniend = onUnauthorized
+ const LoginPageAccessDenied = () => {
+ return <Fragment>
+ <NotificationCard
+ notification={{
+ message: i18n.str`Access denied`,
+ description: i18n.str`Session expired or password changed.`,
+ type: "ERROR",
+ }}
+ />
+ <LoginPage onConfirm={changeToken} />
+ </Fragment>
+
+ }
+
+ function IfAdminCreateDefaultOr<T>(Next: FunctionComponent<any>) {
+ return function IfAdminCreateDefaultOrImpl(props?: T) {
+ if (admin && id === "default") {
+ return (
+ <Fragment>
+ <NotificationCard
+ notification={{
+ message: i18n.str`No 'default' instance configured yet.`,
+ description: i18n.str`Create a 'default' instance to begin using the merchant backoffice.`,
+ type: "INFO",
+ }}
+ />
+ </Fragment>
+ );
+ }
+ if (props) {
+ return <Next {...props} />;
+ }
+ return <Next />;
+ };
+ }
+
+ const clearTokenAndGoToRoot = () => {
+ route("/");
+ // clear all tokens
+ updateToken(undefined)
+ updateDefaultToken(undefined)
+ };
+
+ return (
+ <InstanceContextProvider value={value}>
+ <Menu
+ instance={id}
+ admin={admin}
+ onShowSettings={() => {
+ route(InstancePaths.interface)
+ }}
+ path={path}
+ onLogout={clearTokenAndGoToRoot}
+ setInstanceName={setInstanceName}
+ isPasswordOk={defaultToken !== undefined}
+ />
+ <KycBanner />
+ <NotificationCard notification={globalNotification} />
+
+ <Router
+ onChange={(e) => {
+ const movingOutFromNotification =
+ globalNotification && e.url !== globalNotification.to;
+ if (movingOutFromNotification) {
+ setGlobalNotification(undefined);
+ }
+ }}
+ >
+ {/**
+ * Admin pages
+ */}
+ {admin && (
+ <Route
+ path={AdminPaths.list_instances}
+ component={InstanceListPage}
+ onCreate={() => {
+ route(AdminPaths.new_instance);
+ }}
+ onUpdate={(id: string): void => {
+ route(`/instance/${id}/update`);
+ }}
+ setInstanceName={setInstanceName}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
+ />
+ )}
+ {admin && (
+ <Route
+ path={AdminPaths.update_instance}
+ component={AdminInstanceUpdatePage}
+ onBack={() => route(AdminPaths.list_instances)}
+ onConfirm={() => {
+ route(AdminPaths.list_instances);
+ }}
+ onUpdateError={ServerErrorRedirectTo(AdminPaths.list_instances)}
+ onLoadError={ServerErrorRedirectTo(AdminPaths.list_instances)}
+ onNotFound={NotFoundPage}
+ />
+ )}
+ {/**
+ * Update instance page
+ */}
+ <Route
+ path={InstancePaths.settings}
+ component={InstanceUpdatePage}
+ onBack={() => {
+ route(`/`);
+ }}
+ onConfirm={() => {
+ route(`/`);
+ }}
+ onUpdateError={noop}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
+ />
+ {/**
+ * Inventory pages
+ */}
+ <Route
+ path={InstancePaths.inventory_list}
+ component={ProductListPage}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
+ onCreate={() => {
+ route(InstancePaths.inventory_new);
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.inventory_update.replace(":pid", id));
+ }}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ />
+ <Route
+ path={InstancePaths.inventory_update}
+ component={ProductUpdatePage}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.inventory_list)}
+ onConfirm={() => {
+ route(InstancePaths.inventory_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.inventory_list);
+ }}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ />
+ <Route
+ path={InstancePaths.inventory_new}
+ component={ProductCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.inventory_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.inventory_list);
+ }}
+ />
+ {/**
+ * Deposit confirmation pages
+ */}
+ <Route
+ path={InstancePaths.deposit_confirmation_list}
+ component={DepositConfirmationListPage}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
+ onCreate={() => {
+ route(InstancePaths.deposit_confirmation_new);
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.deposit_confirmation_update.replace(":pid", id));
+ }}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ />
+ <Route
+ path={InstancePaths.deposit_confirmation_update}
+ component={DepositConfirmationUpdatePage}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.deposit_confirmation_list)}
+ onConfirm={() => {
+ route(InstancePaths.deposit_confirmation_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.deposit_confirmation_list);
+ }}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ />
+ <Route
+ path={InstancePaths.deposit_confirmation_new}
+ component={DepositConfirmationCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.deposit_confirmation_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.deposit_confirmation_list);
+ }}
+ />
+ <Route path={InstancePaths.interface} component={Settings} />
+ {/**
+ * Example pages
+ */}
+ <Route path="/loading" component={Loading} />
+ <Route default component={NotFoundPage} />
+ </Router>
+ </InstanceContextProvider>
+ );
+}
+
+export function Redirect({ to }: { to: string }): null {
+ useEffect(() => {
+ route(to, true);
+ });
+ return null;
+}
+
+function AdminInstanceUpdatePage({
+ id,
+ ...rest
+}: { id: string } & InstanceUpdatePageProps): VNode {
+ const [token, changeToken] = useBackendInstanceToken(id);
+ const updateLoginStatus = (token?: LoginToken): void => {
+ changeToken(token);
+ };
+ const value = useMemo(
+ () => ({ id, token, admin: true, changeToken }),
+ [id, token],
+ );
+ const { i18n } = useTranslationContext();
+
+ return (
+ <InstanceContextProvider value={value}>
+ <InstanceAdminUpdatePage
+ {...rest}
+ instanceId={id}
+ onLoadError={(error: HttpError<MerchantBackend.ErrorDetail>) => {
+ const notif =
+ error.type === ErrorType.TIMEOUT
+ ? {
+ message: i18n.str`The request to the backend take too long and was cancelled`,
+ description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`,
+ type: "ERROR" as const,
+ }
+ : {
+ message: i18n.str`The backend reported a problem: HTTP status #${error.status}`,
+ description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`,
+ details:
+ error.type === ErrorType.CLIENT ||
+ error.type === ErrorType.SERVER
+ ? error.payload.detail
+ : undefined,
+ type: "ERROR" as const,
+ };
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <LoginPage onConfirm={updateLoginStatus} />
+ </Fragment>
+ );
+ }}
+ onUnauthorized={() => {
+ return (
+ <Fragment>
+ <NotificationCard
+ notification={{
+ message: i18n.str`Access denied`,
+ description: i18n.str`The access token provided is invalid`,
+ type: "ERROR",
+ }}
+ />
+ <LoginPage onConfirm={updateLoginStatus} />
+ </Fragment>
+ );
+ }}
+ />
+ </InstanceContextProvider>
+ );
+}
+
+function KycBanner(): VNode {
+ const kycStatus = useInstanceKYCDetails();
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings();
+ const today = format(new Date(), dateFormatForSettings(settings));
+ const [lastHide, setLastHide] = useSimpleLocalStorage("kyc-last-hide");
+ const hasBeenHidden = today === lastHide;
+ const needsToBeShown = kycStatus.ok && kycStatus.data.type === "redirect";
+ if (hasBeenHidden || !needsToBeShown) return <Fragment />;
+ return (
+ <NotificationCard
+ notification={{
+ type: "WARN",
+ message: "KYC verification needed",
+ description: (
+ <div>
+ <p>
+ Some transfer are on hold until a KYC process is completed. Go to
+ the KYC section in the left panel for more information
+ </p>
+ <div class="buttons is-right">
+ <button class="button" onClick={() => setLastHide(today)}>
+ <i18n.Translate>Hide for today</i18n.Translate>
+ </button>
+ </div>
+ </div>
+ ),
+ }}
+ />
+ );
+}