gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] 02/03: print and setup totp


From: gnunet
Subject: [taler-wallet-core] 02/03: print and setup totp
Date: Mon, 13 Mar 2023 04:02:08 +0100

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository wallet-core.

commit b874f9a0c50084803de58febb698864aa8dd061a
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Sun Mar 12 23:56:54 2023 -0300

    print and setup totp
---
 .../src/components/form/InputSelector.tsx          |  7 ++-
 .../merchant-backoffice-ui/src/declaration.d.ts    |  9 +++
 .../merchant-backoffice-ui/src/hooks/templates.ts  |  6 +-
 .../paths/instance/templates/create/CreatePage.tsx | 43 ++++++++++++--
 .../src/paths/instance/templates/qr/QrPage.tsx     | 65 ++++++++++++++++++++--
 .../src/paths/instance/templates/qr/index.tsx      |  2 -
 .../paths/instance/templates/update/UpdatePage.tsx | 42 ++++++++++++--
 .../src/paths/instance/templates/use/UsePage.tsx   | 19 ++++++-
 .../src/paths/instance/templates/use/index.tsx     |  1 +
 .../merchant-backoffice-ui/src/utils/crypto.ts     | 53 ++++++++++++++++++
 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>&nbsp;</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);
+}

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]