From e1d86816a7c07cb8ca2d54676d5cdbbe513f2ba7 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 4 Sep 2023 14:17:55 -0300 Subject: backoffcie new version, lot of changes --- .../paths/instance/templates/create/CreatePage.tsx | 127 +++++---------------- .../src/paths/instance/templates/list/index.tsx | 51 ++++++--- .../src/paths/instance/templates/qr/QrPage.tsx | 62 ++-------- .../src/paths/instance/templates/qr/index.tsx | 2 +- .../paths/instance/templates/update/UpdatePage.tsx | 121 +++----------------- 5 files changed, 91 insertions(+), 272 deletions(-) (limited to 'packages/merchant-backoffice-ui/src/paths/instance/templates') 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>({ 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 = { - 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), - 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 (
@@ -139,7 +128,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { > 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.`} /> - - 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)} + + name="otp_id" + label={i18n.str`OTP device`} + readonly + tooltip={i18n.str`Use to verify transaction in offline mode.`} + /> + 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 ? ( - - - 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={ - - {showKey ? ( - - ) : ( - - )} - - } - side={ - - - - - } - /> - {showKey && ( - - -
- {qrText} -
-
- )} -
- ) : undefined} +
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(undefined); const { deleteTemplate } = useTemplateAPI(); const result = useInstanceTemplates({ position }, (id) => setPosition(id)); + const [deleting, setDeleting] = + useState(null); if (result.loading) return ; 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 && ( + setDeleting(null)} + onConfirm={async (): Promise => { + 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); + }} + > +

+ If you delete the template "{deleting.template_description}" (ID:{" "} + {deleting.template_id}) you may loose information +

+

+ Deleting an template cannot be undone. +

+
+ )} ); } 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>({ - amount: template.template_contract.amount, - summary: template.template_contract.summary, + amount: contract.amount, + summary: contract.summary, }); const errors: FormErrors = {}; - 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 = {} 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 (
- {oauthUri && ( - { - setSetupTOTP(false); - }} - > -

Scan this qr code with your TOTP device

- -
-            {oauthUriWithoutSecret}
-          
-
- )}
@@ -176,14 +142,6 @@ export function QrPage({ template, id: templateId, onBack }: Props): VNode { > Print - {oauthUri && ( - - )}
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 ( <> - + ); } 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>(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), - 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), }; 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 (
@@ -128,7 +115,7 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
- {backend.url}/instances/template/{template.id} + {backend.url}/templates/{template.id}
@@ -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.`} /> - - 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 ? ( - - - 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={ - - {showKey ? ( - - ) : ( - - )} - - } - side={ - - - - - } - /> - {showKey && ( - - -
- {qrText} -
-
- )} -
- ) : undefined}
-- cgit v1.2.3