Skip to content

Commit

Permalink
Merge pull request #30 from atlp-rwanda/feat-OTP-2FA-verfication
Browse files Browse the repository at this point in the history
feat-OTP-2FA
  • Loading branch information
faid-terence authored Jun 24, 2024
2 parents e1792f9 + 710793a commit 3a91a4b
Show file tree
Hide file tree
Showing 17 changed files with 632 additions and 22 deletions.
26 changes: 25 additions & 1 deletion src/Routes/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { useSelector } from 'react-redux';
import { RootState } from '../redux/store';
import GoogleLoginSuccess from '../pages/Authentication/GoogleLoginSuccess';
import { useJwt } from 'react-jwt';
import OtpPage from '../pages/Authentication/OtpPage';
import SuspendedAccount from '../components/SuspendedAccount/SuspendedAccount';

const Router = () => {
const { userToken } = useSelector((state: RootState) => state.auth);
Expand Down Expand Up @@ -65,7 +67,7 @@ const Router = () => {
}
/>
<Route
path="/login/success"
path="/login/google-auth"
element={
<>
<PageTitle title="Knights Store | Login" />
Expand All @@ -76,6 +78,28 @@ const Router = () => {
</>
}
/>
<Route
path="/suspended-account"
element={
<>
<PageTitle title="Knights Store | Suspended Account" />
{userToken && <Navigate to="/" />}
<SuspendedAccount />
</>
}
/>
<Route
path="/otp-verficaton"
element={
<>
<PageTitle title="Knights Store | Verify OTP" />
<OtpPage />
{userToken && isAdmin && <Navigate to="/admin/dashboard" />}
{userToken && isVendor && <Navigate to="/vendor/dashboard" />}
{userToken && isBuyer && <Navigate to="/" />}
</>
}
/>
</Routes>
);
};
Expand Down
3 changes: 3 additions & 0 deletions src/assets/iconoir_xmark-circle (2).svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 6 additions & 1 deletion src/components/Menu/DesktopMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ import React from 'react';
import { AppDispatch } from '../../redux/store';
import { clearCredentials } from '../../redux/reducers/authReducer';
import { useDispatch } from 'react-redux';
import { clearUser } from '../../redux/reducers/userReducer';
import { useNavigate } from 'react-router-dom';

function DesktopMenu() {
const navigate = useNavigate();
const dispatch = useDispatch<AppDispatch>();

const logoutHandler = () => {
dispatch(clearCredentials(''));
dispatch(clearCredentials());
dispatch(clearUser());
navigate('/');
};

return (
Expand Down
20 changes: 20 additions & 0 deletions src/components/SuspendedAccount/SuspendedAccount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';
import iconXMark from '../../assets/iconoir_xmark-circle (2).svg';

function SuspendedAccount() {
return (
<div className="flex flex-col justify-center items-center min-h-[60vh] bg-background3 text-baseBlack">
<img src={iconXMark} alt="" className="w-24 pb-7" />
<p className="text-center text-xl sm:text-2xl font-semibold">Your account has been suspended!</p>
<p className=" text-base sm:text-lg text-center ">
Please contact our support team at{' '}
<a href="mailto:knights@andela.com" className="italic underline text-orange">
knights@andela.com
</a>{' '}
for more information.
</p>
</div>
);
}

export default SuspendedAccount;
27 changes: 22 additions & 5 deletions src/pages/Authentication/GoogleLoginSuccess.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,44 @@ import { useDispatch } from 'react-redux';
import { AppDispatch } from '../../redux/store';
import PropagateLoader from 'react-spinners/PropagateLoader';
import { clearCredentials, setCredentials } from '../../redux/reducers/authReducer';
import { useNavigate } from 'react-router-dom';
import toast from 'react-hot-toast';
import { setUser } from '../../redux/reducers/userReducer';

const GoogleLoginSuccess = () => {
const dispatch = useDispatch<AppDispatch>();
const navigate = useNavigate();

useEffect(() => {
axios
.get(`${import.meta.env.VITE_APP_API_URL}/user/login/success`, { withCredentials: true })
.then((response) => {
dispatch(setCredentials(response.data.data.token));
if (response.data?.data.message === 'Please provide the OTP sent to your email or phone') {
dispatch(setUser(response.data.data!.email));
navigate('/otp-verficaton');
} else {
dispatch(setCredentials(response.data.data.token));
}
})
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.catch((error) => {
dispatch(clearCredentials(''));
if (error.response?.data?.message === 'Your account has been suspended') {
navigate('/suspended-account');
} else {
toast.error(error.response?.data?.message || 'Unexpected error', {
duration: 4000
});
navigate('/');
}
dispatch(clearCredentials());
});
}, [dispatch]);
}, [dispatch, navigate]);

return (
<div className="flex flex-col justify-center items-center min-h-[60vh] bg-background3 text-baseBlack">
<div className="bg-black mb-10">
<PropagateLoader color="#070F2B" />
</div>
<span className="text-lg pl-4">Signing in with Google </span>
<span className="text-lg pl-4">Signing in with Google</span>
<span className="text-neutrals700 italic text-sm pl-4">Please wait a moment.</span>
</div>
);
Expand Down
29 changes: 23 additions & 6 deletions src/pages/Authentication/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import { RootState, AppDispatch } from '../../redux/store';
import toast from 'react-hot-toast';
import { loginUser } from '../../redux/actions/loginAction';
import google from '../../images/google.png';
import { setCredentials } from '../../redux/reducers/authReducer';
import { Link } from 'react-router-dom';
import { clearCredentials, setCredentials } from '../../redux/reducers/authReducer';
import { Link, useNavigate } from 'react-router-dom';
import { resetState } from '../../redux/reducers/loginReducer';
import { setUser } from '../../redux/reducers/userReducer';

export interface DecodedToken {
id: string;
Expand All @@ -29,6 +30,7 @@ function Login() {
reset
} = useForm<LoginData>();

const navigate = useNavigate();
const dispatch = useDispatch<AppDispatch>();
const { login: loginResponse, loading, error } = useSelector((state: RootState) => state.login);

Expand All @@ -38,16 +40,31 @@ function Login() {

useEffect(() => {
if (error) {
toast.error(error);
toast.error(error, {
duration: 4000
});
if (error === 'Your account has been suspended') {
navigate('/suspended-account');
} else {
navigate('/');
}
dispatch(clearCredentials());
dispatch(resetState());
}
if (loginResponse) {
toast.success(loginResponse.data!.message);
toast.success(loginResponse.data!.message, {
duration: 4000
});
if (loginResponse.data!.message === 'Please provide the OTP sent to your email or phone') {
dispatch(setUser(loginResponse.data!.email));
navigate('/otp-verficaton');
} else {
dispatch(setCredentials(loginResponse.data?.token));
}
reset();
dispatch(setCredentials(loginResponse.data?.token));
dispatch(resetState());
}
}, [error, loginResponse, dispatch, reset]);
}, [error, navigate, loginResponse, dispatch, reset]);

const googleAuth = () => {
window.open(`${import.meta.env.VITE_APP_API_URL}/user/google-auth`, '_self');
Expand Down
131 changes: 131 additions & 0 deletions src/pages/Authentication/OtpPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import React from 'react';
import { useState, ChangeEvent, FormEvent } from 'react';
import { toast } from 'react-hot-toast';
import axios from 'axios';
import { useDispatch, useSelector } from 'react-redux';
import { AppDispatch, RootState } from '../../redux/store';
import { setCredentials } from '../../redux/reducers/authReducer';

const OtpPage = () => {
interface OtpForm {
first: string;
second: string;
third: string;
fourth: string;
fifth: string;
sixth: string;
}

const initialForm = {
first: '',
second: '',
third: '',
fourth: '',
fifth: '',
sixth: ''
};

const [otpForm, setOtpForm] = useState<OtpForm>(initialForm);
const [loading, setLoading] = useState(false);
const { email } = useSelector((state: RootState) => state.currentUser);

const dispatch = useDispatch<AppDispatch>();
const fields = ['first', 'second', 'third', 'fourth', 'fifth', 'sixth'] as const;

const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
if (/^\d?$/.test(value)) {
setOtpForm((prevFormData) => ({ ...prevFormData, [name]: value }));
}
};

const handleError = (error: any) => {
axios.isAxiosError(error)
? toast.error(error.response?.data?.message || 'Server Down. Try again later!')
: toast.error((error as Error).message);
};

const handleSubmit = async (event: FormEvent) => {
event.preventDefault();
try {
const allFieldsFilled = fields.every((field) => otpForm[field]);
if (!allFieldsFilled) {
toast.error('Fill all the OTP fields.');
return;
}

const otpCode = Object.values(otpForm).join('');
if (!email) throw Error('Something went wrong, Login again');
const reqData = { email, otp: otpCode };
setLoading(true);
const response = await axios.post(`${import.meta.env.VITE_APP_API_URL}/user/verify-otp`, reqData);

if (response.status === 200) {
dispatch(setCredentials(response.data.data.token));
}
} catch (error) {
handleError(error);
} finally {
setOtpForm(initialForm);
setLoading(false);
}
};

const resendOTP = async () => {
try {
if (!email) throw Error('Something went wrong, Login again');
const reqData = { email };
setLoading(true);
const response = await axios.post(`${import.meta.env.VITE_APP_API_URL}/user/resend-otp`, reqData);

if (response.data.data.message === 'OTP sent successfully') toast.success('OTP resent successfully');
} catch (error) {
handleError(error);
} finally {
setLoading(false);
}
};

return (
<>
<div className="flex bg-transparent1 items-center justify-center h-[530px]">
<div className="bg-transparent1 sm:bg-white p-5 sm:p-10 text-black flex flex-col gap-6 items-center">
<h2 className="font-medium text-[20px]">Verify Your Identity</h2>
<p className="font-light text-xs sm:text-[12.3px] max-w-[281px] sm:max-w-[412px] text-center">
Protecting your account is our priority. Please confirm your identity by providing the code sent to your
email/phone.
</p>

<form className="flex flex-col gap-6 items-center" onSubmit={handleSubmit}>
<div className="flex gap-[17px] text-black">
{fields.map((field) => (
<input
key={field}
className="w-8 h-8 sm:w-[50px] sm:h-[50px] bg-white border border-grey3 border-solid text-center"
maxLength={1}
onChange={handleChange}
name={field}
value={otpForm[field]}
/>
))}
</div>
<button
type={loading ? 'button' : 'submit'}
className={`bg-primary text-white text-[16px] py-2 px-2 rounded-sm ${loading ? 'cursor-not-allowed' : 'cursor-pointer'} w-60`}
>
{loading ? 'Loading...' : 'Verify'}
</button>
</form>
<p className="font-light text-xs sm:text-[12.3px] max-w-[296px] sm:max-w-[329px] text-center">
It may take a minute to receive verification message. Haven’t received it yet?
<span onClick={resendOTP} className="text-orange cursor-pointer">
Resend
</span>
</p>
</div>
</div>
</>
);
};

export default OtpPage;
3 changes: 1 addition & 2 deletions src/redux/reducers/authReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ const authSlice = createSlice({
localStorage.removeItem('userToken');
}
},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
clearCredentials: (state, action) => {
clearCredentials: (state) => {
state.userToken = null;
localStorage.removeItem('userToken');
}
Expand Down
2 changes: 2 additions & 0 deletions src/redux/reducers/rootReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { combineReducers } from '@reduxjs/toolkit';
import productReducer from './productReducer';
import registerReducer from './registerReducer';
import verifyEmailReducer from './verifyEmailReducer';
import userReducer from './userReducer';
import loginReducer from './loginReducer';
import authReducer from './authReducer';

const rootReducer = combineReducers({
auth: authReducer,
currentUser: userReducer,
register: registerReducer,
login: loginReducer,
verifyEmail: verifyEmailReducer,
Expand Down
21 changes: 21 additions & 0 deletions src/redux/reducers/userReducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { createSlice } from '@reduxjs/toolkit';

const initialState = {
email: ''
};

const currentUserSlice = createSlice({
name: 'currentUser',
initialState: initialState,
reducers: {
setUser: (state, action) => {
state.email = action.payload;
},
clearUser: (state) => {
state.email = '';
}
}
});

export const { setUser, clearUser } = currentUserSlice.actions;
export default currentUserSlice.reducer;
Loading

0 comments on commit 3a91a4b

Please sign in to comment.