diff options
Diffstat (limited to 'packages/bank-ui/src/Routing.tsx')
-rw-r--r-- | packages/bank-ui/src/Routing.tsx | 612 |
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); + } +} |