diff --git a/package.json b/package.json index 3c355d1..8f5776f 100644 --- a/package.json +++ b/package.json @@ -12,19 +12,27 @@ "@emotion/cache": "^11.13.1", "@emotion/react": "^11.13.0", "@emotion/styled": "^11.13.0", + "@hookform/resolvers": "^3.9.0", "@mui/icons-material": "^5.16.6", "@mui/material-nextjs": "^5.16.6", "next": "14.2.5", "react": "^18", "react-dom": "^18", - "sass": "^1.77.8" + "react-hook-form": "^7.52.2", + "sass": "^1.77.8", + "zod": "^3.23.8" }, "devDependencies": { + "@testing-library/jest-dom": "^6.4.8", + "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.5.2", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "axios-mock-adapter": "^2.0.0", "eslint": "^8", "eslint-config-next": "14.2.5", + "jest": "^29.7.0", "typescript": "^5" } } diff --git a/src/app/(auth)/log-in/page.tsx b/src/app/(auth)/log-in/page.tsx index b2a5ffa..d5cc86b 100644 --- a/src/app/(auth)/log-in/page.tsx +++ b/src/app/(auth)/log-in/page.tsx @@ -1,3 +1,4 @@ +"use client" import React from "react"; import styles from "./loginPage.module.scss"; import Box from "@mui/material/Box"; @@ -7,8 +8,42 @@ import { UTILITY_CONSTANT } from "@/utilities/utilityConstant"; import Link from "next/link"; import CustomizedInputsStyled from "@/ui/CustomizedInputsStyled"; import CustomizedButtons from "@/ui/customizedButtons"; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useState } from 'react'; +import { loginApi } from "@/services/api/loginApi"; +import { FormControl } from '@mui/material'; +import CustomTextField from "@/ui/CustomTextField"; + +const loginSchema = z.object({ + username: z.string(), + password: z.string(), +}); + +type LoginFormValues = z.infer; export default function LoginPage() { + + const { register, handleSubmit, formState: { errors }, control } = useForm({ + resolver: zodResolver(loginSchema), + }); + console.log("errors", errors) + const [error, setError] = useState(''); + + const onSubmit = async (data: LoginFormValues) => { + try { + const response = await loginApi(data); + localStorage.setItem('token', response.token); + localStorage.setItem('refreshToken', response.refreshToken); + console.log("🚀 ~ response @@@ @@@ @@@ @@@ @@@@:", response) + + // router.push('/dashboard'); + } catch (err) { + setError('Invalid credentials'); + } + }; + return (
@@ -40,7 +75,7 @@ export default function LoginPage() { paddingBlock={5} > logo - +
Welcome to Convexsol Sign in to your account - - + - + - +
=> { + const response = await axiosInstance.post('/auth/login', data); + return response.data; +}; diff --git a/src/services/axios/axiosInstance.ts b/src/services/axios/axiosInstance.ts new file mode 100644 index 0000000..3fe3d38 --- /dev/null +++ b/src/services/axios/axiosInstance.ts @@ -0,0 +1,88 @@ +// axiosInstance.ts +import axios from 'axios'; + +const axiosInstance = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_URL, + headers: { + 'Content-Type': 'application/json', + }, +}); + +let isRefreshing = false; +let failedQueue: any[] = []; + +const processQueue = (error: any, token: string | null = null) => { + failedQueue.forEach((prom) => { + if (token) { + prom.resolve(token); + } else { + prom.reject(error); + } + }); + + failedQueue = []; +}; + +axiosInstance.interceptors.request.use( + (config) => { + const token = localStorage.getItem('token'); + if (token) { + config.headers['Authorization'] = `Bearer ${token}`; + } + return config; + }, + (error) => Promise.reject(error) +); + +axiosInstance.interceptors.response.use( + (response) => response, + async (error) => { + const originalRequest = error.config; + if (error.response.status === 401 && !originalRequest._retry) { + if (isRefreshing) { + return new Promise(function (resolve, reject) { + failedQueue.push({ resolve, reject }); + }) + .then((token) => { + originalRequest.headers['Authorization'] = 'Bearer ' + token; + return axiosInstance(originalRequest); + }) + .catch((err) => Promise.reject(err)); + } + + originalRequest._retry = true; + isRefreshing = true; + + const refreshToken = localStorage.getItem('refreshToken'); + if (!refreshToken) { + window.location.href = '/log-in'; + return Promise.reject(error); + } + + try { + const { data } = await axios.post( + `${process.env.NEXT_PUBLIC_API_URL}/auth/refresh`, + { token: refreshToken } + ); + localStorage.setItem('token', data.token); + localStorage.setItem('refreshToken', data.refreshToken); + axiosInstance.defaults.headers['Authorization'] = + 'Bearer ' + data.token; + processQueue(null, data.token); + return axiosInstance(originalRequest); + } catch (err) { + processQueue(err, null); + localStorage.removeItem('token'); + localStorage.removeItem('refreshToken'); + window.location.href = '/login'; + return Promise.reject(err); + } finally { + isRefreshing = false; + } + } + + return Promise.reject(error); + } +); + +export default axiosInstance; diff --git a/src/ui/CustomTextField.tsx b/src/ui/CustomTextField.tsx new file mode 100644 index 0000000..6610188 --- /dev/null +++ b/src/ui/CustomTextField.tsx @@ -0,0 +1,35 @@ +// components/CustomTextField.tsx +import React from 'react'; +import TextField from '@mui/material/TextField'; +import { Controller } from 'react-hook-form'; + +interface CustomTextFieldProps { + name: string; + control: any; + label: string; + type?: string; + error?: boolean; + helperText?: string; +} + +const CustomTextField: React.FC = ({ name, control, label, type = 'text', error, helperText }) => { + return ( + ( + + )} + /> + ); +}; + +export default CustomTextField; diff --git a/src/ui/CustomizedInputsStyled.tsx b/src/ui/CustomizedInputsStyled.tsx index 68e6f23..11af0e5 100644 --- a/src/ui/CustomizedInputsStyled.tsx +++ b/src/ui/CustomizedInputsStyled.tsx @@ -5,6 +5,7 @@ import { styled } from "@mui/material/styles"; import TextField from "@mui/material/TextField"; import Visibility from "@mui/icons-material/Visibility"; import VisibilityOff from "@mui/icons-material/VisibilityOff"; +import { Controller } from 'react-hook-form'; // Define your styled component const CssTextField = styled(TextField)({ @@ -32,6 +33,10 @@ type InputType = { label: string; type: string; endAdornmentBoolean?: React.ReactNode; + name: string; + control: any; + error?: boolean; + helperText?: string; }; // Client component @@ -39,15 +44,28 @@ export default function CustomizedInputsStyled({ label, type, endAdornmentBoolean, + name, + control, + error, + helperText, }: InputType) { return ( - <> - }} - /> - + ( + }} + id="custom-css-outlined-input" + {...field} + type={type} + error={error} + helperText={helperText} + variant="outlined" + fullWidth + /> + )} + /> ); } diff --git a/src/ui/customizedButtons.tsx b/src/ui/customizedButtons.tsx index 33e7efc..b722dd7 100644 --- a/src/ui/customizedButtons.tsx +++ b/src/ui/customizedButtons.tsx @@ -16,12 +16,14 @@ const ColorButton = styled(Button)(() => ({ type InputType = { label: string; statIcon?: React.ReactNode; + onPress?: () => void; + btnType?: "submit" | "button"; }; -export default function CustomizedButtons({ label, statIcon }: InputType) { +export default function CustomizedButtons({ label, statIcon, onPress, btnType = "button" }: InputType) { return ( <> - + {statIcon && statIcon} {label}