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