login changes
This commit is contained in:
parent
3088141451
commit
fbbbfb8f67
10
package.json
10
package.json
@ -12,19 +12,27 @@
|
|||||||
"@emotion/cache": "^11.13.1",
|
"@emotion/cache": "^11.13.1",
|
||||||
"@emotion/react": "^11.13.0",
|
"@emotion/react": "^11.13.0",
|
||||||
"@emotion/styled": "^11.13.0",
|
"@emotion/styled": "^11.13.0",
|
||||||
|
"@hookform/resolvers": "^3.9.0",
|
||||||
"@mui/icons-material": "^5.16.6",
|
"@mui/icons-material": "^5.16.6",
|
||||||
"@mui/material-nextjs": "^5.16.6",
|
"@mui/material-nextjs": "^5.16.6",
|
||||||
"next": "14.2.5",
|
"next": "14.2.5",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"sass": "^1.77.8"
|
"react-hook-form": "^7.52.2",
|
||||||
|
"sass": "^1.77.8",
|
||||||
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
|
"axios-mock-adapter": "^2.0.0",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "14.2.5",
|
"eslint-config-next": "14.2.5",
|
||||||
|
"jest": "^29.7.0",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
"use client"
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import styles from "./loginPage.module.scss";
|
import styles from "./loginPage.module.scss";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
@ -7,8 +8,42 @@ import { UTILITY_CONSTANT } from "@/utilities/utilityConstant";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import CustomizedInputsStyled from "@/ui/CustomizedInputsStyled";
|
import CustomizedInputsStyled from "@/ui/CustomizedInputsStyled";
|
||||||
import CustomizedButtons from "@/ui/customizedButtons";
|
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<typeof loginSchema>;
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
|
|
||||||
|
const { register, handleSubmit, formState: { errors }, control } = useForm<LoginFormValues>({
|
||||||
|
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 (
|
return (
|
||||||
<main className={styles.loginPage}>
|
<main className={styles.loginPage}>
|
||||||
<Grid container sx={{ height: "100%" }}>
|
<Grid container sx={{ height: "100%" }}>
|
||||||
@ -40,7 +75,7 @@ export default function LoginPage() {
|
|||||||
paddingBlock={5}
|
paddingBlock={5}
|
||||||
>
|
>
|
||||||
<img src={UTILITY_CONSTANT.IMAGES.AUTH.HEADER_LOG} alt="logo" />
|
<img src={UTILITY_CONSTANT.IMAGES.AUTH.HEADER_LOG} alt="logo" />
|
||||||
<Box width={"100%"}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Typography variant="h4">Welcome to Convexsol</Typography>
|
<Typography variant="h4">Welcome to Convexsol</Typography>
|
||||||
<Typography variant="h4">Sign in to your account</Typography>
|
<Typography variant="h4">Sign in to your account</Typography>
|
||||||
<Box
|
<Box
|
||||||
@ -51,17 +86,26 @@ export default function LoginPage() {
|
|||||||
flexDirection={"column"}
|
flexDirection={"column"}
|
||||||
gap={3}
|
gap={3}
|
||||||
>
|
>
|
||||||
<CustomizedInputsStyled label="User Name" type="text" />
|
<CustomTextField
|
||||||
<CustomizedInputsStyled
|
name="username"
|
||||||
|
control={control}
|
||||||
|
label="Username"
|
||||||
|
error={!!errors.username}
|
||||||
|
helperText={errors.username?.message}
|
||||||
|
/>
|
||||||
|
<CustomTextField
|
||||||
|
name="password"
|
||||||
|
control={control}
|
||||||
label="Password"
|
label="Password"
|
||||||
type="password"
|
type="password"
|
||||||
endAdornmentBoolean
|
error={!!errors.password}
|
||||||
|
helperText={errors.password?.message}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box width={"100%"} marginBlockStart={"20px"}>
|
<Box width={"100%"} marginBlockStart={"20px"}>
|
||||||
<CustomizedButtons label={"Sign In"} />
|
<CustomizedButtons btnType="submit" label={"Sign In"} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</form>
|
||||||
<Box
|
<Box
|
||||||
width={"100%"}
|
width={"100%"}
|
||||||
display={"flex"}
|
display={"flex"}
|
||||||
|
24
src/services/api/loginApi.ts
Normal file
24
src/services/api/loginApi.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// api.ts
|
||||||
|
import axiosInstance from '../axios/axiosInstance';
|
||||||
|
|
||||||
|
interface LoginResponse {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
gender: string;
|
||||||
|
image: string;
|
||||||
|
token: string;
|
||||||
|
refreshToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoginData {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loginApi = async (data: LoginData): Promise<LoginResponse> => {
|
||||||
|
const response = await axiosInstance.post<LoginResponse>('/auth/login', data);
|
||||||
|
return response.data;
|
||||||
|
};
|
88
src/services/axios/axiosInstance.ts
Normal file
88
src/services/axios/axiosInstance.ts
Normal file
@ -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;
|
35
src/ui/CustomTextField.tsx
Normal file
35
src/ui/CustomTextField.tsx
Normal file
@ -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<CustomTextFieldProps> = ({ name, control, label, type = 'text', error, helperText }) => {
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
name={name}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
label={label}
|
||||||
|
type={type}
|
||||||
|
error={error}
|
||||||
|
helperText={helperText}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomTextField;
|
@ -5,6 +5,7 @@ import { styled } from "@mui/material/styles";
|
|||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import Visibility from "@mui/icons-material/Visibility";
|
import Visibility from "@mui/icons-material/Visibility";
|
||||||
import VisibilityOff from "@mui/icons-material/VisibilityOff";
|
import VisibilityOff from "@mui/icons-material/VisibilityOff";
|
||||||
|
import { Controller } from 'react-hook-form';
|
||||||
|
|
||||||
// Define your styled component
|
// Define your styled component
|
||||||
const CssTextField = styled(TextField)({
|
const CssTextField = styled(TextField)({
|
||||||
@ -32,6 +33,10 @@ type InputType = {
|
|||||||
label: string;
|
label: string;
|
||||||
type: string;
|
type: string;
|
||||||
endAdornmentBoolean?: React.ReactNode;
|
endAdornmentBoolean?: React.ReactNode;
|
||||||
|
name: string;
|
||||||
|
control: any;
|
||||||
|
error?: boolean;
|
||||||
|
helperText?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Client component
|
// Client component
|
||||||
@ -39,15 +44,28 @@ export default function CustomizedInputsStyled({
|
|||||||
label,
|
label,
|
||||||
type,
|
type,
|
||||||
endAdornmentBoolean,
|
endAdornmentBoolean,
|
||||||
|
name,
|
||||||
|
control,
|
||||||
|
error,
|
||||||
|
helperText,
|
||||||
}: InputType) {
|
}: InputType) {
|
||||||
return (
|
return (
|
||||||
<>
|
<Controller
|
||||||
<CssTextField
|
name={name}
|
||||||
label={label}
|
control={control}
|
||||||
type={type}
|
render={({ field }) => (
|
||||||
id="custom-css-outlined-input"
|
<CssTextField
|
||||||
InputProps={{ endAdornment: endAdornmentBoolean && <VisibilityOff /> }}
|
label={label}
|
||||||
/>
|
InputProps={{ endAdornment: endAdornmentBoolean && <VisibilityOff /> }}
|
||||||
</>
|
id="custom-css-outlined-input"
|
||||||
|
{...field}
|
||||||
|
type={type}
|
||||||
|
error={error}
|
||||||
|
helperText={helperText}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -16,12 +16,14 @@ const ColorButton = styled(Button)<ButtonProps>(() => ({
|
|||||||
type InputType = {
|
type InputType = {
|
||||||
label: string;
|
label: string;
|
||||||
statIcon?: React.ReactNode;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<ColorButton variant="contained">
|
<ColorButton type={btnType} onClick={onPress} variant="contained">
|
||||||
{statIcon && statIcon}
|
{statIcon && statIcon}
|
||||||
{label}
|
{label}
|
||||||
</ColorButton>
|
</ColorButton>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user