mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-11-25 05:41:13 +00:00
Make modal work when enabling and disabling 2FA
This commit is contained in:
parent
bf7b75e070
commit
5f611b8561
5 changed files with 215 additions and 75 deletions
|
@ -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",
|
||||
|
|
|
@ -448,3 +448,7 @@ br.big {
|
|||
.skip-link:focus {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.totp-link {
|
||||
width: fit-content;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
}),
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
65
yarn.lock
65
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue