show qr to import TOTP into other app
This commit is contained in:
parent
9dbf0bd7d2
commit
4f30506dca
@ -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>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user