Use new 2FA flow for login

This commit is contained in:
SleeplessOne1917 2023-10-03 19:50:00 -04:00
parent ad17358fb3
commit 98961d2ada
2 changed files with 66 additions and 62 deletions

View file

@ -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)}

View file

@ -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,17 +26,34 @@ 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 handleLoginSuccess(i: Login, loginRes: LoginResponse) {
UserService.Instance.login({
res: loginRes,
});
const site = await HttpService.client.getSite();
if (site.state === "success") {
UserService.Instance.myUserInfo = site.data.my_user;
}
const { prev } = getLoginQueryParams();
prev
? i.props.history.replace(prev)
: i.props.history.action === "PUSH"
? i.props.history.back()
: i.props.history.replace("/");
}
async function handleLoginSubmit(i: Login, event: any) {
event.preventDefault();
const { password, totp_2fa_token, username_or_email } = i.state.form;
const { password, username_or_email } = i.state.form;
if (username_or_email && password) {
i.setState({ loginRes: { state: "loading" } });
@ -43,39 +61,23 @@ async function handleLoginSubmit(i: Login, event: any) {
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");
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: { state: "failed", msg: loginRes.msg } });
i.setState({ loginRes });
break;
}
case "success": {
UserService.Instance.login({
res: loginRes.data,
});
const site = await HttpService.client.getSite();
if (site.state === "success") {
UserService.Instance.myUserInfo = site.data.my_user;
}
const { prev } = getLoginQueryParams();
prev
? i.props.history.replace(prev)
: i.props.history.action === "PUSH"
? i.props.history.back()
: i.props.history.replace("/");
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">