diff --git a/src/entryPoints/AuthModule/PreLoginModule/DecisionScreen.res b/src/entryPoints/AuthModule/PreLoginModule/DecisionScreen.res index d2c264667..64d6657c1 100644 --- a/src/entryPoints/AuthModule/PreLoginModule/DecisionScreen.res +++ b/src/entryPoints/AuthModule/PreLoginModule/DecisionScreen.res @@ -18,7 +18,7 @@ let make = () => { | MERCHANT_SELECT | ACCEPT_INVITE => - | TOTP => + | TOTP => | FORCE_SET_PASSWORD | RESET_PASSWORD => diff --git a/src/entryPoints/AuthModule/TwoFaAuth/TotpSetup.res b/src/entryPoints/AuthModule/TwoFaAuth/TotpSetup.res index daf68f163..a07064f4f 100644 --- a/src/entryPoints/AuthModule/TwoFaAuth/TotpSetup.res +++ b/src/entryPoints/AuthModule/TwoFaAuth/TotpSetup.res @@ -4,16 +4,15 @@ let p3Regular = HSwitchUtils.getTextClass((P3, Regular)) module EnterAccessCode = { @react.component - let make = (~setTwoFaPageState, ~onClickVerifyAccessCode) => { + let make = (~setTwoFaPageState, ~onClickVerifyAccessCode, ~errorHandling) => { let showToast = ToastState.useShowToast() let verifyRecoveryCodeLogic = TotpHooks.useVerifyRecoveryCode() let (recoveryCode, setRecoveryCode) = React.useState(_ => "") let (buttonState, setButtonState) = React.useState(_ => Button.Normal) - let verifyAccessCode = async () => { + let verifyAccessCode = async _ => { + open LogicUtils try { - open LogicUtils - setButtonState(_ => Button.Loading) if recoveryCode->String.length > 0 { @@ -25,7 +24,12 @@ module EnterAccessCode = { } setButtonState(_ => Button.Normal) } catch { - | _ => { + | Exn.Error(e) => { + let err = Exn.message(e)->Option.getOr("Something went wrong") + let errorCode = err->safeParse->getDictFromJsonObject->getString("code", "") + if errorCode == "UR_49" { + errorHandling(errorCode) + } setRecoveryCode(_ => "") setButtonState(_ => Button.Normal) } @@ -105,6 +109,7 @@ module ConfigureTotpScreen = { ~twoFaStatus, ~setTwoFaPageState, ~terminateTwoFactorAuth, + ~errorHandling, ) => { open TwoFaTypes @@ -115,9 +120,8 @@ module ConfigureTotpScreen = { let (buttonState, setButtonState) = React.useState(_ => Button.Normal) let verifyTOTP = async () => { + open LogicUtils try { - open LogicUtils - setButtonState(_ => Button.Loading) if otp->String.length > 0 { @@ -135,7 +139,12 @@ module ConfigureTotpScreen = { } setButtonState(_ => Button.Normal) } catch { - | _ => { + | Exn.Error(e) => { + let err = Exn.message(e)->Option.getOr("Something went wrong") + let errorCode = err->safeParse->getDictFromJsonObject->getString("code", "") + if errorCode == "UR_48" { + errorHandling(errorCode) + } setOtp(_ => "") setButtonState(_ => Button.Normal) } @@ -225,7 +234,7 @@ module ConfigureTotpScreen = { } @react.component -let make = () => { +let make = (~setTwoFaPageState, ~twoFaPageState, ~errorHandling) => { open HSwitchUtils open TwoFaTypes @@ -238,7 +247,6 @@ let make = () => { let (isQrVisible, setIsQrVisible) = React.useState(_ => false) let (totpUrl, setTotpUrl) = React.useState(_ => "") let (twoFaStatus, setTwoFaStatus) = React.useState(_ => TWO_FA_NOT_SET) - let (twoFaPageState, setTwoFaPageState) = React.useState(_ => TOTP_SHOW_QR) let (showNewQR, setShowNewQR) = React.useState(_ => false) let delayTimer = () => { @@ -325,14 +333,16 @@ let make = () => { {switch twoFaPageState { | TOTP_SHOW_QR => | TOTP_SHOW_RC => | TOTP_INPUT_RECOVERY_CODE => - + }}
{"Log in with a different account?"->React.string} diff --git a/src/entryPoints/AuthModule/TwoFaAuth/TwoFaLanding.res b/src/entryPoints/AuthModule/TwoFaAuth/TwoFaLanding.res new file mode 100644 index 000000000..0c384863e --- /dev/null +++ b/src/entryPoints/AuthModule/TwoFaAuth/TwoFaLanding.res @@ -0,0 +1,126 @@ +let p2Regular = HSwitchUtils.getTextClass((P2, Regular)) + +module AttemptsExpiredComponent = { + @react.component + let make = (~expiredType, ~setTwoFaPageState, ~setTwoFaStatus) => { + open TwoFaTypes + open HSwitchUtils + let {setAuthStatus} = React.useContext(AuthInfoProvider.authStatusContext) + + let belowComponent = switch expiredType { + | TOTP_ATTEMPTS_EXPIRED => +

+ {"or "->React.string} + { + setTwoFaStatus(_ => TwoFaNotExpired) + setTwoFaPageState(_ => TOTP_INPUT_RECOVERY_CODE) + }}> + {"Use recovery-code"->React.string} + +

+ | RC_ATTEMPTS_EXPIRED => +

+ {"or "->React.string} + { + setTwoFaStatus(_ => TwoFaNotExpired) + setTwoFaPageState(_ => TOTP_SHOW_QR) + }}> + {"Use totp"->React.string} + +

+ | TWO_FA_EXPIRED => React.null + } + + +
+
+

+ {"There have been multiple unsuccessful sign-in attempts for this account. Please wait a moment before trying again."->React.string} +

+ {belowComponent} +
+
+ {"Log in with a different account?"->React.string} +

setAuthStatus(LoggedOut)}> + {"Click here to log out."->React.string} +

+
+
+
+ } +} + +@react.component +let make = () => { + open TwoFaTypes + let getURL = APIUtils.useGetURL() + let fetchDetails = APIUtils.useGetMethod() + let {setAuthStatus} = React.useContext(AuthInfoProvider.authStatusContext) + let (twoFaStatus, setTwoFaStatus) = React.useState(_ => TwoFaNotExpired) + let (twoFaPageState, setTwoFaPageState) = React.useState(_ => TOTP_SHOW_QR) + let (screenState, setScreenState) = React.useState(_ => PageLoaderWrapper.Loading) + + let handlePageBasedOnAttempts = responseDict => { + switch responseDict { + | Some(value) => + if value.totp.attemptsRemaining > 0 && value.recoveryCode.attemptsRemaining > 0 { + setTwoFaStatus(_ => TwoFaNotExpired) + setTwoFaPageState(_ => TOTP_SHOW_QR) + } else if value.totp.attemptsRemaining == 0 && value.recoveryCode.attemptsRemaining == 0 { + setTwoFaStatus(_ => TwoFaExpired(TWO_FA_EXPIRED)) + } else if value.totp.attemptsRemaining == 0 { + setTwoFaStatus(_ => TwoFaExpired(TOTP_ATTEMPTS_EXPIRED)) + } else if value.recoveryCode.attemptsRemaining == 0 { + setTwoFaStatus(_ => TwoFaExpired(RC_ATTEMPTS_EXPIRED)) + } + | None => setTwoFaStatus(_ => TwoFaNotExpired) + } + } + + let checkTwofaStatus = async () => { + try { + let url = getURL( + ~entityName=USERS, + ~userType=#CHECK_TWO_FACTOR_AUTH_STATUS_V2, + ~methodType=Get, + ) + let response = await fetchDetails(url) + let responseDict = response->TwoFaUtils.jsonTocheckTwofaResponseType + handlePageBasedOnAttempts(responseDict.status) + setScreenState(_ => PageLoaderWrapper.Success) + } catch { + | _ => { + setScreenState(_ => PageLoaderWrapper.Error("Failed to fetch!")) + setAuthStatus(LoggedOut) + } + } + } + + let errorHandling = errorCode => { + if errorCode == "UR_48" { + setTwoFaStatus(_ => TwoFaExpired(TOTP_ATTEMPTS_EXPIRED)) + } else if errorCode == "UR_49" { + setTwoFaStatus(_ => TwoFaExpired(RC_ATTEMPTS_EXPIRED)) + } + } + + React.useEffect(() => { + checkTwofaStatus()->ignore + None + }, []) + + + {switch twoFaStatus { + | TwoFaExpired(expiredType) => + + | TwoFaNotExpired => + }} + +} diff --git a/src/entryPoints/AuthModule/TwoFaAuth/TwoFaTypes.res b/src/entryPoints/AuthModule/TwoFaAuth/TwoFaTypes.res index 289485444..fad0c7f59 100644 --- a/src/entryPoints/AuthModule/TwoFaAuth/TwoFaTypes.res +++ b/src/entryPoints/AuthModule/TwoFaAuth/TwoFaTypes.res @@ -1,3 +1,25 @@ -type twoFaPageState = TOTP_SHOW_QR | TOTP_SHOW_RC | TOTP_INPUT_RECOVERY_CODE +type twoFaPageState = + | TOTP_SHOW_QR + | TOTP_SHOW_RC + | TOTP_INPUT_RECOVERY_CODE type twoFaStatus = TWO_FA_NOT_SET | TWO_FA_SET + +type twoFaValueType = { + isCompleted: bool, + attemptsRemaining: int, +} + +type twoFatype = { + totp: twoFaValueType, + recoveryCode: twoFaValueType, +} + +type checkTwofaResponseType = {status: option} + +type expiredTypes = + | TOTP_ATTEMPTS_EXPIRED + | RC_ATTEMPTS_EXPIRED + | TWO_FA_EXPIRED + +type twoFaStatusType = TwoFaExpired(expiredTypes) | TwoFaNotExpired diff --git a/src/entryPoints/AuthModule/TwoFaAuth/TwoFaUtils.res b/src/entryPoints/AuthModule/TwoFaAuth/TwoFaUtils.res index 3832ceac7..8c5e06e8b 100644 --- a/src/entryPoints/AuthModule/TwoFaAuth/TwoFaUtils.res +++ b/src/entryPoints/AuthModule/TwoFaAuth/TwoFaUtils.res @@ -63,3 +63,33 @@ let downloadRecoveryCodes = (~recoveryCodes) => { ~content=JSON.stringifyWithIndent(recoveryCodes->getJsonFromArrayOfString, 3), ) } + +let jsonToTwoFaValueType: Dict.t<'a> => TwoFaTypes.twoFaValueType = dict => { + open LogicUtils + + { + isCompleted: dict->getBool("is_completed", false), + attemptsRemaining: dict->getInt("remaining_attempts", 4), + } +} + +let jsonTocheckTwofaResponseType: JSON.t => TwoFaTypes.checkTwofaResponseType = json => { + open LogicUtils + let jsonToDict = json->getDictFromJsonObject->Dict.get("status") + + let statusValue = switch jsonToDict { + | Some(json) => { + let dict = json->getDictFromJsonObject + let twoFaValue: TwoFaTypes.twoFatype = { + totp: dict->getDictfromDict("totp")->jsonToTwoFaValueType, + recoveryCode: dict->getDictfromDict("recovery_code")->jsonToTwoFaValueType, + } + Some(twoFaValue) + } + | None => None + } + + { + status: statusValue, + } +} diff --git a/src/screens/APIUtils/APIUtils.res b/src/screens/APIUtils/APIUtils.res index 6888eb4de..12f79baeb 100644 --- a/src/screens/APIUtils/APIUtils.res +++ b/src/screens/APIUtils/APIUtils.res @@ -661,6 +661,7 @@ let useGetURL = () => { // SPT FLOWS (Totp) | #BEGIN_TOTP => `${userUrl}/2fa/totp/begin` + | #CHECK_TWO_FACTOR_AUTH_STATUS_V2 => `${userUrl}/2fa/v2` | #VERIFY_TOTP => `${userUrl}/2fa/totp/verify` | #VERIFY_RECOVERY_CODE => `${userUrl}/2fa/recovery_code/verify` | #GENERATE_RECOVERY_CODES => `${userUrl}/2fa/recovery_code/generate` diff --git a/src/screens/APIUtils/APIUtilsTypes.res b/src/screens/APIUtils/APIUtilsTypes.res index aaa908515..82a6d2ca4 100644 --- a/src/screens/APIUtils/APIUtilsTypes.res +++ b/src/screens/APIUtils/APIUtilsTypes.res @@ -114,6 +114,7 @@ type userType = [ | #USER_DETAILS | #LIST_ROLES_FOR_ROLE_UPDATE | #ACCEPT_INVITATION_HOME + | #CHECK_TWO_FACTOR_AUTH_STATUS_V2 | #NONE ]