diff options
| author | Sebastian <sebasjm@gmail.com> | 2023-03-12 23:56:54 -0300 | 
|---|---|---|
| committer | Sebastian <sebasjm@gmail.com> | 2023-03-12 23:56:54 -0300 | 
| commit | b874f9a0c50084803de58febb698864aa8dd061a (patch) | |
| tree | 50f23d8faa674a94646a21c5821fd4f494c60f64 /packages/merchant-backoffice-ui | |
| parent | ae1aee13581469a8398321b57e95cc85f210047b (diff) | |
print and setup totp
Diffstat (limited to 'packages/merchant-backoffice-ui')
10 files changed, 227 insertions, 20 deletions
| diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx index 7a419ebb9..021977dfe 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx @@ -25,6 +25,7 @@ interface Props<T> extends InputProps<T> {    readonly?: boolean;    expand?: boolean;    values: string[]; +  convert?: (v: string) => any;    toStr?: (v?: any) => string;    fromStr?: (s: string) => any;  } @@ -41,6 +42,7 @@ export function InputSelector<T>({    label,    help,    values, +  convert,    toStr = defaultToString,  }: Props<keyof T>): VNode {    const { error, value, onChange } = useField<T>(name); @@ -66,7 +68,10 @@ export function InputSelector<T>({                disabled={readonly}                readonly={readonly}                onChange={(e) => { -                onChange(e.currentTarget.value as any); +                const v = convert +                  ? convert(e.currentTarget.value) +                  : e.currentTarget.value; +                onChange(v);                }}              >                {placeholder && <option>{placeholder}</option>} diff --git a/packages/merchant-backoffice-ui/src/declaration.d.ts b/packages/merchant-backoffice-ui/src/declaration.d.ts index c9380760c..9fc4f0d77 100644 --- a/packages/merchant-backoffice-ui/src/declaration.d.ts +++ b/packages/merchant-backoffice-ui/src/declaration.d.ts @@ -1287,6 +1287,9 @@ export namespace MerchantBackend {        // This parameter is optional.        pos_key?: string; +      // Algorithm for computing the POS confirmation, 0 for none. +      pos_algorithm?: number; +        // Additional information in a separate template.        template_contract: TemplateContractDetails;      } @@ -1313,6 +1316,9 @@ export namespace MerchantBackend {        // This parameter is optional.        pos_key?: string; +      // Algorithm for computing the POS confirmation, 0 for none. +      pos_algorithm?: Integer; +        // Additional information in a separate template.        template_contract: TemplateContractDetails;      } @@ -1338,6 +1344,9 @@ export namespace MerchantBackend {        // This parameter is optional.        pos_key?: string; +      // Algorithm for computing the POS confirmation, 0 for none. +      pos_algorithm?: Integer; +        // Additional information in a separate template.        template_contract: TemplateContractDetails;      } diff --git a/packages/merchant-backoffice-ui/src/hooks/templates.ts b/packages/merchant-backoffice-ui/src/hooks/templates.ts index dd096e4f9..97fb165b9 100644 --- a/packages/merchant-backoffice-ui/src/hooks/templates.ts +++ b/packages/merchant-backoffice-ui/src/hooks/templates.ts @@ -244,7 +244,11 @@ export function useTemplateDetails(    });    if (isValidating) return { loading: true, data: data?.data }; -  if (data) return data; +  if (data) { +    const d = structuredClone(data); +    d.data.pos_algorithm = 1; +    return d; +  }    if (error) return error.info;    return { loading: true };  } 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 22f86002a..144e968c5 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 @@ -31,9 +31,11 @@ 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 { MerchantBackend } from "../../../../declaration.js"; +import { randomBase32Key } from "../../../../utils/crypto.js";  import { undefinedIfEmpty } from "../../../../utils/table.js";  type Entity = MerchantBackend.Template.TemplateAddDetails; @@ -43,6 +45,13 @@ interface Props {    onBack?: () => void;  } +const algorithms = ["0", "1", "2"]; +const algorithmsNames = [ +  "off", +  "30s 8d TOTP-SHA1 without amount", +  "30s 8d eTOTP-SHA1 with amount", +]; +  export function CreatePage({ onCreate, onBack }: Props): VNode {    const { i18n } = useTranslationContext();    const backend = useBackendContext(); @@ -104,7 +113,6 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {                  label={i18n.str`Identifier`}                  tooltip={i18n.str`Name of the template in URLs.`}                /> -                <Input<Entity>                  name="template_description"                  label={i18n.str`Description`} @@ -134,12 +142,35 @@ 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.`}                /> -              <Input<Entity> -                name="pos_key" -                label={i18n.str`Point-of-sale key`} -                help="" -                tooltip={i18n.str`Useful to validate the purchase`} +              <InputSelector<Entity> +                name="pos_algorithm" +                label={i18n.str`Veritifaction algorithm`} +                tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`} +                values={algorithms} +                toStr={(v) => algorithmsNames[v]} +                convert={(v) => Number(v)}                /> +              {state.pos_algorithm && state.pos_algorithm > 0 ? ( +                <Input<Entity> +                  name="pos_key" +                  label={i18n.str`Point-of-sale key`} +                  help="" +                  tooltip={i18n.str`Useful to validate the purchase`} +                  side={ +                    <span data-tooltip={i18n.str`generate random secret key`}> +                      <button +                        class="button is-info mr-3" +                        onClick={(e) => { +                          const pos_key = randomBase32Key(); +                          setState((s) => ({ ...s, pos_key })); +                        }} +                      > +                        <i18n.Translate>random</i18n.Translate> +                      </button> +                    </span> +                  } +                /> +              ) : undefined}              </FormProvider>              <div class="buttons is-right mt-5"> 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 756909d15..66ac72ff5 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 @@ -31,8 +31,10 @@ import {  } from "../../../../components/form/FormProvider.js";  import { Input } from "../../../../components/form/Input.js";  import { InputCurrency } from "../../../../components/form/InputCurrency.js"; +import { ConfirmModal } from "../../../../components/modal/index.js";  import { useBackendContext } from "../../../../context/backend.js";  import { useConfigContext } from "../../../../context/config.js"; +import { useInstanceContext } from "../../../../context/instance.js";  import { MerchantBackend } from "../../../../declaration.js";  type Entity = MerchantBackend.Template.UsingTemplateDetails; @@ -46,7 +48,9 @@ interface Props {  export function QrPage({ template, 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, @@ -82,8 +86,33 @@ export function QrPage({ template, id: templateId, onBack }: Props): VNode {    const payTemplateUri = `${talerProto}//pay-template/${merchantURL.hostname}/${templateId}${paramsStr}`; +  const issuer = encodeURIComponent( +    `${new URL(backendUrl).hostname}/${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;    return (      <div> +      {oauthUri && ( +        <ConfirmModal +          description="Setup TOTP" +          active={setupTOTP} +          onConfirm={() => { +            setSetupTOTP(false); +          }} +        > +          <p>Scan this qr code with your TOTP device</p> +          <QR text={oauthUri} /> +          <pre style={{ textAlign: "center" }}> +            <a href={oauthUri}>{oauthUri}</a> +          </pre> +        </ConfirmModal> +      )}        <section class="section is-main-section">          <div class="columns">            <div class="column" /> @@ -114,20 +143,48 @@ export function QrPage({ template, id: templateId, onBack }: Props): VNode {                    <i18n.Translate>Cancel</i18n.Translate>                  </button>                )} -              <button class="button is-info" onClick={onBack}> +              <button +                class="button is-info" +                onClick={() => saveAsPDF(templateId)} +              >                  <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" />          </div>        </section> -      <section> -        <pre> +      <section id="printThis"> +        <QR text={payTemplateUri} /> +        <pre style={{ textAlign: "center" }}>            <a href={payTemplateUri}>{payTemplateUri}</a>          </pre> -        <QR text={payTemplateUri} />        </section>      </div>    );  } + +function saveAsPDF(name: string): void { +  const printWindow = window.open("", "", "height=400,width=800"); +  if (!printWindow) return; +  const divContents = document.getElementById("printThis"); +  if (!divContents) return; +  printWindow.document.write( +    `<html><head><title>Order template for ${name}</title><style>`, +  ); +  printWindow.document.write("</style></head><body> </body></html>"); +  printWindow.document.close(); +  printWindow.document.body.appendChild(divContents.cloneNode(true)); +  printWindow.addEventListener("load", () => { +    printWindow.print(); +    printWindow.close(); +  }); +} 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 97d25b700..044cc7d79 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 @@ -51,10 +51,8 @@ export default function TemplateQrPage({    onNotFound,    onUnauthorized,  }: Props): VNode { -  const { createOrderFromTemplate } = useTemplateAPI();    const result = useTemplateDetails(tid);    const [notif, setNotif] = useState<Notification | undefined>(undefined); -  const { i18n } = useTranslationContext();    if (result.clientError && result.isUnauthorized) return onUnauthorized();    if (result.clientError && result.isNotfound) return onNotFound(); 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 eba212517..e34e2c746 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 @@ -31,9 +31,11 @@ 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 { MerchantBackend, WithId } from "../../../../declaration.js"; +import { randomBase32Key } from "../../../../utils/crypto.js";  import { undefinedIfEmpty } from "../../../../utils/table.js";  type Entity = MerchantBackend.Template.TemplatePatchDetails & WithId; @@ -44,6 +46,13 @@ interface Props {    template: Entity;  } +const algorithms = ["0", "1", "2"]; +const algorithmsNames = [ +  "off", +  "30s 8d TOTP-SHA1 without amount", +  "30s 8d eTOTP-SHA1 with amount", +]; +  export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {    const { i18n } = useTranslationContext();    const backend = useBackendContext(); @@ -143,12 +152,35 @@ 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.`}                  /> -                <Input<Entity> -                  name="pos_key" -                  label={i18n.str`Point-of-sale key`} -                  help="" -                  tooltip={i18n.str`Useful to validate the purchase`} +                <InputSelector<Entity> +                  name="pos_algorithm" +                  label={i18n.str`Veritifaction algorithm`} +                  tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`} +                  values={algorithms} +                  toStr={(v) => algorithmsNames[v]} +                  convert={(v) => Number(v)}                  /> +                {state.pos_algorithm && state.pos_algorithm > 0 ? ( +                  <Input<Entity> +                    name="pos_key" +                    label={i18n.str`Point-of-sale key`} +                    help="" +                    tooltip={i18n.str`Useful to validate the purchase`} +                    side={ +                      <span data-tooltip={i18n.str`generate random secret key`}> +                        <button +                          class="button is-info mr-3" +                          onClick={(e) => { +                            const pos_key = randomBase32Key(); +                            setState((s) => ({ ...s, pos_key })); +                          }} +                        > +                          <i18n.Translate>random</i18n.Translate> +                        </button> +                      </span> +                    } +                  /> +                ) : undefined}                </FormProvider>                <div class="buttons is-right mt-5"> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx index a63469763..5abc6b153 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx @@ -34,12 +34,13 @@ import { MerchantBackend } from "../../../../declaration.js";  type Entity = MerchantBackend.Template.UsingTemplateDetails;  interface Props { +  id: string;    template: MerchantBackend.Template.TemplateDetails;    onCreateOrder: (d: Entity) => Promise<void>;    onBack?: () => void;  } -export function UsePage({ template, onCreateOrder, onBack }: Props): VNode { +export function UsePage({ id, template, onCreateOrder, onBack }: Props): VNode {    const { i18n } = useTranslationContext();    const [state, setState] = useState<Partial<Entity>>({ @@ -75,6 +76,22 @@ export function UsePage({ template, onCreateOrder, onBack }: Props): VNode {    return (      <div> +      <section class="section"> +        <section class="hero is-hero-bar"> +          <div class="hero-body"> +            <div class="level"> +              <div class="level-left"> +                <div class="level-item"> +                  <span class="is-size-4"> +                    <i18n.Translate>New order for template</i18n.Translate>:{" "} +                    <b>{id}</b> +                  </span> +                </div> +              </div> +            </div> +          </div> +        </section> +      </section>        <section class="section is-main-section">          <div class="columns">            <div class="column" /> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx index d5fa6d39d..b6175bcfb 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx @@ -68,6 +68,7 @@ export default function TemplateUsePage({        <NotificationCard notification={notif} />        <UsePage          template={result.data} +        id={tid}          onBack={onBack}          onCreateOrder={(            request: MerchantBackend.Template.UsingTemplateDetails, diff --git a/packages/merchant-backoffice-ui/src/utils/crypto.ts b/packages/merchant-backoffice-ui/src/utils/crypto.ts new file mode 100644 index 000000000..7bab8abf1 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/utils/crypto.ts @@ -0,0 +1,53 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +const encTable = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; +// base32 RFC 3548 +function encodeBase32(data: ArrayBuffer) { +  const dataBytes = new Uint8Array(data); +  let sb = ""; +  const size = data.byteLength; +  let bitBuf = 0; +  let numBits = 0; +  let pos = 0; +  while (pos < size || numBits > 0) { +    if (pos < size && numBits < 5) { +      const d = dataBytes[pos++]; +      bitBuf = (bitBuf << 8) | d; +      numBits += 8; +    } +    if (numBits < 5) { +      // zero-padding +      bitBuf = bitBuf << (5 - numBits); +      numBits = 5; +    } +    const v = (bitBuf >>> (numBits - 5)) & 31; +    sb += encTable[v]; +    numBits -= 5; +  } +  return sb; +} + +export function randomBase32Key(): string { +  var buf = new Uint8Array(20); +  window.crypto.getRandomValues(buf); +  return encodeBase32(buf); +} | 
