mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-11-28 23:31:14 +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 },
|
{ 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)}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
Loading…
Reference in a new issue