/* 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 */ import { LocalNotificationBanner, useLocalNotification, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { AccessToken, HttpStatusCode, TranslatedString, assertUnreachable, } from "@gnu-taler/taler-util"; import { useBankCoreApiContext } from "./context/config.js"; import { useSettingsContext } from "./context/settings.js"; import { useBackendState } from "./hooks/backend.js"; import { AccountPage } from "./pages/AccountPage/index.js"; import { BankFrame } from "./pages/BankFrame.js"; import { DownloadStats } from "./pages/DownloadStats.js"; import { LoginForm } from "./pages/LoginForm.js"; import { PublicHistoriesPage } from "./pages/PublicHistoriesPage.js"; import { RegistrationPage } from "./pages/RegistrationPage.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 { RemoveAccount } from "./pages/admin/RemoveAccount.js"; import { CreateCashout } from "./pages/business/CreateCashout.js"; import { ShowCashoutDetails } from "./pages/business/ShowCashoutDetails.js"; import { urlPattern, useCurrentLocation } from "./route.js"; import { useNavigationContext } from "./context/navigation.js"; import { useEffect } from "preact/hooks"; export function Routing(): VNode { const backend = useBackendState(); if (backend.state.status === "loggedIn") { const { isUserAdministrator, username } = backend.state; return ( ); } return ( { backend.logIn({ username, token: token }); }} /> ); } const publicPages = { login: urlPattern(/\/login/, () => "#/login"), register: urlPattern(/\/register/, () => "#/register"), publicAccounts: urlPattern(/\/public-accounts/, () => "#/public-accounts"), operationDetails: urlPattern<{ wopid: string }>( /\/operation\/(?[a-zA-Z0-9]+)/, ({ wopid }) => `#/operation/${wopid}`, ), solveSecondFactor: urlPattern(/\/2fa/, () => "#/2fa"), }; function PublicRounting({ onLoggedUser, }: { onLoggedUser: (username: string, token: AccessToken) => void; }): VNode { const settings = useSettingsContext(); const { i18n } = useTranslationContext(); const location = useCurrentLocation(publicPages); const { navigateTo } = useNavigationContext(); const { api } = useBankCoreApiContext(); const [notification, notify, handleError] = useLocalNotification(); useEffect(() => { if (location === undefined) { navigateTo(publicPages.login.url({})); } }, [location]); if (location === undefined) { return ; } async function doAutomaticLogin(username: string, password: string) { await handleError(async () => { const resp = await api .getAuthenticationAPI(username) .createAccessToken(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, }); case HttpStatusCode.NotFound: return notify({ type: "error", title: i18n.str`Account not found`, description: resp.detail.hint as TranslatedString, debug: resp.detail, }); default: assertUnreachable(resp); } } }); } switch (location.name) { case "login": { return (

{i18n.str`Welcome to ${settings.bankName}!`}

); } case "publicAccounts": { return ; } case "operationDetails": { return ( navigateTo(publicPages.login.url({}))} routeClose={publicPages.login} onAuthorizationRequired={() => navigateTo(publicPages.solveSecondFactor.url({})) } /> ); } case "register": { return ( ); } case "solveSecondFactor": { return ( 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"), solveSecondFactor: urlPattern(/\/2fa/, () => "#/2fa"), cashoutCreate: urlPattern(/\/new-cashout/, () => "#/new-cashout"), cashoutDetails: urlPattern<{ cid: string }>( /\/cashout\/(?[a-zA-Z0-9]+)/, ({ cid }) => `#/cashout/${cid}`, ), wireTranserCreate: urlPattern<{ account?: string, subject?: string, amount?: string, }>( /\/wire-transfer\/(?[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"), accountDetails: urlPattern<{ account: string }>( /\/profile\/(?[a-zA-Z0-9_-]+)\/details/, ({ account }) => `#/profile/${account}/details`, ), accountChangePassword: urlPattern<{ account: string }>( /\/profile\/(?[a-zA-Z0-9_-]+)\/change-password/, ({ account }) => `#/profile/${account}/change-password`, ), accountDelete: urlPattern<{ account: string }>( /\/profile\/(?[a-zA-Z0-9_-]+)\/delete/, ({ account }) => `#/profile/${account}/delete`, ), accountCashouts: urlPattern<{ account: string }>( /\/profile\/(?[a-zA-Z0-9_-]+)\/cashouts/, ({ account }) => `#/profile/${account}/cashouts`, ), startOperation: urlPattern<{ wopid: string }>( /\/start-operation\/(?[a-zA-Z0-9-]+)/, ({ wopid }) => `#/start-operation/${wopid}`, ), operationDetails: urlPattern<{ wopid: string }>( /\/operation\/(?[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 ; } switch (location.name) { case "operationDetails": { return ( navigateTo(privatePages.home.url({}))} routeClose={privatePages.home} onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({})) } /> ); } case "startOperation": { return ( navigateTo(privatePages.home.url({}))} routeClose={privatePages.home} onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({})) } /> ); } case "solveSecondFactor": { return ( navigateTo(privatePages.home.url({}))} routeClose={privatePages.home} /> ); } case "publicAccountList": { return ; } case "statsDownload": { return ; } case "accountCreate": { return ( navigateTo(privatePages.home.url({}))} /> ); } case "accountDetails": { return ( navigateTo(privatePages.home.url({}))} routeHere={privatePages.accountDetails} routeMyAccountCashout={privatePages.myAccountCashouts} routeMyAccountDelete={privatePages.myAccountDelete} routeMyAccountDetails={privatePages.myAccountDetails} routeMyAccountPassword={privatePages.myAccountPassword} onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({})) } routeClose={privatePages.home} /> ); } case "accountChangePassword": { return ( navigateTo(privatePages.home.url({}))} routeMyAccountCashout={privatePages.myAccountCashouts} routeMyAccountDelete={privatePages.myAccountDelete} routeMyAccountDetails={privatePages.myAccountDetails} routeMyAccountPassword={privatePages.myAccountPassword} onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({})) } routeClose={privatePages.home} /> ); } case "accountDelete": { return ( navigateTo(privatePages.home.url({}))} onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({})) } routeCancel={privatePages.home} /> ); } case "accountCashouts": { return ( navigateTo(privatePages.solveSecondFactor.url({})) } /> ); } case "myAccountDelete": { return ( navigateTo(privatePages.home.url({}))} onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({})) } routeCancel={privatePages.home} /> ); } case "myAccountDetails": { return ( navigateTo(privatePages.home.url({}))} routeMyAccountCashout={privatePages.myAccountCashouts} routeMyAccountDelete={privatePages.myAccountDelete} routeMyAccountDetails={privatePages.myAccountDetails} routeMyAccountPassword={privatePages.myAccountPassword} onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({})) } routeClose={privatePages.home} /> ); } case "myAccountPassword": { return ( navigateTo(privatePages.home.url({}))} routeMyAccountCashout={privatePages.myAccountCashouts} routeMyAccountDelete={privatePages.myAccountDelete} routeMyAccountDetails={privatePages.myAccountDetails} routeMyAccountPassword={privatePages.myAccountPassword} onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({})) } routeClose={privatePages.home} /> ); } case "myAccountCashouts": { return ( navigateTo(privatePages.solveSecondFactor.url({})) } routeClose={privatePages.home} /> ); } case "home": { if (isAdmin) { return ( 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 ( navigateTo(privatePages.home.url({}))} onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({})) } onOperationCreated={(wopid) => navigateTo(privatePages.startOperation.url({ wopid })) } /> ); } case "cashoutCreate": { return ( navigateTo(privatePages.solveSecondFactor.url({})) } routeClose={privatePages.home} /> ); } case "cashoutDetails": { return ( ); } case "wireTranserCreate": { return ( navigateTo(privatePages.solveSecondFactor.url({})) } routeCancel={privatePages.home} onSuccess={() => navigateTo(privatePages.home.url({}))} /> ); } case "homeChargeWallet": { return ( navigateTo(privatePages.home.url({}))} onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({})) } onOperationCreated={(wopid) => navigateTo(privatePages.startOperation.url({ wopid })) } /> ); } case "homeWireTransfer": { return ( navigateTo(privatePages.home.url({}))} onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({})) } onOperationCreated={(wopid) => navigateTo(privatePages.startOperation.url({ wopid })) } /> ); } default: assertUnreachable(location); } }