show qr to import TOTP into other app

This commit is contained in:
Sebastian 2023-06-23 10:36:24 -03:00
parent 9dbf0bd7d2
commit 4f30506dca
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
2 changed files with 143 additions and 93 deletions

View File

@ -24,7 +24,7 @@ import {
MerchantTemplateContractDetails, MerchantTemplateContractDetails,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser"; 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 { useState } from "preact/hooks";
import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
import { import {
@ -44,6 +44,8 @@ import {
randomBase32Key, randomBase32Key,
} from "../../../../utils/crypto.js"; } from "../../../../utils/crypto.js";
import { undefinedIfEmpty } from "../../../../utils/table.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; 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 { export function CreatePage({ onCreate, onBack }: Props): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const backend = useBackendContext(); const backend = useBackendContext();
const { id: instanceId } = useInstanceContext();
const issuer = new URL(backend.url).hostname;
const [showKey, setShowKey] = useState(false); const [showKey, setShowKey] = useState(false);
const [state, setState] = useState<Partial<Entity>>({ const [state, setState] = useState<Partial<Entity>>({
@ -120,6 +124,8 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
return onCreate(state as any); 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 ( return (
<div> <div>
<section class="section is-main-section"> <section class="section is-main-section">
@ -175,54 +181,73 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
fromStr={(v) => Number(v)} fromStr={(v) => Number(v)}
/> />
{state.pos_algorithm && state.pos_algorithm > 0 ? ( {state.pos_algorithm && state.pos_algorithm > 0 ? (
<InputWithAddon<Entity> <Fragment>
name="pos_key" <InputWithAddon<Entity>
label={i18n.str`Point-of-sale key`} name="pos_key"
inputType={showKey ? "text" : "password"} label={i18n.str`Point-of-sale key`}
help="Be sure to be very hard to guess or use the random generator" inputType={showKey ? "text" : "password"}
tooltip={i18n.str`Useful to validate the purchase`} help="Be sure to be very hard to guess or use the random generator"
fromStr={(v) => v.toUpperCase()} tooltip={i18n.str`Useful to validate the purchase`}
addonAfter={ fromStr={(v) => v.toUpperCase()}
<span class="icon"> addonAfter={
{showKey ? ( <span class="icon">
<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 ? ( {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} ) : undefined}
</FormProvider> </FormProvider>

View File

@ -24,7 +24,7 @@ import {
MerchantTemplateContractDetails, MerchantTemplateContractDetails,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser"; 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 { useState } from "preact/hooks";
import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
import { import {
@ -44,6 +44,8 @@ import {
randomBase32Key, randomBase32Key,
} from "../../../../utils/crypto.js"; } from "../../../../utils/crypto.js";
import { undefinedIfEmpty } from "../../../../utils/table.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; 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 { export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const backend = useBackendContext(); const backend = useBackendContext();
const { id: instanceId } = useInstanceContext();
const issuer = new URL(backend.url).hostname;
const [showKey, setShowKey] = useState(false); const [showKey, setShowKey] = useState(false);
const [state, setState] = useState<Partial<Entity>>(template); const [state, setState] = useState<Partial<Entity>>(template);
@ -113,6 +117,8 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
return onUpdate(state as any); 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 ( return (
<div> <div>
<section class="section"> <section class="section">
@ -185,55 +191,74 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
fromStr={(v) => Number(v)} fromStr={(v) => Number(v)}
/> />
{state.pos_algorithm && state.pos_algorithm > 0 ? ( {state.pos_algorithm && state.pos_algorithm > 0 ? (
<InputWithAddon<Entity> <Fragment>
name="pos_key" <InputWithAddon<Entity>
label={i18n.str`Point-of-sale key`} name="pos_key"
inputType={showKey ? "text" : "password"} label={i18n.str`Point-of-sale key`}
help="Be sure to be very hard to guess or use the random generator" inputType={showKey ? "text" : "password"}
expand help="Be sure to be very hard to guess or use the random generator"
tooltip={i18n.str`Useful to validate the purchase`} expand
fromStr={(v) => v.toUpperCase()} tooltip={i18n.str`Useful to validate the purchase`}
addonAfter={ fromStr={(v) => v.toUpperCase()}
<span class="icon"> addonAfter={
{showKey ? ( <span class="icon">
<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 ? ( {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} ) : undefined}
</FormProvider> </FormProvider>