diff options
author | Sebastian <sebasjm@gmail.com> | 2023-06-23 10:36:24 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2023-06-23 10:36:24 -0300 |
commit | 4f30506dcacc587586381b1a8fa20c5442784e41 (patch) | |
tree | 5f7dafbaf96727c589ad82516efaa7830610f429 /packages/merchant-backoffice-ui | |
parent | 9dbf0bd7d2dc9353aa210a0c33faf0a5c5086f73 (diff) |
show qr to import TOTP into other app
Diffstat (limited to 'packages/merchant-backoffice-ui')
-rw-r--r-- | packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx | 117 | ||||
-rw-r--r-- | packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx | 119 |
2 files changed, 143 insertions, 93 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 137d50b3e..4dde202c4 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 { h, VNode } from "preact"; +import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { @@ -44,6 +44,8 @@ import { randomBase32Key, } from "../../../../utils/crypto.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; +import { QR } from "../../../../components/exception/QR.js"; +import { useInstanceContext } from "../../../../context/instance.js"; type Entity = MerchantBackend.Template.TemplateAddDetails; @@ -58,6 +60,8 @@ 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 [showKey, setShowKey] = useState(false); const [state, setState] = useState<Partial<Entity>>({ @@ -120,6 +124,8 @@ 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}`; + return ( <div> <section class="section is-main-section"> @@ -175,54 +181,73 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { fromStr={(v) => Number(v)} /> {state.pos_algorithm && state.pos_algorithm > 0 ? ( - <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); - }} - > + <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 ? ( - <i18n.Translate>hide</i18n.Translate> + <i class="mdi mdi-eye" /> ) : ( - <i18n.Translate>show</i18n.Translate> + <i class="mdi mdi-eye-off" /> )} - </button> - </span> - } - /> + </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> 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 20e40cf9b..30e5502bb 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 @@ -24,7 +24,7 @@ import { MerchantTemplateContractDetails, } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; +import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { @@ -44,6 +44,8 @@ import { randomBase32Key, } from "../../../../utils/crypto.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; +import { QR } from "../../../../components/exception/QR.js"; +import { useInstanceContext } from "../../../../context/instance.js"; type Entity = MerchantBackend.Template.TemplatePatchDetails & WithId; @@ -59,6 +61,8 @@ 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); @@ -113,6 +117,8 @@ 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> <section class="section"> @@ -185,55 +191,74 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { fromStr={(v) => Number(v)} /> {state.pos_algorithm && state.pos_algorithm > 0 ? ( - <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); - }} - > + <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 ? ( - <i18n.Translate>hide</i18n.Translate> + <i class="mdi mdi-eye" /> ) : ( - <i18n.Translate>show</i18n.Translate> + <i class="mdi mdi-eye-off" /> )} - </button> - </span> - } - /> + </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> |