summaryrefslogtreecommitdiff
path: root/packages/bank-ui/src/Routing.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/bank-ui/src/Routing.tsx')
-rw-r--r--packages/bank-ui/src/Routing.tsx612
1 files changed, 612 insertions, 0 deletions
diff --git a/packages/bank-ui/src/Routing.tsx b/packages/bank-ui/src/Routing.tsx
new file mode 100644
index 000000000..23635d4cd
--- /dev/null
+++ b/packages/bank-ui/src/Routing.tsx
@@ -0,0 +1,612 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-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 {
+ LocalNotificationBanner,
+ urlPattern,
+ useBankCoreApiContext,
+ useCurrentLocation,
+ useLocalNotification,
+ useNavigationContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+
+import {
+ AbsoluteTime,
+ AccessToken,
+ HttpStatusCode,
+ TranslatedString,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import { useEffect } from "preact/hooks";
+import { useSessionState } from "./hooks/session.js";
+import { AccountPage } from "./pages/AccountPage/index.js";
+import { BankFrame } from "./pages/BankFrame.js";
+import { LoginForm } from "./pages/LoginForm.js";
+import { PublicHistoriesPage } from "./pages/PublicHistoriesPage.js";
+import { RegistrationPage } from "./pages/RegistrationPage.js";
+import { ShowNotifications } from "./pages/ShowNotifications.js";
+import { SolveChallengePage } from "./pages/SolveChallengePage.js";
+import { WireTransfer } from "./pages/WireTransfer.js";
+import { WithdrawalOperationPage } from "./pages/WithdrawalOperationPage.js";
+import { CashoutListForAccount } from "./pages/account/CashoutListForAccount.js";
+import { ShowAccountDetails } from "./pages/account/ShowAccountDetails.js";
+import { UpdateAccountPassword } from "./pages/account/UpdateAccountPassword.js";
+import { AdminHome } from "./pages/admin/AdminHome.js";
+import { CreateNewAccount } from "./pages/admin/CreateNewAccount.js";
+import { DownloadStats } from "./pages/admin/DownloadStats.js";
+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";
+
+export function Routing(): VNode {
+ const session = useSessionState();
+
+ if (session.state.status === "loggedIn") {
+ const { isUserAdministrator, username } = session.state;
+ return (
+ <BankFrame
+ account={username}
+ routeAccountDetails={privatePages.myAccountDetails}
+ >
+ <PrivateRouting username={username} isAdmin={isUserAdministrator} />
+ </BankFrame>
+ );
+ }
+ return (
+ <BankFrame>
+ <PublicRounting
+ onLoggedUser={(username, token) => {
+ session.logIn({ username, token: token });
+ }}
+ />
+ </BankFrame>
+ );
+}
+
+const publicPages = {
+ login: urlPattern(/\/login/, () => "#/login"),
+ register: urlPattern(/\/register/, () => "#/register"),
+ publicAccounts: urlPattern(/\/public-accounts/, () => "#/public-accounts"),
+ operationDetails: urlPattern<{ wopid: string }>(
+ /\/operation\/(?<wopid>[a-zA-Z0-9-]+)/,
+ ({ wopid }) => `#/operation/${wopid}`,
+ ),
+ solveSecondFactor: urlPattern(/\/2fa/, () => "#/2fa"),
+};
+
+function PublicRounting({
+ onLoggedUser,
+}: {
+ onLoggedUser: (username: string, token: AccessToken) => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const location = useCurrentLocation(publicPages);
+ const { navigateTo } = useNavigationContext();
+ const { config, lib } = useBankCoreApiContext();
+ const [notification, notify, handleError] = useLocalNotification();
+
+ useEffect(() => {
+ if (location === undefined) {
+ navigateTo(publicPages.login.url({}));
+ }
+ }, [location]);
+
+ if (location === undefined) {
+ return <Fragment />;
+ }
+
+ async function doAutomaticLogin(username: string, password: string) {
+ await handleError(async () => {
+ const resp = await lib
+ .auth(username)
+ .createAccessTokenBasic(username, password, {
+ scope: "readwrite",
+ duration: { d_us: "forever" },
+ refreshable: true,
+ });
+ if (resp.type === "ok") {
+ onLoggedUser(username, resp.body.access_token);
+ } else {
+ switch (resp.case) {
+ case HttpStatusCode.Unauthorized:
+ return notify({
+ type: "error",
+ title: i18n.str`Wrong credentials for "${username}"`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`Account not found`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ default:
+ assertUnreachable(resp);
+ }
+ }
+ });
+ }
+
+ switch (location.name) {
+ case "login": {
+ return (
+ <Fragment>
+ <div class="sm:mx-auto sm:w-full sm:max-w-sm">
+ <h2 class="text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">{i18n.str`Welcome to ${config.bank_name}!`}</h2>
+ </div>
+ <LoginForm routeRegister={publicPages.register} />
+ </Fragment>
+ );
+ }
+ case "publicAccounts": {
+ return <PublicHistoriesPage />;
+ }
+ case "operationDetails": {
+ return (
+ <WithdrawalOperationPage
+ operationId={location.values.wopid}
+ routeWithdrawalDetails={publicPages.operationDetails}
+ purpose="after-confirmation"
+ onOperationAborted={() => navigateTo(publicPages.login.url({}))}
+ routeClose={publicPages.login}
+ onAuthorizationRequired={() =>
+ navigateTo(publicPages.solveSecondFactor.url({}))
+ }
+ />
+ );
+ }
+ case "register": {
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
+ <RegistrationPage
+ onRegistrationSuccesful={doAutomaticLogin}
+ routeCancel={publicPages.login}
+ />
+ </Fragment>
+ );
+ }
+ case "solveSecondFactor": {
+ return (
+ <SolveChallengePage
+ onChallengeCompleted={() => navigateTo(publicPages.login.url({}))}
+ routeClose={publicPages.login}
+ />
+ );
+ }
+ default:
+ assertUnreachable(location);
+ }
+}
+
+export const privatePages = {
+ homeChargeWallet: urlPattern(
+ /\/account\/charge-wallet/,
+ () => "#/account/charge-wallet",
+ ),
+ homeWireTransfer: urlPattern<{
+ account?: string;
+ subject?: string;
+ amount?: string;
+ }>(/\/account\/wire-transfer/, () => "#/account/wire-transfer"),
+ home: urlPattern(/\/account/, () => "#/account"),
+ notifications: urlPattern(/\/notifications/, () => "#/notifications"),
+ solveSecondFactor: urlPattern(/\/2fa/, () => "#/2fa"),
+ cashoutCreate: urlPattern(/\/new-cashout/, () => "#/new-cashout"),
+ cashoutDetails: urlPattern<{ cid: string }>(
+ /\/cashout\/(?<cid>[a-zA-Z0-9]+)/,
+ ({ cid }) => `#/cashout/${cid}`,
+ ),
+ wireTranserCreate: urlPattern<{
+ account?: string;
+ subject?: string;
+ amount?: string;
+ }>(
+ /\/wire-transfer\/(?<account>[a-zA-Z0-9]+)/,
+ ({ account }) => `#/wire-transfer/${account}`,
+ ),
+ publicAccountList: urlPattern(/\/public-accounts/, () => "#/public-accounts"),
+ statsDownload: urlPattern(/\/download-stats/, () => "#/download-stats"),
+ accountCreate: urlPattern(/\/new-account/, () => "#/new-account"),
+ myAccountDelete: urlPattern(
+ /\/delete-my-account/,
+ () => "#/delete-my-account",
+ ),
+ myAccountDetails: urlPattern(/\/my-profile/, () => "#/my-profile"),
+ myAccountPassword: urlPattern(/\/my-password/, () => "#/my-password"),
+ myAccountCashouts: urlPattern(/\/my-cashouts/, () => "#/my-cashouts"),
+ conversionConfig: urlPattern(/\/conversion/, () => "#/conversion"),
+ accountDetails: urlPattern<{ account: string }>(
+ /\/profile\/(?<account>[a-zA-Z0-9_-]+)\/details/,
+ ({ account }) => `#/profile/${account}/details`,
+ ),
+ accountChangePassword: urlPattern<{ account: string }>(
+ /\/profile\/(?<account>[a-zA-Z0-9_-]+)\/change-password/,
+ ({ account }) => `#/profile/${account}/change-password`,
+ ),
+ accountDelete: urlPattern<{ account: string }>(
+ /\/profile\/(?<account>[a-zA-Z0-9_-]+)\/delete/,
+ ({ account }) => `#/profile/${account}/delete`,
+ ),
+ accountCashouts: urlPattern<{ account: string }>(
+ /\/profile\/(?<account>[a-zA-Z0-9_-]+)\/cashouts/,
+ ({ account }) => `#/profile/${account}/cashouts`,
+ ),
+ startOperation: urlPattern<{ wopid: string }>(
+ /\/start-operation\/(?<wopid>[a-zA-Z0-9-]+)/,
+ ({ wopid }) => `#/start-operation/${wopid}`,
+ ),
+ operationDetails: urlPattern<{ wopid: string }>(
+ /\/operation\/(?<wopid>[a-zA-Z0-9-]+)/,
+ ({ wopid }) => `#/operation/${wopid}`,
+ ),
+};
+
+function PrivateRouting({
+ username,
+ isAdmin,
+}: {
+ username: string;
+ isAdmin: boolean;
+}): VNode {
+ const { navigateTo } = useNavigationContext();
+ const location = useCurrentLocation(privatePages);
+ useEffect(() => {
+ if (location === undefined) {
+ navigateTo(privatePages.home.url({}));
+ }
+ }, [location]);
+
+ if (location === undefined) {
+ return <Fragment />;
+ }
+
+ switch (location.name) {
+ case "operationDetails": {
+ return (
+ <WithdrawalOperationPage
+ operationId={location.values.wopid}
+ routeWithdrawalDetails={privatePages.operationDetails}
+ purpose="after-confirmation"
+ onOperationAborted={() => navigateTo(privatePages.home.url({}))}
+ routeClose={privatePages.home}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ />
+ );
+ }
+ case "startOperation": {
+ return (
+ <WithdrawalOperationPage
+ operationId={location.values.wopid}
+ routeWithdrawalDetails={privatePages.operationDetails}
+ purpose="after-creation"
+ onOperationAborted={() => navigateTo(privatePages.home.url({}))}
+ routeClose={privatePages.home}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ />
+ );
+ }
+ case "solveSecondFactor": {
+ return (
+ <SolveChallengePage
+ onChallengeCompleted={() => navigateTo(privatePages.home.url({}))}
+ routeClose={privatePages.home}
+ />
+ );
+ }
+ case "publicAccountList": {
+ return <PublicHistoriesPage />;
+ }
+ case "statsDownload": {
+ return <DownloadStats routeCancel={privatePages.home} />;
+ }
+ case "accountCreate": {
+ return (
+ <CreateNewAccount
+ routeCancel={privatePages.home}
+ onCreateSuccess={() => navigateTo(privatePages.home.url({}))}
+ />
+ );
+ }
+ case "accountDetails": {
+ return (
+ <ShowAccountDetails
+ account={location.values.account}
+ onUpdateSuccess={() => navigateTo(privatePages.home.url({}))}
+ routeHere={privatePages.accountDetails}
+ routeMyAccountCashout={privatePages.myAccountCashouts}
+ routeMyAccountDelete={privatePages.myAccountDelete}
+ routeMyAccountDetails={privatePages.myAccountDetails}
+ routeMyAccountPassword={privatePages.myAccountPassword}
+ routeConversionConfig={privatePages.conversionConfig}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeClose={privatePages.home}
+ />
+ );
+ }
+ case "accountChangePassword": {
+ return (
+ <UpdateAccountPassword
+ focus
+ account={location.values.account}
+ routeHere={privatePages.accountChangePassword}
+ onUpdateSuccess={() => navigateTo(privatePages.home.url({}))}
+ routeMyAccountCashout={privatePages.myAccountCashouts}
+ routeMyAccountDelete={privatePages.myAccountDelete}
+ routeMyAccountDetails={privatePages.myAccountDetails}
+ routeMyAccountPassword={privatePages.myAccountPassword}
+ routeConversionConfig={privatePages.conversionConfig}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeClose={privatePages.home}
+ />
+ );
+ }
+ case "accountDelete": {
+ return (
+ <RemoveAccount
+ account={location.values.account}
+ routeHere={privatePages.accountDelete}
+ onUpdateSuccess={() => navigateTo(privatePages.home.url({}))}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeCancel={privatePages.home}
+ />
+ );
+ }
+ case "accountCashouts": {
+ return (
+ <CashoutListForAccount
+ account={location.values.account}
+ routeCreateCashout={privatePages.cashoutCreate}
+ routeCashoutDetails={privatePages.cashoutDetails}
+ routeClose={privatePages.home}
+ routeMyAccountCashout={privatePages.myAccountCashouts}
+ routeMyAccountDelete={privatePages.myAccountDelete}
+ routeMyAccountDetails={privatePages.myAccountDetails}
+ routeMyAccountPassword={privatePages.myAccountPassword}
+ routeConversionConfig={privatePages.conversionConfig}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ />
+ );
+ }
+ case "myAccountDelete": {
+ return (
+ <RemoveAccount
+ account={username}
+ routeHere={privatePages.accountDelete}
+ onUpdateSuccess={() => navigateTo(privatePages.home.url({}))}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeCancel={privatePages.home}
+ />
+ );
+ }
+ case "myAccountDetails": {
+ return (
+ <ShowAccountDetails
+ account={username}
+ routeHere={privatePages.accountDetails}
+ onUpdateSuccess={() => navigateTo(privatePages.home.url({}))}
+ routeMyAccountCashout={privatePages.myAccountCashouts}
+ routeConversionConfig={privatePages.conversionConfig}
+ routeMyAccountDelete={privatePages.myAccountDelete}
+ routeMyAccountDetails={privatePages.myAccountDetails}
+ routeMyAccountPassword={privatePages.myAccountPassword}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeClose={privatePages.home}
+ />
+ );
+ }
+ case "myAccountPassword": {
+ return (
+ <UpdateAccountPassword
+ focus
+ account={username}
+ routeHere={privatePages.accountChangePassword}
+ onUpdateSuccess={() => navigateTo(privatePages.home.url({}))}
+ routeMyAccountCashout={privatePages.myAccountCashouts}
+ routeMyAccountDelete={privatePages.myAccountDelete}
+ routeMyAccountDetails={privatePages.myAccountDetails}
+ routeMyAccountPassword={privatePages.myAccountPassword}
+ routeConversionConfig={privatePages.conversionConfig}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeClose={privatePages.home}
+ />
+ );
+ }
+ case "myAccountCashouts": {
+ return (
+ <CashoutListForAccount
+ account={username}
+ routeCashoutDetails={privatePages.cashoutDetails}
+ routeCreateCashout={privatePages.cashoutCreate}
+ routeMyAccountCashout={privatePages.myAccountCashouts}
+ routeMyAccountDelete={privatePages.myAccountDelete}
+ routeMyAccountDetails={privatePages.myAccountDetails}
+ routeMyAccountPassword={privatePages.myAccountPassword}
+ routeConversionConfig={privatePages.conversionConfig}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeClose={privatePages.home}
+ />
+ );
+ }
+ case "home": {
+ if (isAdmin) {
+ return (
+ <AdminHome
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeCreate={privatePages.accountCreate}
+ routeRemoveAccount={privatePages.accountDelete}
+ routeShowAccount={privatePages.accountDetails}
+ routeShowCashoutsAccount={privatePages.accountCashouts}
+ routeUpdatePasswordAccount={privatePages.accountChangePassword}
+ routeCreateWireTransfer={privatePages.wireTranserCreate}
+ routeDownloadStats={privatePages.statsDownload}
+ />
+ );
+ }
+ return (
+ <AccountPage
+ account={username}
+ tab={undefined}
+ routeCreateWireTransfer={privatePages.wireTranserCreate}
+ routePublicAccounts={privatePages.publicAccountList}
+ routeOperationDetails={privatePages.startOperation}
+ routeChargeWallet={privatePages.homeChargeWallet}
+ routeWireTransfer={privatePages.homeWireTransfer}
+ routeSolveSecondFactor={privatePages.solveSecondFactor}
+ routeCashout={privatePages.myAccountCashouts}
+ routeClose={privatePages.home}
+ onClose={() => navigateTo(privatePages.home.url({}))}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ onOperationCreated={(wopid) =>
+ navigateTo(privatePages.startOperation.url({ wopid }))
+ }
+ />
+ );
+ }
+ case "cashoutCreate": {
+ return (
+ <CreateCashout
+ account={username}
+ routeHere={privatePages.cashoutCreate}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeClose={privatePages.home}
+ />
+ );
+ }
+ case "cashoutDetails": {
+ return (
+ <ShowCashoutDetails
+ id={location.values.cid}
+ routeClose={privatePages.myAccountCashouts}
+ />
+ );
+ }
+ case "wireTranserCreate": {
+ return (
+ <WireTransfer
+ toAccount={location.values.account}
+ withAmount={location.values.amount}
+ withSubject={location.values.subject}
+ routeHere={privatePages.wireTranserCreate}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeCancel={privatePages.home}
+ onSuccess={() => navigateTo(privatePages.home.url({}))}
+ />
+ );
+ }
+ case "homeChargeWallet": {
+ return (
+ <AccountPage
+ account={username}
+ tab="charge-wallet"
+ routeChargeWallet={privatePages.homeChargeWallet}
+ routeWireTransfer={privatePages.homeWireTransfer}
+ routeCreateWireTransfer={privatePages.wireTranserCreate}
+ routePublicAccounts={privatePages.publicAccountList}
+ routeOperationDetails={privatePages.startOperation}
+ routeCashout={privatePages.myAccountCashouts}
+ routeSolveSecondFactor={privatePages.solveSecondFactor}
+ routeClose={privatePages.home}
+ onClose={() => navigateTo(privatePages.home.url({}))}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ onOperationCreated={(wopid) =>
+ navigateTo(privatePages.startOperation.url({ wopid }))
+ }
+ />
+ );
+ }
+ case "conversionConfig": {
+ return (
+ <ConversionConfig
+ routeMyAccountCashout={privatePages.myAccountCashouts}
+ routeMyAccountDelete={privatePages.myAccountDelete}
+ routeMyAccountDetails={privatePages.myAccountDetails}
+ routeMyAccountPassword={privatePages.myAccountPassword}
+ routeConversionConfig={privatePages.conversionConfig}
+ routeCancel={privatePages.home}
+ onUpdateSuccess={() => {
+ navigateTo(privatePages.home.url({}));
+ }}
+ />
+ );
+ }
+ case "homeWireTransfer": {
+ return (
+ <AccountPage
+ account={username}
+ tab="wire-transfer"
+ routeChargeWallet={privatePages.homeChargeWallet}
+ routeWireTransfer={privatePages.homeWireTransfer}
+ routeCreateWireTransfer={privatePages.wireTranserCreate}
+ routePublicAccounts={privatePages.publicAccountList}
+ routeOperationDetails={privatePages.startOperation}
+ routeSolveSecondFactor={privatePages.solveSecondFactor}
+ routeCashout={privatePages.myAccountCashouts}
+ routeClose={privatePages.home}
+ onClose={() => navigateTo(privatePages.home.url({}))}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ onOperationCreated={(wopid) =>
+ navigateTo(privatePages.startOperation.url({ wopid }))
+ }
+ />
+ );
+ }
+ case "notifications": {
+ return <ShowNotifications />;
+ }
+ default:
+ assertUnreachable(location);
+ }
+}