mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-11-25 13:51:13 +00:00
Use new 2FA flow for login
This commit is contained in:
parent
ad17358fb3
commit
98961d2ada
2 changed files with 66 additions and 62 deletions
|
@ -31,6 +31,11 @@ function handleInput(
|
|||
{ modal, i }: { modal: TotpModal; i: number },
|
||||
event: any,
|
||||
) {
|
||||
if (isNaN(event.target.value)) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
modal.setState(prev => ({ ...prev, totp: prev.totp + event.target.value }));
|
||||
document.getElementById(`totp-input-${i + 1}`)?.focus();
|
||||
|
||||
|
@ -114,16 +119,17 @@ export default class TotpModal extends Component<
|
|||
tabIndex={-1}
|
||||
aria-hidden
|
||||
aria-labelledby="#totpModalTitle"
|
||||
data-bs-backdrop="static"
|
||||
>
|
||||
<div className="modal-dialog modal-fullscreen-sm-down">
|
||||
<div className="modal-content">
|
||||
<header className="modal-header">
|
||||
<h3 className="modal-title" id="totpModalTitle">
|
||||
{type === "generate"
|
||||
? "Generate TOTP"
|
||||
? "Enable 2 Factor Authentication"
|
||||
: type === "remove"
|
||||
? "Remove TOTP"
|
||||
: "Enter TOTP"}
|
||||
? "Disable 2 Factor Authentication"
|
||||
: "Enter 2FA Token"}
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
|
@ -133,9 +139,9 @@ export default class TotpModal extends Component<
|
|||
id="totp-close-button"
|
||||
/>
|
||||
</header>
|
||||
<div className="modal-body">
|
||||
<div className="modal-body d-flex flex-column align-items-center justify-content-center">
|
||||
{type === "generate" && (
|
||||
<div className="mx-auto">
|
||||
<div>
|
||||
<a
|
||||
className="btn btn-secondary mx-auto d-block totp-link"
|
||||
href={secretUrl}
|
||||
|
@ -143,9 +149,9 @@ export default class TotpModal extends Component<
|
|||
Click here for your TOTP link
|
||||
</a>
|
||||
<div className="mx-auto mt-3 w-50 h-50 text-center">
|
||||
<span className="fw-semibold">
|
||||
<strong className="fw-semibold">
|
||||
or scan this QR code in your authenticator app
|
||||
</span>
|
||||
</strong>
|
||||
<img
|
||||
src={this.state.qrCode}
|
||||
className="d-block mt-1 mx-auto"
|
||||
|
@ -176,7 +182,7 @@ export default class TotpModal extends Component<
|
|||
disabled={totp.length !== i}
|
||||
aria-labelledby="totp-input-label"
|
||||
id={`totp-input-${i}`}
|
||||
className="form-control form-control-lg mx-2"
|
||||
className="form-control form-control-lg mx-2 p-1 p-md-2 text-center"
|
||||
onInput={linkEvent({ modal: this, i }, handleInput)}
|
||||
onKeyUp={linkEvent({ modal: this, i }, handleKeyUp)}
|
||||
onPaste={linkEvent(this, handlePaste)}
|
||||
|
|
|
@ -10,6 +10,7 @@ import { toast } from "../../toast";
|
|||
import { HtmlTags } from "../common/html-tags";
|
||||
import { Spinner } from "../common/icon";
|
||||
import PasswordInput from "../common/password-input";
|
||||
import TotpModal from "../common/totp-modal";
|
||||
|
||||
interface LoginProps {
|
||||
prev?: string;
|
||||
|
@ -25,42 +26,15 @@ const getLoginQueryParams = () =>
|
|||
interface State {
|
||||
loginRes: RequestState<LoginResponse>;
|
||||
form: {
|
||||
username_or_email?: string;
|
||||
password?: string;
|
||||
totp_2fa_token?: string;
|
||||
username_or_email: string;
|
||||
password: string;
|
||||
};
|
||||
showTotp: boolean;
|
||||
siteRes: GetSiteResponse;
|
||||
}
|
||||
|
||||
async function handleLoginSubmit(i: Login, event: any) {
|
||||
event.preventDefault();
|
||||
const { password, totp_2fa_token, username_or_email } = i.state.form;
|
||||
|
||||
if (username_or_email && password) {
|
||||
i.setState({ loginRes: { state: "loading" } });
|
||||
|
||||
const loginRes = await HttpService.client.login({
|
||||
username_or_email,
|
||||
password,
|
||||
totp_2fa_token,
|
||||
});
|
||||
switch (loginRes.state) {
|
||||
case "failed": {
|
||||
if (loginRes.msg === "missing_totp_token") {
|
||||
i.setState({ showTotp: true });
|
||||
toast(I18NextService.i18n.t("enter_two_factor_code"), "info");
|
||||
} else {
|
||||
toast(I18NextService.i18n.t(loginRes.msg), "danger");
|
||||
}
|
||||
|
||||
i.setState({ loginRes: { state: "failed", msg: loginRes.msg } });
|
||||
break;
|
||||
}
|
||||
|
||||
case "success": {
|
||||
async function handleLoginSuccess(i: Login, loginRes: LoginResponse) {
|
||||
UserService.Instance.login({
|
||||
res: loginRes.data,
|
||||
res: loginRes,
|
||||
});
|
||||
const site = await HttpService.client.getSite();
|
||||
|
||||
|
@ -75,7 +49,35 @@ async function handleLoginSubmit(i: Login, event: any) {
|
|||
: i.props.history.action === "PUSH"
|
||||
? i.props.history.back()
|
||||
: i.props.history.replace("/");
|
||||
}
|
||||
|
||||
async function handleLoginSubmit(i: Login, event: any) {
|
||||
event.preventDefault();
|
||||
const { password, username_or_email } = i.state.form;
|
||||
|
||||
if (username_or_email && password) {
|
||||
i.setState({ loginRes: { state: "loading" } });
|
||||
|
||||
const loginRes = await HttpService.client.login({
|
||||
username_or_email,
|
||||
password,
|
||||
});
|
||||
switch (loginRes.state) {
|
||||
case "failed": {
|
||||
if (loginRes.msg === "missing_totp_token") {
|
||||
const Modal = (await import("bootstrap/js/dist/modal")).default;
|
||||
const modal = new Modal(document.getElementById("totpModal")!);
|
||||
modal.show();
|
||||
} else {
|
||||
toast(I18NextService.i18n.t(loginRes.msg), "danger");
|
||||
}
|
||||
|
||||
i.setState({ loginRes });
|
||||
break;
|
||||
}
|
||||
|
||||
case "success": {
|
||||
handleLoginSuccess(i, loginRes.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -88,10 +90,6 @@ function handleLoginUsernameChange(i: Login, event: any) {
|
|||
);
|
||||
}
|
||||
|
||||
function handleLoginTotpChange(i: Login, event: any) {
|
||||
i.setState(prevState => (prevState.form.totp_2fa_token = event.target.value));
|
||||
}
|
||||
|
||||
function handleLoginPasswordChange(i: Login, event: any) {
|
||||
i.setState(prevState => (prevState.form.password = event.target.value));
|
||||
}
|
||||
|
@ -104,13 +102,17 @@ export class Login extends Component<
|
|||
|
||||
state: State = {
|
||||
loginRes: { state: "empty" },
|
||||
form: {},
|
||||
showTotp: false,
|
||||
form: {
|
||||
username_or_email: "",
|
||||
password: "",
|
||||
},
|
||||
siteRes: this.isoData.site_res,
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.handleSubmitTotp = this.handleSubmitTotp.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -137,6 +139,7 @@ export class Login extends Component<
|
|||
title={this.documentTitle}
|
||||
path={this.context.router.route.match.url}
|
||||
/>
|
||||
<TotpModal type="login" onSubmit={this.handleSubmitTotp} />
|
||||
<div className="row">
|
||||
<div className="col-12 col-lg-6 offset-lg-3">{this.loginForm()}</div>
|
||||
</div>
|
||||
|
@ -144,6 +147,23 @@ export class Login extends Component<
|
|||
);
|
||||
}
|
||||
|
||||
async handleSubmitTotp(totp: string) {
|
||||
const loginRes = await HttpService.client.login({
|
||||
password: this.state.form.password,
|
||||
username_or_email: this.state.form.username_or_email,
|
||||
totp_2fa_token: totp,
|
||||
});
|
||||
|
||||
const succeeded = loginRes.state === "success";
|
||||
if (succeeded) {
|
||||
handleLoginSuccess(this, loginRes.data);
|
||||
} else {
|
||||
toast("Invalid 2FA Token", "danger");
|
||||
}
|
||||
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
loginForm() {
|
||||
return (
|
||||
<div>
|
||||
|
@ -178,28 +198,6 @@ export class Login extends Component<
|
|||
showForgotLink
|
||||
/>
|
||||
</div>
|
||||
{this.state.showTotp && (
|
||||
<div className="mb-3 row">
|
||||
<label
|
||||
className="col-sm-6 col-form-label"
|
||||
htmlFor="login-totp-token"
|
||||
>
|
||||
{I18NextService.i18n.t("two_factor_token")}
|
||||
</label>
|
||||
<div className="col-sm-6">
|
||||
<input
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
className="form-control"
|
||||
id="login-totp-token"
|
||||
pattern="[0-9]*"
|
||||
autoComplete="one-time-code"
|
||||
value={this.state.form.totp_2fa_token}
|
||||
onInput={linkEvent(this, handleLoginTotpChange)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-3 row">
|
||||
<div className="col-sm-10">
|
||||
<button type="submit" className="btn btn-secondary">
|
||||
|
|
Loading…
Reference in a new issue