diff options
Diffstat (limited to 'packages/bank-ui/src/pages/admin/RemoveAccount.tsx')
-rw-r--r-- | packages/bank-ui/src/pages/admin/RemoveAccount.tsx | 267 |
1 files changed, 267 insertions, 0 deletions
diff --git a/packages/bank-ui/src/pages/admin/RemoveAccount.tsx b/packages/bank-ui/src/pages/admin/RemoveAccount.tsx new file mode 100644 index 000000000..61def9a95 --- /dev/null +++ b/packages/bank-ui/src/pages/admin/RemoveAccount.tsx @@ -0,0 +1,267 @@ +/* + 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 { + AbsoluteTime, + Amounts, + HttpStatusCode, + TalerError, + TalerErrorCode, + TranslatedString, + assertUnreachable, +} from "@gnu-taler/taler-util"; +import { + Attention, + Loading, + LocalNotificationBanner, + ShowInputErrorLabel, + notifyInfo, + useLocalNotification, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js"; +import { useBankCoreApiContext } from "../../context/config.js"; +import { useAccountDetails } from "../../hooks/account.js"; +import { useSessionState } from "../../hooks/session.js"; +import { undefinedIfEmpty } from "../../utils.js"; +import { LoginForm } from "../LoginForm.js"; +import { doAutoFocus } from "../PaytoWireTransferForm.js"; +import { useBankState } from "../../hooks/bank-state.js"; +import { RouteDefinition } from "../../route.js"; + +export function RemoveAccount({ + account, + routeCancel, + onUpdateSuccess, + onAuthorizationRequired, + focus, + routeHere, +}: { + focus?: boolean; + routeHere: RouteDefinition<{ account: string }>; + onAuthorizationRequired: () => void; + routeCancel: RouteDefinition; + onUpdateSuccess: () => void; + account: string; +}): VNode { + const { i18n } = useTranslationContext(); + const result = useAccountDetails(account); + const [accountName, setAccountName] = useState<string | undefined>(); + + const { state } = useSessionState(); + const token = state.status !== "loggedIn" ? undefined : state.token; + const { bank: api } = useBankCoreApiContext(); + const [notification, notify, handleError] = useLocalNotification(); + const [, updateBankState] = useBankState(); + + if (!result) { + return <Loading />; + } + if (result instanceof TalerError) { + return <ErrorLoadingWithDebug error={result} />; + } + if (result.type === "fail") { + switch (result.case) { + case HttpStatusCode.Unauthorized: + return <LoginForm currentUser={account} />; + case HttpStatusCode.NotFound: + return <LoginForm currentUser={account} />; + default: + assertUnreachable(result); + } + } + + const balance = Amounts.parse(result.body.balance.amount); + if (!balance) { + return <div>there was an error reading the balance</div>; + } + const isBalanceEmpty = Amounts.isZero(balance); + if (!isBalanceEmpty) { + return ( + <Fragment> + <Attention type="warning" title={i18n.str`Can't delete the account`}> + <i18n.Translate> + The account can't be delete while still holding some balance. First + make sure that the owner make a complete cashout. + </i18n.Translate> + </Attention> + <div class="mt-5 sm:mt-6"> + <a + href={routeCancel.url({})} + name="close" + class="inline-flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + > + <i18n.Translate>Close</i18n.Translate> + </a> + </div> + </Fragment> + ); + } + + async function doRemove() { + if (!token) return; + await handleError(async () => { + const resp = await api.deleteAccount({ username: account, token }); + if (resp.type === "ok") { + notifyInfo(i18n.str`Account removed`); + onUpdateSuccess(); + } else { + switch (resp.case) { + case HttpStatusCode.Unauthorized: + return notify({ + type: "error", + title: i18n.str`No enough permission to delete the account.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case HttpStatusCode.NotFound: + return notify({ + type: "error", + title: i18n.str`The username was not found.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT: + return notify({ + type: "error", + title: i18n.str`Can't delete a reserved username.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case TalerErrorCode.BANK_ACCOUNT_BALANCE_NOT_ZERO: + return notify({ + type: "error", + title: i18n.str`Can't delete an account with balance different than zero.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case HttpStatusCode.Accepted: { + updateBankState("currentChallenge", { + operation: "delete-account", + id: String(resp.body.challenge_id), + sent: AbsoluteTime.never(), + location: routeHere.url({ account }), + request: account, + }); + return onAuthorizationRequired(); + } + default: { + assertUnreachable(resp); + } + } + } + }); + } + + const errors = undefinedIfEmpty({ + accountName: !accountName + ? i18n.str`Required` + : account !== accountName + ? i18n.str`Name doesn't match` + : undefined, + }); + + return ( + <div> + <LocalNotificationBanner notification={notification} /> + + <Attention + type="warning" + title={i18n.str`You are going to remove the account`} + > + <i18n.Translate>This step can't be undone.</i18n.Translate> + </Attention> + + <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg"> + <div class="px-4 sm:px-0"> + <h2 class="text-base font-semibold leading-7 text-gray-900"> + <i18n.Translate>Deleting account "{account}"</i18n.Translate> + </h2> + </div> + <form + class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2" + autoCapitalize="none" + autoCorrect="off" + onSubmit={(e) => { + e.preventDefault(); + }} + > + <div class="px-4 py-6 sm:p-8"> + <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> + <div class="sm:col-span-5"> + <label + class="block text-sm font-medium leading-6 text-gray-900" + for="password" + > + {i18n.str`Verification`} + </label> + <div class="mt-2"> + <input + ref={focus ? doAutoFocus : undefined} + type="text" + class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + name="password" + id="password" + data-error={ + !!errors?.accountName && accountName !== undefined + } + value={accountName ?? ""} + onChange={(e) => { + setAccountName(e.currentTarget.value); + }} + placeholder={account} + autocomplete="off" + /> + <ShowInputErrorLabel + message={errors?.accountName} + isDirty={accountName !== undefined} + /> + </div> + <p class="mt-2 text-sm text-gray-500"> + <i18n.Translate> + Enter the account name that is going to be deleted + </i18n.Translate> + </p> + </div> + </div> + </div> + <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8"> + <a + href={routeCancel.url({})} + name="cancel" + class="text-sm font-semibold leading-6 text-gray-900" + > + <i18n.Translate>Cancel</i18n.Translate> + </a> + <button + type="submit" + name="delete" + class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600" + disabled={!!errors} + onClick={(e) => { + e.preventDefault(); + doRemove(); + }} + > + <i18n.Translate>Delete</i18n.Translate> + </button> + </div> + </form> + </div> + </div> + ); +} |