diff options
Diffstat (limited to 'packages/demobank-ui/src/pages/admin')
-rw-r--r-- | packages/demobank-ui/src/pages/admin/AccountForm.tsx | 104 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/admin/AdminHome.tsx | 17 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/admin/RemoveAccount.tsx | 16 |
3 files changed, 114 insertions, 23 deletions
diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx b/packages/demobank-ui/src/pages/admin/AccountForm.tsx index 859c04396..7296e7744 100644 --- a/packages/demobank-ui/src/pages/admin/AccountForm.tsx +++ b/packages/demobank-ui/src/pages/admin/AccountForm.tsx @@ -1,9 +1,9 @@ import { AmountString, Amounts, PaytoString, TalerCorebankApi, TranslatedString, buildPayto, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; -import { CopyButton, ShowInputErrorLabel, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Attention, CopyButton, ShowInputErrorLabel, useTranslationContext } from "@gnu-taler/web-util/browser"; import { ComponentChildren, Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { useBankCoreApiContext } from "../../context/config.js"; -import { ErrorMessageMappingFor, PartialButDefined, WithIntermediate, undefinedIfEmpty, validateIBAN } from "../../utils.js"; +import { VersionHint, useBankCoreApiContext } from "../../context/config.js"; +import { ErrorMessageMappingFor, PartialButDefined, TanChannel, WithIntermediate, undefinedIfEmpty, validateIBAN } from "../../utils.js"; import { InputAmount, doAutoFocus } from "../PaytoWireTransferForm.js"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; import { getRandomPassword } from "../rnd.js"; @@ -24,6 +24,7 @@ export type AccountFormData = { cashout_payto_uri?: string, email?: string, phone?: string, + tan_channel?: TanChannel | "remove", } type ChangeByPurposeType = { @@ -55,7 +56,7 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ onChange: ChangeByPurposeType[PurposeType]; purpose: PurposeType; }): VNode { - const { config } = useBankCoreApiContext() + const { config, hints } = useBankCoreApiContext() const { i18n } = useTranslationContext(); const { state: credentials } = useBackendState(); const [form, setForm] = useState<AccountFormData>({}); @@ -75,8 +76,11 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ email: template?.contact_data?.email ?? "", phone: template?.contact_data?.phone ?? "", username: username ?? "", + tan_channel: template?.tan_channel, } + const OLD_CASHOUT_API = hints.indexOf(VersionHint.CASHOUT_BEFORE_2FA) !== -1 + const showingCurrentUserInfo = credentials.status !== "loggedIn" ? false : username === credentials.username const userIsAdmin = credentials.status !== "loggedIn" ? false : credentials.isUserAdministrator @@ -86,6 +90,9 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ const editableThreshold = userIsAdmin && (purpose === "create" || purpose === "update") const editableAccount = purpose === "create" && userIsAdmin + const hasPhone = !!defaultValue.phone || !!form.phone + const hasEmail = !!defaultValue.email || !!form.email + function updateForm(newForm: typeof defaultValue): void { const cashoutParsed = !newForm.cashout_payto_uri ? undefined @@ -173,6 +180,8 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ payto_uri: internalURI, is_public: !!newForm.isPublic, is_taler_exchange: !!newForm.isExchange, + // @ts-ignore + tan_channel: newForm.tan_channel === "remove" ? null : newForm.tan_channel, } callback(result) return; @@ -190,6 +199,8 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ debit_threshold: threshold, is_public: !!newForm.isPublic, name: newForm.name, + // @ts-ignore + tan_channel: newForm?.tan_channel === "remove" ? null : newForm.tan_channel, } callback(result) return; @@ -409,7 +420,87 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ <span aria-hidden="true" data-enabled={form.isExchange ?? defaultValue.isExchange ? "true" : "false"} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span> </button> </div> - </div>} + </div> + } + {/* channel, not shown if old cashout api */} + {OLD_CASHOUT_API ? undefined : config.supported_tan_channels.length === 0 ? + <div class="sm:col-span-5"> + <Attention type="warning" title={i18n.str`No cashout channel available`}> + <i18n.Translate> + This server doesn't support second factor authentication. + </i18n.Translate> + </Attention> + </div> + : + <div class="sm:col-span-5"> + <label + class="block text-sm font-medium leading-6 text-gray-900" + for="channel" + > + {i18n.str`Confirmation the operation using`} + </label> + <div class="mt-2 max-w-xl text-sm text-gray-500"> + <div class="px-4 mt-4 grid grid-cols-1 gap-y-6"> + {config.supported_tan_channels.indexOf(TanChannel.EMAIL) === -1 ? undefined : + <label onClick={(e) => { + if (!hasEmail) return; + if (form.tan_channel === TanChannel.EMAIL) { + form.tan_channel = "remove" + } else { + form.tan_channel = TanChannel.EMAIL + } + updateForm(structuredClone(form)) + e.preventDefault() + }} data-disabled={purpose === "show" || !hasEmail} data-selected={(form.tan_channel ?? defaultValue.tan_channel) === TanChannel.EMAIL} + class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border bg-white data-[disabled=true]:bg-gray-200 p-4 shadow-sm focus:outline-none border-gray-300 data-[selected=true]:ring-2 data-[selected=true]:ring-indigo-600"> + <input type="radio" name="channel" value="Newsletter" class="sr-only" /> + <span class="flex flex-1"> + <span class="flex flex-col"> + <span id="project-type-0-label" class="block text-sm font-medium text-gray-900 "> + <i18n.Translate>Email</i18n.Translate> + </span> + {purpose !== "show" && !hasEmail && i18n.str`add a email in your profile to enable this option`} + </span> + </span> + <svg data-selected={(form.tan_channel ?? defaultValue.tan_channel) === TanChannel.EMAIL} class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /> + </svg> + </label> + } + + {config.supported_tan_channels.indexOf(TanChannel.SMS) === -1 ? undefined : + <label onClick={(e) => { + if (!hasPhone) return; + if (form.tan_channel === TanChannel.SMS) { + form.tan_channel = "remove" + } else { + form.tan_channel = TanChannel.SMS + } + updateForm(structuredClone(form)) + e.preventDefault() + }} data-disabled={purpose === "show" || !hasPhone} data-selected={(form.tan_channel ?? defaultValue.tan_channel) === TanChannel.SMS} + class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border data-[disabled=true]:bg-gray-200 p-4 shadow-sm focus:outline-none border-gray-300 data-[selected=true]:ring-2 data-[selected=true]:ring-indigo-600"> + <input type="radio" name="channel" value="Existing Customers" class="sr-only" /> + <span class="flex flex-1"> + <span class="flex flex-col"> + <span id="project-type-1-label" class="block text-sm font-medium text-gray-900"> + <i18n.Translate>SMS</i18n.Translate> + </span> + {purpose !== "show" && !hasPhone && i18n.str`add a phone number in your profile to enable this option`} + </span> + </span> + <svg data-selected={(form.tan_channel ?? defaultValue.tan_channel) === TanChannel.SMS} class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /> + </svg> + </label> + } + <pre> + {JSON.stringify(form, undefined, 2)} + </pre> + </div> + </div> + </div> + } <div class="sm:col-span-5"> <div class="flex items-center justify-between"> @@ -434,9 +525,6 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ </div> </div> - <pre> - {JSON.stringify(errors, undefined, 2)} - </pre> {children} </form> ); diff --git a/packages/demobank-ui/src/pages/admin/AdminHome.tsx b/packages/demobank-ui/src/pages/admin/AdminHome.tsx index 82a341dbe..f5bce1396 100644 --- a/packages/demobank-ui/src/pages/admin/AdminHome.tsx +++ b/packages/demobank-ui/src/pages/admin/AdminHome.tsx @@ -16,18 +16,17 @@ import { AccountList } from "./AccountList.js"; * Query account information and show QR code if there is pending withdrawal */ interface Props { - onRegister: () => void; - onCreateAccount: () => void; onShowAccountDetails: (aid: string) => void; onRemoveAccount: (aid: string) => void; onUpdateAccountPassword: (aid: string) => void; onShowCashoutForAccount: (aid: string) => void; + onAuthorizationRequired: () => void; } -export function AdminHome({ onCreateAccount, onRegister, onRemoveAccount, onShowAccountDetails, onShowCashoutForAccount, onUpdateAccountPassword }: Props): VNode { +export function AdminHome({ onCreateAccount, onAuthorizationRequired, onRemoveAccount, onShowAccountDetails, onShowCashoutForAccount, onUpdateAccountPassword }: Props): VNode { return <Fragment> <Metrics /> - <WireTransfer onRegister={onRegister} /> + <WireTransfer onAuthorizationRequired={onAuthorizationRequired} /> <Transactions account="admin" /> <AccountList @@ -184,11 +183,11 @@ function Metrics(): VNode { </div> </dl> <div class="flex justify-end mt-2"> - <a href="#/download-stats" - class="disabled:opacity-50 disabled:cursor-default cursor-pointer 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> - download stats as csv - </i18n.Translate></a> + <a href="#/download-stats" + class="disabled:opacity-50 disabled:cursor-default cursor-pointer 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> + download stats as csv + </i18n.Translate></a> </div> </Fragment> diff --git a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx index 3f7d62935..beadad957 100644 --- a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx +++ b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx @@ -1,4 +1,4 @@ -import { Amounts, HttpStatusCode, TalerError, TalerErrorCode, TranslatedString } from "@gnu-taler/taler-util"; +import { AbsoluteTime, Amounts, HttpStatusCode, TalerError, TalerErrorCode, TranslatedString } 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"; @@ -16,9 +16,11 @@ export function RemoveAccount({ account, onCancel, onUpdateSuccess, + onAuthorizationRequired, focus, }: { focus?: boolean; + onAuthorizationRequired: () => void, onCancel: () => void; onUpdateSuccess: () => void; account: string; @@ -92,11 +94,13 @@ export function RemoveAccount({ debug: resp.detail, }) case HttpStatusCode.Accepted: { - updateBankState("currentChallengeId", resp.body.challenge_id) - return notify({ - type: "info", - title: i18n.str`The operation needs a confirmation to complete.`, - }); + updateBankState("currentChallenge", { + operation: "delete-account", + id: String(resp.body.challenge_id), + sent: AbsoluteTime.never(), + request: account, + }) + return onAuthorizationRequired() } default: { assertUnreachable(resp) |