mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +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( | ||||||
|  |     apiUrl(ApiEndpoints.auth_login_2fa), | ||||||
|  |     undefined, | ||||||
|  |     'post', | ||||||
|  |     { | ||||||
|       code: values.code |       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