diff --git a/config.template.js b/config.template.js index 73967662..a2f8b829 100644 --- a/config.template.js +++ b/config.template.js @@ -7,5 +7,6 @@ window.config = { EXTENSION_PLUGIN_USERS_LIST: '', BUILD_TIME: '', ENABLE_SSO: '', - ENV: '' + ENV: '', + GOOGLE_CAPTCHA_SITE_KEY: '' }; diff --git a/configuration.js b/configuration.js index 6a455ba7..713a6bf5 100644 --- a/configuration.js +++ b/configuration.js @@ -10,5 +10,6 @@ window.config = { // https://apm.np.navi-tech.in, BUILD_TIME: 0, ENABLE_SSO: "true", - ENV: "dev" + ENV: "dev", + GOOGLE_CAPTCHA_SITE_KEY: "6LekZqYlAAAAAAyZxFIsHGwCkQu-YfQ7mv7b2d4Z" }; diff --git a/entrypoint.sh b/entrypoint.sh index 8de250ac..d546501e 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -8,6 +8,7 @@ sed -i "s~~${AUTH_BASE_URL}~g" /usr/share/nginx/html/configuratio sed -i "s~~${AUTH_CLIENT_ID}~g" /usr/share/nginx/html/configuration.js sed -i "s~~${SENTRY_DSN}~g" /usr/share/nginx/html/configuration.js sed -i "s~~${ENABLE_SSO}~g" /usr/share/nginx/html/configuration.js +sed -i "s~~${GOOGLE_CAPTCHA_SITE_KEY}~g" /usr/share/nginx/html/configuration.js sed -i 's~~/configuration.js~g' /usr/share/nginx/html/index.html exec "$@" diff --git a/package.json b/package.json index 9fb0fa82..17ea9ae6 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "crypto-js": "^4.1.1", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-google-recaptcha": "^2.1.0", "react-hook-form": "7.38.0", "react-redux": "^7.2.6", "react-router-dom": "^6.3.0", @@ -66,6 +67,7 @@ "@types/jest": "^27.4.0", "@types/react": "18.0.17", "@types/react-dom": "18.0.6", + "@types/react-google-recaptcha": "^2.1.5", "@typescript-eslint/eslint-plugin": "^5.11.0", "@typescript-eslint/parser": "^5.11.0", "@vitejs/plugin-react": "^1.0.7", diff --git a/src/pages/auth/AuthActions.ts b/src/pages/auth/AuthActions.ts index 47b9b075..5b96091e 100644 --- a/src/pages/auth/AuthActions.ts +++ b/src/pages/auth/AuthActions.ts @@ -1,5 +1,5 @@ import { Dispatch } from '@reduxjs/toolkit'; -import { setAuthData } from '../../reducers/commonSlice'; +import { setAuthData, setCaptchaVisibility } from '../../reducers/commonSlice'; import { setLeaderboard, setPerformance } from '../../reducers/leaderboardSlice'; import axiosInstance, { ApiKeys, getApiUrl, getErrorMessage } from '../../utils/ApiHelper'; import { @@ -199,6 +199,7 @@ export const verifyOTP = ( localStorage.setItem('LONGHORN_SESSION_TOKEN', response.data.accessToken); localStorage.setItem('pno', btoa(phoneNumber || '')); dispatch(setAuthData({ token: response.data.accessToken, isLoggedIn: true })); + dispatch(setCaptchaVisibility(response?.data?.data?.showCaptcha)); navigateToCases(); const authParams = { sessionId: response.data.accessToken, @@ -208,6 +209,7 @@ export const verifyOTP = ( } }) .catch(error => { + dispatch(setCaptchaVisibility(error?.response?.data?.data?.showCaptcha)); const message = error?.response?.data?.message; dispatch(setGlobalError({ message })); }) @@ -265,3 +267,17 @@ export const getPerformance = (agentReferenceId: string) => { }); }; }; + +export const verifyCaptcha = (response: string, phoneNumber: any) => { + const url = + getApiUrl(ApiKeys.VERIFY_CAPTCHA) + `?response=${response}` + `&phoneNumber=${phoneNumber}`; + return axiosInstance + .post(url) + .then(response => { + const data = response?.data; + return data?.success; + }) + .catch(() => { + clearToken(); + }); +}; diff --git a/src/pages/auth/components/Login.tsx b/src/pages/auth/components/Login.tsx index 13ecfb2b..3ff305b0 100644 --- a/src/pages/auth/components/Login.tsx +++ b/src/pages/auth/components/Login.tsx @@ -13,6 +13,7 @@ import MobileNumberInputScreen from './partials/MobileNumberInputScreen'; import OTPInputScreen from './partials/OTPInputScreen'; import { readQueryParams } from '../../../utils/QueryParamsHelper'; import { titleString } from '../../../constants/Global'; + const Login = () => { const showOTPScreen = useSelector((state: RootState) => state.auth.showOTPScreen); const mobileNumber = useSelector((state: RootState) => state?.auth?.mobileNumber); diff --git a/src/pages/auth/components/partials/MobileNumberInputScreen.tsx b/src/pages/auth/components/partials/MobileNumberInputScreen.tsx index 3995a7f3..aa57a9a3 100644 --- a/src/pages/auth/components/partials/MobileNumberInputScreen.tsx +++ b/src/pages/auth/components/partials/MobileNumberInputScreen.tsx @@ -13,6 +13,8 @@ import APP_ROUTES from 'src/layout/Routes'; import { LoginErrorMessage } from './ErrorMessage'; import { resetGlobalError, setGlobalError } from '../../reducers/authSlice'; import GoogleIcon from '../../../../assets/icons/GoogleIcon'; +import Recaptcha from './Recaptcha'; +import { setCaptchaVisibility } from 'src/reducers/commonSlice'; const enableSSOFlag = window.config.ENABLE_SSO; const MobileNumberInputScreen = () => { @@ -33,6 +35,7 @@ const MobileNumberInputScreen = () => { useEffect(() => { dispatch(resetGlobalError()); + dispatch(setCaptchaVisibility(false)); if (/\D/g.test(phoneNumber)) { const result = phoneNumber.replace(/\D/g, ''); setPhoneNumber(result); diff --git a/src/pages/auth/components/partials/OTPInputScreen.tsx b/src/pages/auth/components/partials/OTPInputScreen.tsx index fc77a2f4..95adb349 100644 --- a/src/pages/auth/components/partials/OTPInputScreen.tsx +++ b/src/pages/auth/components/partials/OTPInputScreen.tsx @@ -18,6 +18,7 @@ import { import styles from '../Login.module.scss'; import { LoginErrorMessage } from './ErrorMessage'; import { getDeviceId } from 'src/utils/FingerPrintJS'; +import Recaptcha from './Recaptcha'; interface OTPInputProps { navigateToCases: () => void; @@ -26,12 +27,15 @@ interface OTPInputProps { const OTPInputScreen = ({ navigateToCases }: OTPInputProps) => { const phoneNumber = useSelector((state: RootState) => state?.auth?.mobileNumber); const otpToken = useSelector((state: RootState) => state?.auth?.otpToken); + const showCaptcha = + useSelector((state: RootState) => state?.common?.userData?.showCaptcha) || false; const verifyOTPError = useSelector((state: RootState) => state?.auth?.verifyOTPError); const isLoading = useSelector((state: RootState) => state?.auth?.isLoading); const dispatch = useDispatch(); const [otpValue, setOTPValue] = React.useState(''); + const [captchaVerified, setCaptchaVerified] = React.useState(false); const [isPristine, setIsPristine] = React.useState(true); const [countDownComplete, setCountDownComplete] = React.useState(false); const [otpDescription, setOtpDescription] = React.useState( @@ -50,7 +54,9 @@ const OTPInputScreen = ({ navigateToCases }: OTPInputProps) => { dispatch(resetGlobalError()); }; - const getButtonDisabledState = (): boolean => !(otpValue?.length === 4) || isLoading; + const getButtonDisabledState = (): boolean => { + return !(otpValue?.length === 4) || isLoading || (showCaptcha && captchaVerified); + }; const handleVerifyOTP = (event: React.SyntheticEvent): void => { event.preventDefault(); @@ -69,6 +75,11 @@ const OTPInputScreen = ({ navigateToCases }: OTPInputProps) => { sendOTP({ phoneNumber: phoneNumber || '' }); }; + const handleVerify = (success: boolean) => { + setCaptchaVerified(success); + dispatch(setShowOTPScreen({ status: false })); + }; + React.useEffect(() => { if (verifyOTPError) { setOtpDescription(OTPInputScreenConstants.InvalidOTP); @@ -167,6 +178,9 @@ const OTPInputScreen = ({ navigateToCases }: OTPInputProps) => { + + {showCaptcha && } + diff --git a/src/pages/auth/components/partials/Recaptcha.tsx b/src/pages/auth/components/partials/Recaptcha.tsx new file mode 100644 index 00000000..8277448a --- /dev/null +++ b/src/pages/auth/components/partials/Recaptcha.tsx @@ -0,0 +1,27 @@ +import ReCAPTCHA from 'react-google-recaptcha'; +import { useSelector } from 'react-redux'; +import { RootState } from 'src/store'; +import { verifyCaptcha } from '../../AuthActions'; + +interface IRecaptcha { + onVerify: (isVerified: boolean) => void; +} + +const Recaptcha = ({ onVerify }: IRecaptcha) => { + const phoneNumber = useSelector((state: RootState) => state?.auth?.mobileNumber); + const siteKey = window.config.GOOGLE_CAPTCHA_SITE_KEY; + function onChange(value: any) { + verifyCaptcha(value, phoneNumber).then(success => { + if (typeof onVerify === 'function') { + onVerify(success); + } + }); + } + return ( +
+ +
+ ); +}; + +export default Recaptcha; diff --git a/src/reducers/commonSlice.ts b/src/reducers/commonSlice.ts index 88f7c854..51f4cb9a 100644 --- a/src/reducers/commonSlice.ts +++ b/src/reducers/commonSlice.ts @@ -32,6 +32,7 @@ export interface User { slashTabOpened?: boolean; naviUser?: boolean; userImpersonated?: boolean; + showCaptcha?: boolean; } export interface CommonState { @@ -57,6 +58,9 @@ export const commonSlice = createSlice({ name: 'common', initialState, reducers: { + setCaptchaVisibility: (state, action) => { + state.userData.showCaptcha = action.payload; + }, setAuthData: (state, action) => { if (action.payload) { setGlobalUserData({ token: action.payload.token, userAgent: action.payload.referenceId }); @@ -87,7 +91,13 @@ export const commonSlice = createSlice({ // } }); -export const { setAuthData, setToast, navbarStatus, setShowStickyNote, setDeviceIdInStore } = - commonSlice.actions; +export const { + setAuthData, + setToast, + navbarStatus, + setShowStickyNote, + setDeviceIdInStore, + setCaptchaVisibility +} = commonSlice.actions; export default commonSlice.reducer; diff --git a/src/types/AppConfig.ts b/src/types/AppConfig.ts index e00aac04..de93a5a5 100644 --- a/src/types/AppConfig.ts +++ b/src/types/AppConfig.ts @@ -10,6 +10,7 @@ export interface AppConfig { SENTRY_DSN: string; BUILD_TIME: number; ENABLE_SSO: string; + GOOGLE_CAPTCHA_SITE_KEY: string; } export enum XREDIRECTTO { diff --git a/src/utils/ApiHelper.ts b/src/utils/ApiHelper.ts index f6a404aa..f35707de 100644 --- a/src/utils/ApiHelper.ts +++ b/src/utils/ApiHelper.ts @@ -73,7 +73,8 @@ export enum ApiKeys { FETCH_CUSTOMER_INFO, FETCH_REPAYMENT_HISTORY, DOWNLOAD_DUNNING_LETTER, - GET_PERFORMANCE + GET_PERFORMANCE, + VERIFY_CAPTCHA } // TODO: try to get rid of `as` @@ -132,6 +133,7 @@ API_URLS[ApiKeys.GET_PERFORMANCE] = 'levels/agents/{agentReferenceId}/chart/LAST API_URLS[ApiKeys.OAUTH_SIGNIN] = '/google/sign-in/url'; API_URLS[ApiKeys.OAUTH_EXCHANGE_SESSION] = '/session/exchange'; +API_URLS[ApiKeys.VERIFY_CAPTCHA] = '/recaptcha/verify'; // TODO: try to get rid of `as` const MOCK_API_URLS: Record = {} as Record;