After pressing the login button and authenticating, I want the application to direct me to the desired page. But either it does not redirect when I press the button, or even if I enter the wrong password, it goes to the desired page and comes back. because it hasn't been authenticated yet. I couldn't find the right way.
When authenticated, I want to go to the page with the resource list. The path of this page is "/".
I'm sharing a few pieces of code to give you an idea. App:
const Layout = ({ children }) => {
return (
<div className="link">
<MiniDrawer>
<Outlet />
</MiniDrawer>
</div>
);
};
const router = createBrowserRouter([
{
path: "/login",
element: <Login />,
},
{
path: "/",
element: (
<AdminGuard>
<Layout />
</AdminGuard>
),
children: [
{
path: "/",
element: <Resources />,
},
{
path: "/create",
element: <CreateResource />,
},
{
path: "/edit/:id",
element: <EditResource />,
},
],
},
]);
function App({ Component }) {
return (
<div className="App">
<RouterProvider router={router} />
</div>
);
}
export default App;
Guard:
import React from "react";
import { Route, Navigate, Outlet } from "react-router-dom";
import { AuthService } from "../services/auth.services";
const AdminGuard = ({ children }) => {
if (AuthService.isAuthenticated()) {
return children;
} else {
return <Navigate to="/login" />;
}
};
export default AdminGuard;
Login Page:
import { useDispatch } from "react-redux";
import LoginView from "./view";
import { AuthService } from "../../services/auth.services";
function Login() {
const dispatch = useDispatch();
function onSubmit({ username, password }) {
dispatch(AuthService.userLogin({ username, password }));
}
return (
<>
<LoginView onSubmit={onSubmit} />
</>
);
}
export default Login;
Auth Service:
import { createAsyncThunk } from "@reduxjs/toolkit";
import { axiosApiInstance } from "./axios.services";
import { TokenService } from "./token.service";
export const AuthService = {};
AuthService.userLogin = createAsyncThunk(
"userSlice/userLogin",
async ({ username, password }, { rejectWithValue }) => {
try {
const response = await axiosApiInstance.post(
"https://localhost:7105/api/Connect",
{ username, password },
{
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username: typeof username === "string" ? username.toString() : "",
password: typeof password === "string" ? password.toString() : "",
}),
}
);
const tokenResponse = { accessToken: response?.data };
TokenService.setToken(tokenResponse);
return tokenResponse;
} catch (error) {
return rejectWithValue(error);
}
}
);
AuthService.isAuthenticated = () => {
return localStorage.getItem("access_token") ? true : false;
};
AuthService.logout = () => {
TokenService.clearToken();
window.location.href = "/login";
};
Token Service:
export const TokenService = {};
TokenService.setToken = ({ accessToken }) => {
localStorage.setItem("access_token", accessToken);
};
TokenService.clearToken = () => {
localStorage.removeItem("access_token");
};
Auth Slice:
import { useNavigate } from "react-router-dom";
import { AuthService } from "../../services/auth.services";
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
const initialState = {
data: {},
isLoading: false,
hasError: false,
};
const authSlice = createSlice({
name: "authSlice",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(AuthService.userLogin.pending, (state) => {
state.isLoading = true;
state.hasError = false;
})
.addCase(AuthService.userLogin.fulfilled, (state, { payload }) => {
state.pending = false;
state.data = payload;
})
.addCase(AuthService.userLogin.rejected, (state) => {
state.isLoading = false;
state.hasError = true;
});
},
});
export const selectAuthState = (state) => state.authSlice.data;
export default authSlice.reducer;
I don't see anything that redirects a user after they authenticate. If I am understanding the issue correctly you need to update the code to handle redirecting to the protected route after they successfully authenticate.
For this I suggest a few changes.
Update the auth check to be stateful instead of relying on reading from localStorage. Updates to localStorage don't trigger React components to rerender so auth checks in the AdminGuard
are likely to be stale. Since the state.authSlice.data
state is the token value the UI should reference this value, or specifically state.authSlice.data.accessToken
.
import React from "react";
import { Navigate, Outlet, useLocation } from "react-router-dom";
import { useSelector } from 'react-redux';
import { selectAuthState } from "../slices/auth.slice";
const AdminGuard = () => {
// Capture the current location for auth redirect
const location = useLocation();
// Access authentication state
const { accessToken } = useSelector(selectAuthState);
// Wait until any initial/pending auth checks complete
if (accessToken === undefined) {
return null; // or loading indicator/spinner/etc
}
// Return Outlet for nested routes if authenticated
// Return redirect with current location in state
return accessToken
? <Outlet />
: <Navigate to="/login" redirect state={{ from: location }} />;
};
export default AdminGuard;
Auth service
Update the logout function to also be a thunk. This is so you can add reducer cases to handle resetting/clearing the state.data
value when a user logs out.
import { createAsyncThunk } from "@reduxjs/toolkit";
import { axiosApiInstance } from "./axios.services";
import { TokenService } from "./token.service";
export const AuthService = {};
AuthService.userLogin = createAsyncThunk(
"userSlice/userLogin",
async ({ username, password }, { rejectWithValue }) => {
try {
const { data } = await axiosApiInstance.post(
"https://localhost:7105/api/Connect",
{ username, password },
{
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username: typeof username === "string" ? username.toString() : "",
password: typeof password === "string" ? password.toString() : "",
}),
}
);
const tokenResponse = { accessToken: data };
TokenService.setToken(tokenResponse);
return tokenResponse;
} catch (error) {
return rejectWithValue(error);
}
}
);
AuthService.userLogout = createAsyncThunk(
"userSlice/userLogin",
() => {
TokenService.clearToken();
}
);
const authSlice = createSlice({
name: "authSlice",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(AuthService.userLogin.pending, (state) => {
state.isLoading = true;
state.hasError = false;
})
.addCase(AuthService.userLogin.fulfilled, (state, action) => {
state.pending = false;
state.data = action.payload;
})
.addCase(AuthService.userLogin.rejected, (state) => {
state.isLoading = false;
state.hasError = true;
})
.addCase(AuthService.userLogout.pending, (state) => {
state.isLoading = true;
state.hasError = false;
})
.addCase(AuthService.userLogout.fulfilled, (state) => {
state.pending = false;
state.data = {}; // <-- reset on logout
})
.addCase(AuthService.userLogout.rejected, (state) => {
state.isLoading = false;
state.hasError = true;
});
},
});
App
Update App
to render the AdminGuard
as a layout route component.
const router = createBrowserRouter([
{
path: "/login",
element: <Login />,
},
{
element: <Layout />,
children: [
{
element: <AdminGuard />,
children: [
{
path: "/",
element: <Resources />,
},
{
path: "/create",
element: <CreateResource />,
},
{
path: "/edit/:id",
element: <EditResource />,
},
],
},
],
},
]);
function App({ Component }) {
return (
<div className="App">
<RouterProvider router={router} />
</div>
);
}
Login
Update the Login
component's submit handler to wait for the login action to resolve and then issue an imperative redirect back to the route path the user was originally attempting to access.
import { useLocation, useNavigate } from 'react-router-dom';
import { useDispatch } from "react-redux";
import LoginView from "./view";
import { AuthService } from "../../services/auth.services";
function Login() {
const dispatch = useDispatch();
const { state } = useLocation();
const navigate = useNavigate();
onSubmit = async ({ username, password }) => {
try {
// Wait for login action to complete, e.g. resolve
await dispatch(AuthService.userLogin({ username, password })).unwrap();
// Redirect to original target, or safe non-protected route
const { from } = state || { from: /* safe fallback that isn't a protected route */ };
navigate(from, { replace: true });
} catch(error) {
// handle errors?
}
}
return (
<>
<LoginView onSubmit={onSubmit} />
</>
);
}
export default Login;
Similarly now the "logout" function is the same, dispatch the AuthService.userLogout
action and redirect after.
const dispatch = useDispatch();
const navigate = useNavigate();
const logout = async () => {
try {
await dispatch(AuthService.userLogout()).unwrap();
navigate("/login", { replace: true });
} catch(error) {
// handle errors?
}
};