Merge pull request 'login setup' (#1) from feature/authentication into develop

Reviewed-on: DigiMantra/digiev_frontend#1
This commit is contained in:
Mohit kalshan 2025-01-21 07:38:43 +00:00
commit f67c9c17bc
16 changed files with 403 additions and 285 deletions

View file

@ -1,6 +1,8 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": false,
"singleQuote": false
}
"useTabs": true,
"semi": true,
"singleQuote": false,
"bracketSpacing": true
}

View file

@ -32,6 +32,7 @@
"mui-tel-input": "^7.0.0",
"prop-types": "^15.8.1",
"react": "^19.0.0",
"react-cookie": "^7.2.2",
"react-dom": "^19.0.0",
"react-dropzone": "^14.3.5",
"react-hook-form": "^7.54.2",
@ -68,8 +69,8 @@
},
"devDependencies": {
"@types/node": "^22.10.5",
"@types/react": "^19.0.3",
"@types/react": "^19.0.4",
"@types/react-dom": "^19.0.2",
"typescript": "^5.7.2"
"typescript": "^5.7.3"
}
}

View file

@ -95,6 +95,9 @@ importers:
react:
specifier: ^19.0.0
version: 19.0.0
react-cookie:
specifier: ^7.2.2
version: 7.2.2(react@19.0.0)
react-dom:
specifier: ^19.0.0
version: 19.0.0(react@19.0.0)
@ -127,13 +130,13 @@ importers:
specifier: ^22.10.5
version: 22.10.5
'@types/react':
specifier: ^19.0.3
specifier: ^19.0.4
version: 19.0.4
'@types/react-dom':
specifier: ^19.0.2
version: 19.0.2(@types/react@19.0.4)
typescript:
specifier: ^5.7.2
specifier: ^5.7.3
version: 5.7.3
packages:
@ -1598,6 +1601,9 @@ packages:
'@types/graceful-fs@4.1.9':
resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==}
'@types/hoist-non-react-statics@3.3.6':
resolution: {integrity: sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==}
'@types/html-minifier-terser@6.1.0':
resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==}
@ -2339,6 +2345,10 @@ packages:
resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==}
engines: {node: '>= 0.6'}
cookie@0.7.2:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'}
cookie@1.0.2:
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
engines: {node: '>=18'}
@ -4915,6 +4925,11 @@ packages:
resolution: {integrity: sha512-sZ41cxiU5llIB003yxxQBYrARBqe0repqPTTYBTmMqTz9szeBbE37BehCE891NZsmdZqqP+xWKdT3eo3vOzN8w==}
engines: {node: '>=14'}
react-cookie@7.2.2:
resolution: {integrity: sha512-e+hi6axHcw9VODoeVu8WyMWyoosa1pzpyjfvrLdF7CexfU+WSGZdDuRfHa4RJgTpfv3ZjdIpHE14HpYBieHFhg==}
peerDependencies:
react: '>= 16.3.0'
react-dev-utils@12.0.1:
resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==}
engines: {node: '>=14'}
@ -5742,6 +5757,9 @@ packages:
resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==}
engines: {node: '>=8'}
universal-cookie@7.2.2:
resolution: {integrity: sha512-fMiOcS3TmzP2x5QV26pIH3mvhexLIT0HmPa3V7Q7knRfT9HG6kTwq02HZGLPw0sAOXrAmotElGRvTLCMbJsvxQ==}
universalify@0.2.0:
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
engines: {node: '>= 4.0.0'}
@ -7883,6 +7901,11 @@ snapshots:
dependencies:
'@types/node': 22.10.5
'@types/hoist-non-react-statics@3.3.6':
dependencies:
'@types/react': 19.0.4
hoist-non-react-statics: 3.3.2
'@types/html-minifier-terser@6.1.0': {}
'@types/http-errors@2.0.4': {}
@ -8744,6 +8767,8 @@ snapshots:
cookie@0.7.1: {}
cookie@0.7.2: {}
cookie@1.0.2: {}
core-js-compat@3.40.0:
@ -11735,6 +11760,13 @@ snapshots:
regenerator-runtime: 0.13.11
whatwg-fetch: 3.6.20
react-cookie@7.2.2(react@19.0.0):
dependencies:
'@types/hoist-non-react-statics': 3.3.6
hoist-non-react-statics: 3.3.2
react: 19.0.0
universal-cookie: 7.2.2
react-dev-utils@12.0.1(eslint@8.57.1)(typescript@5.7.3)(webpack@5.97.1):
dependencies:
'@babel/code-frame': 7.26.2
@ -12745,6 +12777,11 @@ snapshots:
dependencies:
crypto-random-string: 2.0.0
universal-cookie@7.2.2:
dependencies:
'@types/cookie': 0.6.0
cookie: 0.7.2
universalify@0.2.0: {}
universalify@2.0.1: {}

View file

@ -10,34 +10,12 @@
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>DigiEV - Eco-friendly Charge</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View file

@ -1,12 +1,28 @@
import { BrowserRouter as Router} from 'react-router-dom';
import AppRouter from './router';
import AppRouter from "./router";
import { useSelector } from "react-redux";
import { useMatch, useNavigate, useSearchParams } from "react-router-dom";
import { useEffect } from "react";
import { RootState } from "./redux/store";
import { withCookies, ReactCookieProps } from "react-cookie";
function App() {
return (
<Router>
<AppRouter />
</Router>
const App: React.FC<ReactCookieProps> = ({ cookies }) => {
const navigate = useNavigate();
const isPanel = useMatch("/auth/login");
const isCookiePresent = !!cookies?.get("authToken");
console.log("cookies present:", isCookiePresent);
const [searchParams] = useSearchParams();
const isAuthenticated = useSelector(
(state: RootState) => state.authReducer.isAuthenticated
);
}
export default App;
useEffect(() => {
if (isPanel && isAuthenticated) {
navigate("/panel/dashboard");
}
}, [isPanel, isAuthenticated, searchParams]);
return <AppRouter />;
};
export default withCookies(App);

View file

@ -11,6 +11,9 @@ import LogoutRoundedIcon from '@mui/icons-material/LogoutRounded';
import MoreVertRoundedIcon from '@mui/icons-material/MoreVertRounded';
import MenuButton from '../MenuButton';
import { Avatar } from '@mui/material';
import { useDispatch } from 'react-redux';
import { logoutUser } from '../../redux/slices/authSlice';
import { useCookies } from 'react-cookie';
const MenuItem = styled(MuiMenuItem)({
margin: '2px 0',
@ -19,12 +22,20 @@ const MenuItem = styled(MuiMenuItem)({
export default function OptionsMenu({ avatar }: { avatar?: boolean }) {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const dispatch = useDispatch();
const [cookies, setCookie, removeCookie] = useCookies(['authToken']);
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const handleLogout = () => {
dispatch(logoutUser({ removeCookie }));
console.log('click')
handleClose();
};
return (
<React.Fragment>
<MenuButton
@ -70,7 +81,7 @@ export default function OptionsMenu({ avatar }: { avatar?: boolean }) {
<MenuItem onClick={handleClose}>Settings</MenuItem>
<Divider />
<MenuItem
onClick={handleClose}
onClick={handleLogout}
sx={{
[`& .${listItemIconClasses.root}`]: {
ml: 'auto',

View file

@ -1,39 +1,22 @@
import { intersection, uniq } from 'lodash';
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { checkUserAuth } from '../redux/slices/authSlice';
const useAuth = (caps, { matchAllCaps = false }) => {
const useAuth = () => {
const dispatch = useDispatch();
const [isAuthorized, setIsAuthorized] = React.useState(false);
const { isAuthenticated, userCapabilities } = useSelector((state) => ({
const { isAuthenticated } = useSelector((state) => ({
isAuthenticated: state.authReducer.isAuthenticated,
userCapabilities: state.authReducer.userCapabilities,
}));
const requiredCaps = React.useMemo(
() => uniq(Array.isArray(caps) ? caps : [caps]),
[caps]
);
React.useEffect(() => {
const userMatchedCaps = intersection(userCapabilities, requiredCaps);
let isUserAuthorized = matchAllCaps
? userMatchedCaps?.length === requiredCaps?.length
: userMatchedCaps?.length > 0;
if (requiredCaps.length === 0) {
isUserAuthorized = true;
}
// if (isAuthenticated === null) {
if (isAuthenticated === null || false || undefined) {
if (isAuthenticated) {
dispatch(checkUserAuth());
} else {
setIsAuthorized(isAuthenticated && isUserAuthorized);
setIsAuthorized(false);
}
}, [dispatch, isAuthenticated, requiredCaps, userCapabilities]);
}, [dispatch, isAuthenticated]);
return { isAuthenticated, isAuthorized };
};

View file

@ -1,29 +1,32 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import reportWebVitals from './reportWebVitals';
import App from './App';
import { Provider } from 'react-redux';
import store from './redux/store/store.ts';
import { Slide, ToastContainer } from 'react-toastify';
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { Provider } from "react-redux";
import store from "./redux/store";
import { Slide, ToastContainer } from "react-toastify";
import { BrowserRouter as Router } from "react-router-dom";
import { CookiesProvider } from "react-cookie";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<CookiesProvider defaultSetOptions={{ path: "/" }}>
<Provider store={store}>
<Router>
<App />
</Router>
<ToastContainer
autoClose={2000}
hideProgressBar
theme="dark"
transition={Slide}
toastStyle={{ border: '1px solid dimgray' }}
toastStyle={{ border: "1px solid dimgray" }}
/>
</Provider>
</CookiesProvider>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View file

@ -1,16 +1,36 @@
import axios from 'axios';
import axios from "axios";
import { Cookies } from "react-cookie";
const cookies = new Cookies();
const http = axios.create({
baseURL: process.env.REACT_APP_BACKEND_URL,
});
console.log(process.env.REACT_APP_BACKEND_URL);
http.interceptors.request.use((config) => {
const authToken = localStorage.getItem('authToken');
const authToken = cookies.get("authToken");
console.log(authToken);
if (authToken) {
config.headers.Authorization = authToken;
}
return config;
});
http.interceptors.response.use(
(response) => response,
(error) => {
const isCookiePresent = cookies.get("authToken");
console.log(isCookiePresent,"jkk")
if (
error.response &&
isCookiePresent &&
(error.response.status === 403 || error.response.status === 401)
) {
cookies.remove("authToken", { path: "/" });
window.location.href = "/";
}
return Promise.reject(error);
}
);
export default http;

View file

@ -0,0 +1,9 @@
import React from 'react'
function NotFoundPage() {
return (
<div>NotFoundPage</div>
)
}
export default NotFoundPage;

View file

@ -19,7 +19,7 @@ const categoryRows = [
];
export default function Vehicles() {
const [modalOpen, setModalOpen] = useState(false);
const [modalOpen, setModalOpen] = useState<boolean>(false);
const [editRow, setEditRow] = useState<any>(null);
const { reset } = useForm();

11
src/redux/reducers.ts Normal file
View file

@ -0,0 +1,11 @@
import { combineReducers } from "@reduxjs/toolkit";
import authReducer from "./slices/authSlice";
const rootReducer = combineReducers({
authReducer,
});
export type RootState = ReturnType<typeof rootReducer>;
export default rootReducer;

View file

@ -1,21 +1,37 @@
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit"
import axios from "axios"
import http from "../../lib/https"
import { toast } from "react-toastify"
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
import axios from "axios";
import http from "../../lib/https";
import { toast } from "react-toastify";
import { Cookies } from "react-cookie";
const cookies = new Cookies();
// Define types for state
interface User {
id: string
email: string
id: string;
email: string;
}
interface AuthState {
user: User | null
isAuthenticated: boolean
isLoading: boolean
error: string | null
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
error: string | null;
}
export const checkUserAuth = createAsyncThunk<
boolean,
void,
{ rejectValue: any }
>("application/checkUserAuth", async (_, thunkAPI) => {
try {
const isCookiePresent = cookies.get("authToken");
if (!isCookiePresent) return thunkAPI.rejectWithValue(null);
return thunkAPI.fulfillWithValue(true);
} catch (error) {
console.log(error);
return thunkAPI.rejectWithValue(error);
}
});
// Async thunk for login
export const loginUser = createAsyncThunk<
User,
@ -23,16 +39,16 @@ export const loginUser = createAsyncThunk<
{ rejectValue: string }
>("auth/login", async ({ email, password }, { rejectWithValue }) => {
try {
const response = await http.post("admin/login", { email, password })
localStorage.setItem("authToken", response.data?.data?.token) // Save token
toast.success(response.data?.message)
return response.data
const response = await http.post("admin/login", { email, password });
cookies.set("authToken", response.data?.data?.token);
toast.success(response.data?.message);
return response.data;
} catch (error: any) {
return rejectWithValue(
error.response?.data?.message || "An error occurred"
)
);
}
})
});
// Async thunk for register
export const registerUser = createAsyncThunk<
@ -44,75 +60,101 @@ export const registerUser = createAsyncThunk<
const response = await axios.post(
"https://health-digi.dmlabs.in/auth/register",
data
)
return response.data
);
return response.data;
} catch (error: any) {
return rejectWithValue(
error.response?.data?.message || "An error occurred"
)
);
}
})
});
export const logoutUser = createAsyncThunk<
void,
{ removeCookie: any },
{ rejectValue: string }
>("auth/logout", async ({ removeCookie }, { rejectWithValue }) => {
try {
removeCookie("authToken", { path: "/auth" });
toast.success("You have been logged out successfully.");
} catch (error: any) {
return rejectWithValue(
error.response?.data?.message || "Failed to log out."
);
}
});
const initialState: AuthState = {
user: null,
isAuthenticated: false,
isLoading: false,
error: null,
}
};
const authSlice = createSlice({
name: "auth",
initialState,
reducers: {
logout: (state) => {
state.user = null
state.isAuthenticated = false
state.user = null;
state.isAuthenticated = false;
},
},
extraReducers: (builder) => {
builder
// Login
.addCase(loginUser.pending, (state) => {
state.isLoading = true
state.error = null
state.isLoading = true;
state.error = null;
})
.addCase(
loginUser.fulfilled,
(state, action: PayloadAction<User>) => {
state.isLoading = false
state.isAuthenticated = true
state.user = action.payload
state.isLoading = false;
state.isAuthenticated = true;
state.user = action.payload;
}
)
.addCase(
loginUser.rejected,
(state, action: PayloadAction<string | undefined>) => {
state.isLoading = false
state.error = action.payload || "An error occurred"
state.isLoading = false;
state.error = action.payload || "An error occurred";
}
)
// Register
.addCase(registerUser.pending, (state) => {
state.isLoading = true
state.error = null
state.isLoading = true;
state.error = null;
})
.addCase(
registerUser.fulfilled,
(state, action: PayloadAction<User>) => {
state.isLoading = false
state.isAuthenticated = true
state.user = action.payload
state.isLoading = false;
state.isAuthenticated = true;
state.user = action.payload;
}
)
.addCase(
registerUser.rejected,
(state, action: PayloadAction<string | undefined>) => {
state.isLoading = false
state.error = action.payload || "An error occurred"
state.isLoading = false;
state.error = action.payload || "An error occurred";
}
)
// Logout
.addCase(logoutUser.fulfilled, (state) => {
state.user = null;
state.isAuthenticated = false;
})
.addCase(
logoutUser.rejected,
(state, action: PayloadAction<string | undefined>) => {
state.error =
action.payload || "An error occurred during logout";
}
);
},
})
});
export const { logout } = authSlice.actions
export default authSlice.reducer
export default authSlice.reducer;

View file

@ -1,13 +1,10 @@
import { configureStore } from '@reduxjs/toolkit';
import authReducer from '../slices/authSlice.ts'
const store = configureStore({
reducer: {
auth: authReducer,
},
});
import rootReducer from './reducers';
export const store = configureStore({
reducer: rootReducer,
})
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export default store;

View file

@ -1,22 +1,29 @@
import { Routes as BaseRoutes, Navigate, Route } from 'react-router-dom';
// import useAuth from "./hooks/useAuth";
import React, { Suspense } from 'react';
import LoadingComponent from './components/Loading';
import DashboardLayout from './layouts/DashboardLayout';
import Login from './pages/Auth/Login';
import SignUp from './pages/Auth/SignUp';
import Dashboard from './pages/Dashboard';
import Vehicles from './pages/Vechiles';
import { Routes as BaseRoutes, Navigate, Route } from "react-router-dom";
import { Suspense } from "react";
import LoadingComponent from "./components/Loading";
import DashboardLayout from "./layouts/DashboardLayout";
import Login from "./pages/Auth/Login";
import SignUp from "./pages/Auth/SignUp";
import Dashboard from "./pages/Dashboard";
import Vehicles from "./pages/Vehicles";
import { useSelector } from "react-redux";
import { RootState } from "./redux/reducers";
import { useCookies } from "react-cookie";
function ProtectedRoute({
caps,
component,
}: {
caps: string[];
component: React.ReactNode;
}) {
if (!localStorage.getItem('authToken'))
return <Navigate to={`/auth/login`} replace />;
interface ProtectedRouteProps {
component: JSX.Element;
cookies?: { get: (key: string) => string | null };
}
function ProtectedRoute({ component }: ProtectedRouteProps): JSX.Element {
const [cookies] = useCookies(["authToken"]);
const isCookiePresent = !!cookies?.authToken;
const isAuthenticated = useSelector(
(state: RootState) => state.authReducer.isAuthenticated
);
if (!isAuthenticated && !isCookiePresent) {
return <Navigate to="/auth/login" replace />;
}
return component;
}
@ -39,11 +46,11 @@ export default function AppRouter() {
<Route path="/panel" element={<DashboardLayout />}>
<Route
path="dashboard"
element={<ProtectedRoute caps={[]} component={<Dashboard />} />}
element={<ProtectedRoute component={<Dashboard />} />}
/>
<Route
path="vehicles"
element={<ProtectedRoute caps={[]} component={<Vehicles />} />}
element={<ProtectedRoute component={<Vehicles />} />}
/>
<Route path="*" element={<>404</>} />
</Route>

View file

@ -10,10 +10,11 @@
],
"compilerOptions": {
"baseUrl": ".",
"jsx": "react-jsx",
"types": ["react", "react-dom"],
"lib": ["dom", "dom.iterable", "esnext"],
"paths": {
"@/*": [
"./src/*"
]
"@/*": ["./src/*"]
}
}
}