diff options
author | Sebastian <sebasjm@gmail.com> | 2023-09-04 14:17:55 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2023-09-04 14:17:55 -0300 |
commit | e1d86816a7c07cb8ca2d54676d5cdbbe513f2ba7 (patch) | |
tree | d4ed5506ab3550a7e9b1a082d7ffeddf9f3c4954 /packages/merchant-backoffice-ui/src/paths/instance/templates | |
parent | ff20c3e25e076c24f7cb93eabe58b6f934f51f35 (diff) | |
download | wallet-core-e1d86816a7c07cb8ca2d54676d5cdbbe513f2ba7.tar.xz |
backoffcie new version, lot of changes
Diffstat (limited to 'packages/merchant-backoffice-ui/src/paths/instance/templates')
5 files changed, 91 insertions, 272 deletions
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx index e20b9bc27..8629d8dee 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx @@ -24,7 +24,7 @@ import { MerchantTemplateContractDetails, } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { @@ -35,17 +35,16 @@ import { Input } from "../../../../components/form/Input.js"; import { InputCurrency } from "../../../../components/form/InputCurrency.js"; import { InputDuration } from "../../../../components/form/InputDuration.js"; import { InputNumber } from "../../../../components/form/InputNumber.js"; -import { InputSelector } from "../../../../components/form/InputSelector.js"; import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; import { useBackendContext } from "../../../../context/backend.js"; +import { useInstanceContext } from "../../../../context/instance.js"; import { MerchantBackend } from "../../../../declaration.js"; import { - isBase32RFC3548Charset, - randomBase32Key, + isBase32RFC3548Charset } from "../../../../utils/crypto.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; -import { QR } from "../../../../components/exception/QR.js"; -import { useInstanceContext } from "../../../../context/instance.js"; +import { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js"; +import { useInstanceOtpDevices } from "../../../../hooks/otp.js"; type Entity = MerchantBackend.Template.TemplateAddDetails; @@ -54,16 +53,11 @@ interface Props { onBack?: () => void; } -const algorithms = [0, 1, 2]; -const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"]; - export function CreatePage({ onCreate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const backend = useBackendContext(); - const { id: instanceId } = useInstanceContext(); - const issuer = new URL(backend.url).hostname; + const devices = useInstanceOtpDevices() - const [showKey, setShowKey] = useState(false); const [state, setState] = useState<Partial<Entity>>({ template_contract: { minimum_age: 0, @@ -78,7 +72,11 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { : Amounts.parse(state.template_contract?.amount); const errors: FormErrors<Entity> = { - template_id: !state.template_id ? i18n.str`should not be empty` : undefined, + template_id: !state.template_id + ? i18n.str`should not be empty` + : !/[a-zA-Z0-9]*/.test(state.template_id) + ? i18n.str`no valid. only characters and numbers` + : undefined, template_description: !state.template_description ? i18n.str`should not be empty` : undefined, @@ -104,15 +102,6 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { ? i18n.str`to short` : undefined, } as Partial<MerchantTemplateContractDetails>), - pos_key: !state.pos_key - ? !state.pos_algorithm - ? undefined - : i18n.str`required` - : !isBase32RFC3548Charset(state.pos_key) - ? i18n.str`just letters and numbers from 2 to 7` - : state.pos_key.length !== 32 - ? i18n.str`size of the key should be 32` - : undefined, }; const hasErrors = Object.keys(errors).some( @@ -124,7 +113,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { return onCreate(state as any); }; - const qrText = `otpauth://totp/${instanceId}/${state.template_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${state.pos_key}`; + const deviceList = !devices.ok ? [] : devices.data.otp_devices return ( <div> @@ -139,7 +128,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { > <InputWithAddon<Entity> name="template_id" - help={`${backend.url}/instances/templates/${state.template_id ?? ""}`} + help={`${backend.url}/templates/${state.template_id ?? ""}`} label={i18n.str`Identifier`} tooltip={i18n.str`Name of the template in URLs.`} /> @@ -172,83 +161,21 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { help="" tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`} /> - <InputSelector<Entity> - name="pos_algorithm" - label={i18n.str`Verification algorithm`} - tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`} - values={algorithms} - toStr={(v) => algorithmsNames[v]} - fromStr={(v) => Number(v)} + <Input<Entity> + name="otp_id" + label={i18n.str`OTP device`} + readonly + tooltip={i18n.str`Use to verify transaction in offline mode.`} + /> + <InputSearchOnList + label={i18n.str`Search device`} + onChange={(p) => setState((v) => ({ ...v, otp_id: p?.id }))} + list={deviceList.map(e => ({ + description: e.device_description, + id: e.otp_device_id + }))} /> - {state.pos_algorithm && state.pos_algorithm > 0 ? ( - <Fragment> - <InputWithAddon<Entity> - name="pos_key" - label={i18n.str`Point-of-sale key`} - inputType={showKey ? "text" : "password"} - help="Be sure to be very hard to guess or use the random generator" - tooltip={i18n.str`Useful to validate the purchase`} - fromStr={(v) => v.toUpperCase()} - addonAfter={ - <span class="icon"> - {showKey ? ( - <i class="mdi mdi-eye" /> - ) : ( - <i class="mdi mdi-eye-off" /> - )} - </span> - } - side={ - <span style={{ display: "flex" }}> - <button - data-tooltip={i18n.str`generate random secret key`} - class="button is-info mr-3" - onClick={(e) => { - const pos_key = randomBase32Key(); - setState((s) => ({ ...s, pos_key })); - }} - > - <i18n.Translate>random</i18n.Translate> - </button> - <button - data-tooltip={ - showKey - ? i18n.str`show secret key` - : i18n.str`hide secret key` - } - class="button is-info mr-3" - onClick={(e) => { - setShowKey(!showKey); - }} - > - {showKey ? ( - <i18n.Translate>hide</i18n.Translate> - ) : ( - <i18n.Translate>show</i18n.Translate> - )} - </button> - </span> - } - /> - {showKey && ( - <Fragment> - <QR text={qrText} /> - <div - style={{ - color: "grey", - fontSize: "small", - width: 200, - textAlign: "center", - margin: "auto", - wordBreak: "break-all", - }} - > - {qrText} - </div> - </Fragment> - )} - </Fragment> - ) : undefined} + </FormProvider> <div class="buttons is-right mt-5"> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx index 2f91298bf..3c9bb231c 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx @@ -36,6 +36,7 @@ import { import { Notification } from "../../../../utils/types.js"; import { ListPage } from "./ListPage.js"; import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { ConfirmModal } from "../../../../components/modal/index.js"; interface Props { onUnauthorized: () => VNode; @@ -61,6 +62,8 @@ export default function ListTemplates({ const [notif, setNotif] = useState<Notification | undefined>(undefined); const { deleteTemplate } = useTemplateAPI(); const result = useInstanceTemplates({ position }, (id) => setPosition(id)); + const [deleting, setDeleting] = + useState<MerchantBackend.Template.TemplateEntry | null>(null); if (result.loading) return <Loading />; if (!result.ok) { @@ -97,23 +100,45 @@ export default function ListTemplates({ onQR={(e) => { onQR(e.template_id); }} - onDelete={(e: MerchantBackend.Template.TemplateEntry) => - deleteTemplate(e.template_id) - .then(() => + onDelete={(e: MerchantBackend.Template.TemplateEntry) => { + setDeleting(e) + } + } + /> + + {deleting && ( + <ConfirmModal + label={`Delete template`} + description={`Delete the template "${deleting.template_description}"`} + danger + active + onCancel={() => setDeleting(null)} + onConfirm={async (): Promise<void> => { + try { + await deleteTemplate(deleting.template_id); setNotif({ - message: i18n.str`template delete successfully`, + message: i18n.str`Template "${deleting.template_description}" (ID: ${deleting.template_id}) has been deleted`, type: "SUCCESS", - }), - ) - .catch((error) => + }); + } catch (error) { setNotif({ - message: i18n.str`could not delete the template`, + message: i18n.str`Failed to delete template`, type: "ERROR", - description: error.message, - }), - ) - } - /> + description: error instanceof Error ? error.message : undefined, + }); + } + setDeleting(null); + }} + > + <p> + If you delete the template <b>"{deleting.template_description}"</b> (ID:{" "} + <b>{deleting.template_id}</b>) you may loose information + </p> + <p class="warning"> + Deleting an template <b>cannot be undone</b>. + </p> + </ConfirmModal> + )} </Fragment> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx index 0f30efafd..c65cf6a19 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx @@ -19,7 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { HttpError, useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { useState } from "preact/hooks"; import { QR } from "../../../../components/exception/QR.js"; @@ -35,35 +35,32 @@ import { useConfigContext } from "../../../../context/config.js"; import { useInstanceContext } from "../../../../context/instance.js"; import { MerchantBackend } from "../../../../declaration.js"; import { stringifyPayTemplateUri } from "@gnu-taler/taler-util"; +import { useOtpDeviceDetails } from "../../../../hooks/otp.js"; +import { Loading } from "../../../../components/exception/loading.js"; type Entity = MerchantBackend.Template.UsingTemplateDetails; interface Props { - template: MerchantBackend.Template.TemplateDetails; + contract: MerchantBackend.Template.TemplateContractDetails; id: string; onBack?: () => void; } -export function QrPage({ template, id: templateId, onBack }: Props): VNode { +export function QrPage({ contract, id: templateId, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const { url: backendUrl } = useBackendContext(); const { id: instanceId } = useInstanceContext(); const config = useConfigContext(); - const [setupTOTP, setSetupTOTP] = useState(false); const [state, setState] = useState<Partial<Entity>>({ - amount: template.template_contract.amount, - summary: template.template_contract.summary, + amount: contract.amount, + summary: contract.summary, }); const errors: FormErrors<Entity> = {}; - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - - const fixedAmount = !!template.template_contract.amount; - const fixedSummary = !!template.template_contract.summary; + const fixedAmount = !!contract.amount; + const fixedSummary = !!contract.summary; const templateParams: Record<string, string> = {} if (!fixedAmount) { @@ -89,40 +86,9 @@ export function QrPage({ template, id: templateId, onBack }: Props): VNode { const issuer = encodeURIComponent( `${new URL(backendUrl).host}/${instanceId}`, ); - const oauthUri = !template.pos_algorithm - ? undefined - : template.pos_algorithm === 1 - ? `otpauth://totp/${issuer}:${templateId}?secret=${template.pos_key}&issuer=${issuer}&algorithm=SHA1&digits=8&period=30` - : template.pos_algorithm === 2 - ? `otpauth://totp/${issuer}:${templateId}?secret=${template.pos_key}&issuer=${issuer}&algorithm=SHA1&digits=8&period=30` - : undefined; - - const keySlice = template.pos_key?.substring(0, 4); - - const oauthUriWithoutSecret = !template.pos_algorithm - ? undefined - : template.pos_algorithm === 1 - ? `otpauth://totp/${issuer}:${templateId}?secret=${keySlice}...&issuer=${issuer}&algorithm=SHA1&digits=8&period=30` - : template.pos_algorithm === 2 - ? `otpauth://totp/${issuer}:${templateId}?secret=${keySlice}...&issuer=${issuer}&algorithm=SHA1&digits=8&period=30` - : undefined; + return ( <div> - {oauthUri && ( - <ConfirmModal - description="Setup TOTP" - active={setupTOTP} - onCancel={() => { - setSetupTOTP(false); - }} - > - <p>Scan this qr code with your TOTP device</p> - <QR text={oauthUri} /> - <pre style={{ textAlign: "center" }}> - <a href={oauthUri}>{oauthUriWithoutSecret}</a> - </pre> - </ConfirmModal> - )} <section class="section is-main-section"> <div class="columns"> <div class="column" /> @@ -176,14 +142,6 @@ export function QrPage({ template, id: templateId, onBack }: Props): VNode { > <i18n.Translate>Print</i18n.Translate> </button> - {oauthUri && ( - <button - class="button is-info" - onClick={() => setSetupTOTP(true)} - > - <i18n.Translate>Setup TOTP</i18n.Translate> - </button> - )} </div> </div> <div class="column" /> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx index 1f74afc2b..7db7478f7 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx @@ -74,7 +74,7 @@ export default function TemplateQrPage({ return ( <> <NotificationCard notification={notif} /> - <QrPage template={result.data} id={tid} onBack={onBack} /> + <QrPage contract={result.data.template_contract} id={tid} onBack={onBack} /> </> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx index 30e5502bb..30d47385c 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx @@ -61,10 +61,7 @@ const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"]; export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const backend = useBackendContext(); - const { id: instanceId } = useInstanceContext(); - const issuer = new URL(backend.url).hostname; - const [showKey, setShowKey] = useState(false); const [state, setState] = useState<Partial<Entity>>(template); const parsedPrice = !state.template_contract?.amount @@ -78,34 +75,25 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { template_contract: !state.template_contract ? undefined : undefinedIfEmpty({ - amount: !state.template_contract?.amount - ? undefined - : !parsedPrice + amount: !state.template_contract?.amount + ? undefined + : !parsedPrice ? i18n.str`not valid` : Amounts.isZero(parsedPrice) - ? i18n.str`must be greater than 0` - : undefined, - minimum_age: - state.template_contract.minimum_age < 0 - ? i18n.str`should be greater that 0` + ? i18n.str`must be greater than 0` : undefined, - pay_duration: !state.template_contract.pay_duration - ? i18n.str`can't be empty` - : state.template_contract.pay_duration.d_us === "forever" + minimum_age: + state.template_contract.minimum_age < 0 + ? i18n.str`should be greater that 0` + : undefined, + pay_duration: !state.template_contract.pay_duration + ? i18n.str`can't be empty` + : state.template_contract.pay_duration.d_us === "forever" ? undefined : state.template_contract.pay_duration.d_us < 1000 * 1000 // less than one second - ? i18n.str`to short` - : undefined, - } as Partial<MerchantTemplateContractDetails>), - pos_key: !state.pos_key - ? !state.pos_algorithm - ? undefined - : i18n.str`required` - : !isBase32RFC3548Charset(state.pos_key) - ? i18n.str`just letters and numbers from 2 to 7` - : state.pos_key.length !== 32 - ? i18n.str`size of the key should be 32` - : undefined, + ? i18n.str`to short` + : undefined, + } as Partial<MerchantTemplateContractDetails>), }; const hasErrors = Object.keys(errors).some( @@ -117,7 +105,6 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { return onUpdate(state as any); }; - const qrText = `otpauth://totp/${instanceId}/${state.id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${state.pos_key}`; return ( <div> @@ -128,7 +115,7 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { <div class="level-left"> <div class="level-item"> <span class="is-size-4"> - {backend.url}/instances/template/{template.id} + {backend.url}/templates/{template.id} </span> </div> </div> @@ -182,84 +169,6 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { help="" tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`} /> - <InputSelector<Entity> - name="pos_algorithm" - label={i18n.str`Verification algorithm`} - tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`} - values={algorithms} - toStr={(v) => algorithmsNames[v]} - fromStr={(v) => Number(v)} - /> - {state.pos_algorithm && state.pos_algorithm > 0 ? ( - <Fragment> - <InputWithAddon<Entity> - name="pos_key" - label={i18n.str`Point-of-sale key`} - inputType={showKey ? "text" : "password"} - help="Be sure to be very hard to guess or use the random generator" - expand - tooltip={i18n.str`Useful to validate the purchase`} - fromStr={(v) => v.toUpperCase()} - addonAfter={ - <span class="icon"> - {showKey ? ( - <i class="mdi mdi-eye" /> - ) : ( - <i class="mdi mdi-eye-off" /> - )} - </span> - } - side={ - <span style={{ display: "flex" }}> - <button - data-tooltip={i18n.str`generate random secret key`} - class="button is-info mr-3" - onClick={(e) => { - const pos_key = randomBase32Key(); - setState((s) => ({ ...s, pos_key })); - }} - > - <i18n.Translate>random</i18n.Translate> - </button> - <button - data-tooltip={ - showKey - ? i18n.str`show secret key` - : i18n.str`hide secret key` - } - class="button is-info mr-3" - onClick={(e) => { - setShowKey(!showKey); - }} - > - {showKey ? ( - <i18n.Translate>hide</i18n.Translate> - ) : ( - <i18n.Translate>show</i18n.Translate> - )} - </button> - </span> - } - /> - {showKey && ( - <Fragment> - <QR text={qrText} /> - <div - style={{ - color: "grey", - fontSize: "small", - width: 200, - textAlign: "center", - margin: "auto", - wordBreak: "break-all", - }} - > - {qrText} - </div> - </Fragment> - )} - </Fragment> - ) : undefined} </FormProvider> <div class="buttons is-right mt-5"> |