mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-14 11:05:41 +00:00
feat(frontend): make mfa login faster (#9595)
* add hidden field and logic to reduce clicks for mfa logins * refactor to seperate function to reduce complexity * fix missing imports * fix style --------- Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
This commit is contained in:
@ -1,3 +1,4 @@
|
|||||||
|
import { ApiEndpoints, apiUrl } from '@lib/index';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import {
|
import {
|
||||||
@ -8,16 +9,14 @@ import {
|
|||||||
Loader,
|
Loader,
|
||||||
PasswordInput,
|
PasswordInput,
|
||||||
Stack,
|
Stack,
|
||||||
TextInput
|
TextInput,
|
||||||
|
VisuallyHidden
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
|
import { showNotification } from '@mantine/notifications';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
|
||||||
import { apiUrl } from '@lib/functions/Api';
|
|
||||||
import { showNotification } from '@mantine/notifications';
|
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import { api } from '../../App';
|
import { api } from '../../App';
|
||||||
import {
|
import {
|
||||||
@ -33,7 +32,7 @@ import { SsoButton } from '../buttons/SSOButton';
|
|||||||
|
|
||||||
export function AuthenticationForm() {
|
export function AuthenticationForm() {
|
||||||
const classicForm = useForm({
|
const classicForm = useForm({
|
||||||
initialValues: { username: '', password: '' }
|
initialValues: { username: '', password: '', code: '' }
|
||||||
});
|
});
|
||||||
const simpleForm = useForm({ initialValues: { email: '' } });
|
const simpleForm = useForm({ initialValues: { email: '' } });
|
||||||
const [classicLoginMode, setMode] = useDisclosure(true);
|
const [classicLoginMode, setMode] = useDisclosure(true);
|
||||||
@ -58,7 +57,9 @@ export function AuthenticationForm() {
|
|||||||
doBasicLogin(
|
doBasicLogin(
|
||||||
classicForm.values.username,
|
classicForm.values.username,
|
||||||
classicForm.values.password,
|
classicForm.values.password,
|
||||||
navigate
|
|
||||||
|
navigate,
|
||||||
|
classicForm.values.code
|
||||||
)
|
)
|
||||||
.then((success) => {
|
.then((success) => {
|
||||||
setIsLoggingIn(false);
|
setIsLoggingIn(false);
|
||||||
@ -140,6 +141,13 @@ export function AuthenticationForm() {
|
|||||||
placeholder={t`Your password`}
|
placeholder={t`Your password`}
|
||||||
{...classicForm.getInputProps('password')}
|
{...classicForm.getInputProps('password')}
|
||||||
/>
|
/>
|
||||||
|
<VisuallyHidden>
|
||||||
|
<TextInput
|
||||||
|
name='TOTP'
|
||||||
|
{...classicForm.getInputProps('code')}
|
||||||
|
hidden={true}
|
||||||
|
/>
|
||||||
|
</VisuallyHidden>
|
||||||
{password_forgotten_enabled() === true && (
|
{password_forgotten_enabled() === true && (
|
||||||
<Group justify='space-between' mt='0'>
|
<Group justify='space-between' mt='0'>
|
||||||
<Anchor
|
<Anchor
|
||||||
|
@ -62,11 +62,12 @@ function post(path: string, params: any, method = 'post') {
|
|||||||
* If login is successful, an API token will be returned.
|
* If login is successful, an API token will be returned.
|
||||||
* This API token is used for any future API requests.
|
* This API token is used for any future API requests.
|
||||||
*/
|
*/
|
||||||
export const doBasicLogin = async (
|
export async function doBasicLogin(
|
||||||
username: string,
|
username: string,
|
||||||
password: string,
|
password: string,
|
||||||
navigate: NavigateFunction
|
navigate: NavigateFunction,
|
||||||
) => {
|
code?: string
|
||||||
|
) {
|
||||||
const { getHost } = useLocalState.getState();
|
const { getHost } = useLocalState.getState();
|
||||||
const { clearUserState, setAuthenticated, fetchUserState } =
|
const { clearUserState, setAuthenticated, fetchUserState } =
|
||||||
useUserState.getState();
|
useUserState.getState();
|
||||||
@ -104,16 +105,9 @@ export const doBasicLogin = async (
|
|||||||
success = true;
|
success = true;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch(async (err) => {
|
||||||
if (err?.response?.status == 401) {
|
if (err?.response?.status == 401) {
|
||||||
setAuthContext(err.response.data?.data);
|
await handlePossibleMFAError(err);
|
||||||
const mfa_flow = err.response.data.data.flows.find(
|
|
||||||
(flow: any) => flow.id == FlowEnum.MfaAuthenticate
|
|
||||||
);
|
|
||||||
if (mfa_flow && mfa_flow.is_pending == true) {
|
|
||||||
success = true;
|
|
||||||
navigate('/mfa');
|
|
||||||
}
|
|
||||||
} else if (err?.response?.status == 409) {
|
} else if (err?.response?.status == 409) {
|
||||||
notifications.show({
|
notifications.show({
|
||||||
title: t`Already logged in`,
|
title: t`Already logged in`,
|
||||||
@ -133,7 +127,40 @@ export const doBasicLogin = async (
|
|||||||
clearUserState();
|
clearUserState();
|
||||||
}
|
}
|
||||||
return success;
|
return success;
|
||||||
};
|
|
||||||
|
async function handlePossibleMFAError(err: any) {
|
||||||
|
setAuthContext(err.response.data?.data);
|
||||||
|
const mfa_flow = err.response.data.data.flows.find(
|
||||||
|
(flow: any) => flow.id == FlowEnum.MfaAuthenticate
|
||||||
|
);
|
||||||
|
if (mfa_flow?.is_pending) {
|
||||||
|
// MFA is required - we might already have a code
|
||||||
|
if (code && code.length > 0) {
|
||||||
|
const rslt = await handleMfaLogin(
|
||||||
|
navigate,
|
||||||
|
undefined,
|
||||||
|
{ code: code },
|
||||||
|
() => {}
|
||||||
|
);
|
||||||
|
if (rslt) {
|
||||||
|
setAuthenticated(true);
|
||||||
|
loginDone = true;
|
||||||
|
success = true;
|
||||||
|
notifications.show({
|
||||||
|
title: t`MFA Login successful`,
|
||||||
|
message: t`MFA details were automatically provided in the browser`,
|
||||||
|
color: 'green'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No code or success - off to the mfa page
|
||||||
|
if (!loginDone) {
|
||||||
|
success = true;
|
||||||
|
navigate('/mfa');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logout the user from the current session
|
* Logout the user from the current session
|
||||||
@ -259,19 +286,25 @@ export function handleReset(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleMfaLogin(
|
export async function handleMfaLogin(
|
||||||
navigate: NavigateFunction,
|
navigate: NavigateFunction,
|
||||||
location: Location<any>,
|
location: Location<any> | undefined,
|
||||||
values: { code: string; remember?: boolean },
|
values: { code: string; remember?: boolean },
|
||||||
setError: (message: string | undefined) => void
|
setError: (message: string | undefined) => void
|
||||||
) {
|
) {
|
||||||
const { setAuthContext } = useServerApiState.getState();
|
const { setAuthContext } = useServerApiState.getState();
|
||||||
|
|
||||||
authApi(apiUrl(ApiEndpoints.auth_login_2fa), undefined, 'post', {
|
const result = await authApi(
|
||||||
code: values.code
|
apiUrl(ApiEndpoints.auth_login_2fa),
|
||||||
})
|
undefined,
|
||||||
|
'post',
|
||||||
|
{
|
||||||
|
code: values.code
|
||||||
|
}
|
||||||
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
handleSuccessFullAuth(response, navigate, location, setError);
|
handleSuccessFullAuth(response, navigate, location, setError);
|
||||||
|
return true;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
// Already logged in, but with a different session
|
// Already logged in, but with a different session
|
||||||
@ -304,7 +337,9 @@ export function handleMfaLogin(
|
|||||||
}
|
}
|
||||||
setError(msg);
|
setError(msg);
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -380,7 +415,7 @@ function handleSuccessFullAuth(
|
|||||||
observeProfile();
|
observeProfile();
|
||||||
fetchGlobalStates(navigate);
|
fetchGlobalStates(navigate);
|
||||||
|
|
||||||
if (navigate) {
|
if (navigate && location) {
|
||||||
followRedirect(navigate, location?.state);
|
followRedirect(navigate, location?.state);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user