Make modal work when enabling and disabling 2FA

This commit is contained in:
SleeplessOne1917 2023-10-03 17:00:34 -04:00
parent bf7b75e070
commit 5f611b8561
5 changed files with 215 additions and 75 deletions

View file

@ -42,6 +42,7 @@
"@babel/preset-typescript": "^7.21.5",
"@babel/runtime": "^7.21.5",
"@emoji-mart/data": "^1.1.0",
"@shortcm/qr-image": "^9.0.2",
"autosize": "^6.0.1",
"babel-loader": "^9.1.2",
"babel-plugin-inferno": "^6.6.0",

View file

@ -448,3 +448,7 @@ br.big {
.skip-link:focus {
top: 0;
}
.totp-link {
width: fit-content;
}

View file

@ -1,34 +1,70 @@
import { Component, linkEvent } from "inferno";
import { I18NextService } from "../../services";
import { toast } from "../../toast";
interface TotpModalProps {
onSubmit: (totp: string) => void;
/**Takes totp as param, returns whether submit was successful*/
onSubmit: (totp: string) => Promise<boolean>;
type: "login" | "remove" | "generate";
secretUrl?: string;
}
interface TotpModalState {
totp: string;
qrCode?: string;
}
const TOTP_LENGTH = 6;
function focusInput() {
document.getElementById("totp-input-0")?.focus();
async function handleSubmit(modal: TotpModal, totp: string) {
const succeeded = await modal.props.onSubmit(totp);
modal.setState({ totp: "" });
if (succeeded) {
document.getElementById("totp-close-button")?.click();
} else {
document.getElementById(`totp-input-0`)?.focus();
}
}
function handleInput(
{ modal, i }: { modal: TotpModal; i: number },
event: any,
) {
const { totp } = modal.state;
modal.setState(prev => ({ ...prev, totp: prev.totp + event.target.value }));
document.getElementById(`totp-input-${i + 1}`)?.focus();
const { totp } = modal.state;
if (totp.length >= TOTP_LENGTH) {
modal.props.onSubmit(totp);
} else {
handleSubmit(modal, totp);
}
}
function handleKeyUp(
{ modal, i }: { modal: TotpModal; i: number },
event: any,
) {
if (event.key === "Backspace" && i > 0) {
event.preventDefault();
modal.setState(prev => ({
totp: prev.totp + event.target.value,
...prev,
totp: prev.totp.slice(0, prev.totp.length - 1),
}));
document.getElementById(`totp-input-${i + 1}`)?.focus();
document.getElementById(`totp-input-${i - 1}`)?.focus();
}
}
function handlePaste(modal: TotpModal, event: any) {
event.preventDefault();
const text: string = event.clipboardData.getData("text");
if (text.length > TOTP_LENGTH || isNaN(Number(text))) {
toast("Invalid TOTP: Must be string of six digits", "danger");
modal.setState({ totp: "" });
} else {
modal.setState({ totp: text });
handleSubmit(modal, text);
}
}
@ -44,12 +80,13 @@ export default class TotpModal extends Component<
super(props, context);
this.clearTotp = this.clearTotp.bind(this);
this.handleShow = this.handleShow.bind(this);
}
componentDidMount() {
async componentDidMount() {
document
.getElementById("totpModal")
?.addEventListener("shown.bs.modal", focusInput);
?.addEventListener("shown.bs.modal", this.handleShow);
document
.getElementById("totpModal")
@ -59,7 +96,7 @@ export default class TotpModal extends Component<
componentWillUnmount() {
document
.getElementById("totpModal")
?.removeEventListener("shown.bs.modal", focusInput);
?.removeEventListener("shown.bs.modal", this.handleShow);
document
.getElementById("totpModal")
@ -67,7 +104,7 @@ export default class TotpModal extends Component<
}
render() {
const { type } = this.props;
const { type, secretUrl } = this.props;
const { totp } = this.state;
return (
@ -93,39 +130,61 @@ export default class TotpModal extends Component<
className="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
id="totp-close-button"
/>
</header>
<form
id="totp-form"
className="modal-body d-flex flex-column justify-content-center"
>
<label
className="form-label"
id="totp-input-label"
htmlFor="totp-input-0"
>
Enter TOTP
</label>
<div className="d-flex justify-content-between align-items-center p-4">
{Array.from(Array(TOTP_LENGTH).keys()).map(i => (
<input
key={
i /*While using indices as keys is usually bad practice, in this case we don't have to worry about the order of the list items changing.*/
}
type="text"
inputMode="numeric"
autoComplete="one-time-code"
maxLength={1}
value={totp[i] ?? ""}
disabled={totp.length !== i}
aria-labelledby="totp-input-label"
id={`totp-input-${i}`}
className="form-control form-control-lg mx-2"
onInput={linkEvent({ modal: this, i }, handleInput)}
/>
))}
</div>
</form>
<div className="modal-body">
{type === "generate" && (
<div className="mx-auto">
<a
className="btn btn-secondary mx-auto d-block totp-link"
href={secretUrl}
>
Click here for your TOTP link
</a>
<div className="mx-auto mt-3 w-50 h-50 text-center">
<span className="fw-semibold">
or scan this QR code in your authenticator app
</span>
<img
src={this.state.qrCode}
className="d-block mt-1 mx-auto"
alt="TOTP QR code"
/>
</div>
</div>
)}
<form id="totp-form">
<label
className="form-label ms-2 mt-4 fw-bold"
id="totp-input-label"
htmlFor="totp-input-0"
>
Enter TOTP
</label>
<div className="d-flex justify-content-between align-items-center p-2">
{Array.from(Array(TOTP_LENGTH).keys()).map(i => (
<input
key={
i /*While using indices as keys is usually bad practice, in this case we don't have to worry about the order of the list items changing.*/
}
type="text"
inputMode="numeric"
autoComplete="one-time-code"
maxLength={1}
value={totp[i] ?? ""}
disabled={totp.length !== i}
aria-labelledby="totp-input-label"
id={`totp-input-${i}`}
className="form-control form-control-lg mx-2"
onInput={linkEvent({ modal: this, i }, handleInput)}
onKeyUp={linkEvent({ modal: this, i }, handleKeyUp)}
onPaste={linkEvent(this, handlePaste)}
/>
))}
</div>
</form>
</div>
<footer className="modal-footer">
<button
type="button"
@ -142,7 +201,22 @@ export default class TotpModal extends Component<
}
clearTotp() {
console.log("clearing");
this.setState({ totp: "" });
}
async handleShow() {
document.getElementById("totp-input-0")?.focus();
if (this.props.type === "generate") {
const { getSVG } = await import("@shortcm/qr-image/lib/svg");
this.setState({
qrCode: URL.createObjectURL(
new Blob([(await getSVG(this.props.secretUrl!)).buffer], {
type: "image/svg+xml",
}),
),
});
}
}
}

View file

@ -1038,35 +1038,29 @@ export class Settings extends Component<any, SettingsState> {
return (
<>
<button
type="button"
className="btn btn-secondary my-2"
onClick={
!totpEnabled ? linkEvent(this, handleGenerateTotp) : undefined
}
data-bs-toggle="modal"
data-bs-target="#totpModal"
>{`${
totpEnabled ? "Disable" : "Enable"
} 2 factor authentication`}</button>
{totpEnabled ? (
<div>
<TotpModal type="remove" onSubmit={this.handleDisable2fa} />
<button type="button" className="btn btn-secondary">
Disable 2 factor authentication
</button>
</div>
<TotpModal type="remove" onSubmit={this.handleDisable2fa} />
) : (
<div>
<TotpModal
type="generate"
secretUrl={
generateTotpRes.state === "success"
? generateTotpRes.data.totp_secret_url
: undefined
}
onSubmit={this.handleEnable2fa}
/>
<button
type="button"
className="btn btn-secondary"
onClick={linkEvent(this, handleGenerateTotp)}
data-bs-toggle="modal"
data-bs-target="#totpModal"
>
Enable 2 factor authentication
</button>
</div>
<TotpModal
type="generate"
onSubmit={this.handleEnable2fa}
secretUrl={
generateTotpRes.state === "success"
? generateTotpRes.data.totp_secret_url
: undefined
}
/>
)}
</>
);
@ -1080,19 +1074,23 @@ export class Settings extends Component<any, SettingsState> {
totp_token: totp,
});
console.log("updating 2fa");
if (updateTotpRes.state === "failed") {
toast(updateTotpRes.msg, "danger");
toast("Invalid TOTP", "danger");
}
this.setState({ updateTotpRes });
return updateTotpRes.state === "success";
}
handleEnable2fa(totp: string) {
this.handleToggle2fa(totp, true);
return this.handleToggle2fa(totp, true);
}
handleDisable2fa(totp: string) {
this.handleToggle2fa(totp, false);
return this.handleToggle2fa(totp, false);
}
handlePersonSearch = debounce(async (text: string) => {

View file

@ -1613,6 +1613,20 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@pdf-lib/standard-fonts@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz#8ba691c4421f71662ed07c9a0294b44528af2d7f"
integrity sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==
dependencies:
pako "^1.0.6"
"@pdf-lib/upng@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@pdf-lib/upng/-/upng-1.0.1.tgz#7dc9c636271aca007a9df4deaf2dd7e7960280cb"
integrity sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==
dependencies:
pako "^1.0.10"
"@pkgjs/parseargs@^0.11.0":
version "0.11.0"
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
@ -1685,6 +1699,16 @@
domhandler "^5.0.3"
selderee "^0.11.0"
"@shortcm/qr-image@^9.0.2":
version "9.0.2"
resolved "https://registry.yarnpkg.com/@shortcm/qr-image/-/qr-image-9.0.2.tgz#a24ed06026466974badb7fc7fc863d704d496bbe"
integrity sha512-/hz2NqFlT0Xmd5FDiYSsb/lDucZbByWeFUiEz1ekFnz6MHtdpv03mSMSsLm+LF8n/LgumjBcKci3gG2TMirIJA==
dependencies:
color-string "^1.9.1"
js-base64 "^3.7.5"
pdf-lib "^1.17.1"
sharp "^0.32.5"
"@surma/rollup-plugin-off-main-thread@^2.2.3":
version "2.2.3"
resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053"
@ -3210,7 +3234,7 @@ color-name@^1.0.0, color-name@~1.1.4:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
color-string@^1.9.0:
color-string@^1.9.0, color-string@^1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4"
integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==
@ -5874,6 +5898,11 @@ jest-worker@^27.4.5:
merge-stream "^2.0.0"
supports-color "^8.0.0"
js-base64@^3.7.5:
version "3.7.5"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.5.tgz#21e24cf6b886f76d6f5f165bfcd69cc55b9e3fca"
integrity sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@ -7451,6 +7480,11 @@ pacote@^8.1.6:
unique-filename "^1.1.0"
which "^1.3.0"
pako@^1.0.10, pako@^1.0.11, pako@^1.0.6:
version "1.0.11"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
parallel-transform@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc"
@ -7570,6 +7604,16 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
pdf-lib@^1.17.1:
version "1.17.1"
resolved "https://registry.yarnpkg.com/pdf-lib/-/pdf-lib-1.17.1.tgz#9e7dd21261a0c1fb17992580885b39e7d08f451f"
integrity sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==
dependencies:
"@pdf-lib/standard-fonts" "^1.0.0"
"@pdf-lib/upng" "^1.0.1"
pako "^1.0.11"
tslib "^1.11.1"
peberminta@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/peberminta/-/peberminta-0.9.0.tgz#8ec9bc0eb84b7d368126e71ce9033501dca2a352"
@ -8570,6 +8614,20 @@ sharp@^0.32.4:
tar-fs "^3.0.4"
tunnel-agent "^0.6.0"
sharp@^0.32.5:
version "0.32.6"
resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.32.6.tgz#6ad30c0b7cd910df65d5f355f774aa4fce45732a"
integrity sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==
dependencies:
color "^4.2.3"
detect-libc "^2.0.2"
node-addon-api "^6.1.0"
prebuild-install "^7.1.1"
semver "^7.5.4"
simple-get "^4.0.1"
tar-fs "^3.0.4"
tunnel-agent "^0.6.0"
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@ -9386,6 +9444,11 @@ ts-api-utils@^1.0.1:
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.1.tgz#8144e811d44c749cd65b2da305a032510774452d"
integrity sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==
tslib@^1.11.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.1.0, tslib@^2.5.0, tslib@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.0.tgz#b295854684dbda164e181d259a22cd779dcd7bc3"