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 }, { modal, i }: { modal: TotpModal; i: number },
event: any, event: any,
) { ) {
if (isNaN(event.target.value)) {
event.preventDefault();
return;
}
modal.setState(prev => ({ ...prev, totp: prev.totp + event.target.value })); modal.setState(prev => ({ ...prev, totp: prev.totp + event.target.value }));
document.getElementById(`totp-input-${i + 1}`)?.focus(); document.getElementById(`totp-input-${i + 1}`)?.focus();
@ -114,16 +119,17 @@ export default class TotpModal extends Component<
tabIndex={-1} tabIndex={-1}
aria-hidden aria-hidden
aria-labelledby="#totpModalTitle" aria-labelledby="#totpModalTitle"
data-bs-backdrop="static"
> >
<div className="modal-dialog modal-fullscreen-sm-down"> <div className="modal-dialog modal-fullscreen-sm-down">
<div className="modal-content"> <div className="modal-content">
<header className="modal-header"> <header className="modal-header">
<h3 className="modal-title" id="totpModalTitle"> <h3 className="modal-title" id="totpModalTitle">
{type === "generate" {type === "generate"
? "Generate TOTP" ? "Enable 2 Factor Authentication"
: type === "remove" : type === "remove"
? "Remove TOTP" ? "Disable 2 Factor Authentication"
: "Enter TOTP"} : "Enter 2FA Token"}
</h3> </h3>
<button <button
type="button" type="button"
@ -133,9 +139,9 @@ export default class TotpModal extends Component<
id="totp-close-button" id="totp-close-button"
/> />
</header> </header>
<div className="modal-body"> <div className="modal-body d-flex flex-column align-items-center justify-content-center">
{type === "generate" && ( {type === "generate" && (
<div className="mx-auto"> <div>
<a <a
className="btn btn-secondary mx-auto d-block totp-link" className="btn btn-secondary mx-auto d-block totp-link"
href={secretUrl} href={secretUrl}
@ -143,9 +149,9 @@ export default class TotpModal extends Component<
Click here for your TOTP link Click here for your TOTP link
</a> </a>
<div className="mx-auto mt-3 w-50 h-50 text-center"> <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 or scan this QR code in your authenticator app
</span> </strong>
<img <img
src={this.state.qrCode} src={this.state.qrCode}
className="d-block mt-1 mx-auto" className="d-block mt-1 mx-auto"
@ -176,7 +182,7 @@ export default class TotpModal extends Component<
disabled={totp.length !== i} disabled={totp.length !== i}
aria-labelledby="totp-input-label" aria-labelledby="totp-input-label"
id={`totp-input-${i}`} 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)} onInput={linkEvent({ modal: this, i }, handleInput)}
onKeyUp={linkEvent({ modal: this, i }, handleKeyUp)} onKeyUp={linkEvent({ modal: this, i }, handleKeyUp)}
onPaste={linkEvent(this, handlePaste)} onPaste={linkEvent(this, handlePaste)}

View file

@ -10,6 +10,7 @@ import { toast } from "../../toast";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
import PasswordInput from "../common/password-input"; import PasswordInput from "../common/password-input";
import TotpModal from "../common/totp-modal";
interface LoginProps { interface LoginProps {
prev?: string; prev?: string;
@ -25,17 +26,34 @@ const getLoginQueryParams = () =>
interface State { interface State {
loginRes: RequestState<LoginResponse>; loginRes: RequestState<LoginResponse>;
form: { form: {
username_or_email?: string; username_or_email: string;
password?: string; password: string;
totp_2fa_token?: string;
}; };
showTotp: boolean;
siteRes: GetSiteResponse; 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) { async function handleLoginSubmit(i: Login, event: any) {
event.preventDefault(); 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) { if (username_or_email && password) {
i.setState({ loginRes: { state: "loading" } }); i.setState({ loginRes: { state: "loading" } });
@ -43,39 +61,23 @@ async function handleLoginSubmit(i: Login, event: any) {
const loginRes = await HttpService.client.login({ const loginRes = await HttpService.client.login({
username_or_email, username_or_email,
password, password,
totp_2fa_token,
}); });
switch (loginRes.state) { switch (loginRes.state) {
case "failed": { case "failed": {
if (loginRes.msg === "missing_totp_token") { if (loginRes.msg === "missing_totp_token") {
i.setState({ showTotp: true }); const Modal = (await import("bootstrap/js/dist/modal")).default;
toast(I18NextService.i18n.t("enter_two_factor_code"), "info"); const modal = new Modal(document.getElementById("totpModal")!);
modal.show();
} else { } else {
toast(I18NextService.i18n.t(loginRes.msg), "danger"); toast(I18NextService.i18n.t(loginRes.msg), "danger");
} }
i.setState({ loginRes: { state: "failed", msg: loginRes.msg } }); i.setState({ loginRes });
break; break;
} }
case "success": { case "success": {
UserService.Instance.login({ handleLoginSuccess(i, loginRes.data);
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("/");
break; 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) { function handleLoginPasswordChange(i: Login, event: any) {
i.setState(prevState => (prevState.form.password = event.target.value)); i.setState(prevState => (prevState.form.password = event.target.value));
} }
@ -104,13 +102,17 @@ export class Login extends Component<
state: State = { state: State = {
loginRes: { state: "empty" }, loginRes: { state: "empty" },
form: {}, form: {
showTotp: false, username_or_email: "",
password: "",
},
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.handleSubmitTotp = this.handleSubmitTotp.bind(this);
} }
componentDidMount() { componentDidMount() {
@ -137,6 +139,7 @@ export class Login extends Component<
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
/> />
<TotpModal type="login" onSubmit={this.handleSubmitTotp} />
<div className="row"> <div className="row">
<div className="col-12 col-lg-6 offset-lg-3">{this.loginForm()}</div> <div className="col-12 col-lg-6 offset-lg-3">{this.loginForm()}</div>
</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() { loginForm() {
return ( return (
<div> <div>
@ -178,28 +198,6 @@ export class Login extends Component<
showForgotLink showForgotLink
/> />
</div> </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="mb-3 row">
<div className="col-sm-10"> <div className="col-sm-10">
<button type="submit" className="btn btn-secondary"> <button type="submit" className="btn btn-secondary">