I was trying to develop a web app in React with Express server and PostgreSQL database also using the OpenWeatherMap API.
I configured an authentication strategy based on JWT Token on the server and created the various registration and access endpoints.
The endpoints work and, especially in the login one, a JWT token is generated, which will then be saved in the localStorage on the client side.
- index.js (back-end)
const jwtStrategy = new JWTStrategy.Strategy(
{
jwtFromRequest: JWTStrategy.ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET,
},
async (jwtPayload, done) => {
try {
const user = await findUserById(jwtPayload.user.id);
if (user) {
return done(null, user);
} else {
return done(null, false);
}
} catch (error) {
console.error("Error finding user by ID:", error);
return done(error, false);
}
}
);
passport.use(jwtStrategy);
// Signup endpoint
app.post("/api/signup", async (req, res) => {
try {
const { name, username, email, password } = req.body;
// Verify if user already exists
const existingUser = await findUserByEmail(email);
if (existingUser) {
return res.status(400).json({ message: "Email already exists" });
}
// Password hashing
const hashedPassword = await bcrypt.hash(password, 10);
// Create new user
const newUser = await signUp(name, username, email, hashedPassword);
res.status(201).json({ message: "User created successfully" });
} catch (error) {
console.error("Error signing up:", error);
res.status(500).json({ message: "Internal server error" });
}
});
// Login endpoint
app.post("/api/login", async (req, res) => {
try {
const { email, password } = req.body;
// Verify if user already exists
const user = await findUserByEmail(email);
if (!user) {
return res.status(401).json({ message: "User not found" });
}
// Verify password match
const passwordMatch = await bcrypt.compare(password, user.password);
if (!passwordMatch) {
return res.status(401).json({ message: "Invalid email or password" });
}
// Token JWT creation
const token = jwt.sign({ user: user }, process.env.JWT_SECRET, {
expiresIn: "1h",
});
res.status(200).json({ token: token });
} catch (error) {
console.error("Error logging in:", error);
res.status(500).json({ message: "Internal server error" });
}
});
I also created an /api/validateToken endpoint (using passport.authenticate()) that does nothing other than return a success message and the user object if the token is valid
Everything works great regarding the backend, but I'm having some problems with rendering elements in the frontend.
In my Header component, I wanted to show the two buttons Login and Register if the user is not authenticated and the two buttons Favorite Cities and Logout if the user is authenticated.
Initially, I tried the following approach:
- Header.jsx
import React from "react";
import { Link } from "react-router-dom";
import "../styles/Header.css";
import { validateToken, logOut } from "../client-utils";
function Header() {
const token = localStorage.getItem("token");
const [loading, setLoading] = React.useState(true);
const [isTokenValid, setIsTokenValid] = React.useState(null);
React.useEffect(() => {
validateToken(token, setIsTokenValid, setLoading);
}, [token]);
function handleLogout() {
logOut(token, setIsTokenValid);
}
return (
<header>
{loading && (
<div className="loading-container">
<div className="loading-spinner"></div>
</div>
)}
<nav>
<ul>
{isTokenValid ? (
<>
<li>
<Link to="/">Preferred Cities</Link>
</li>
<li>
<button onClick={handleLogout}>Logout</button>
</li>
</>
) : (
<>
<li>
<Link to="/signup">Sign Up</Link>
</li>
<li>
<Link to="/login">Sign In</Link>
</li>
</>
)}
</ul>
</nav>
</header>
);
}
export default Header;
With validateToken() and logOut() declare as follows:
async function validateToken(token, setIsTokenValid, setLoading) {
try {
const response = await axios.get(
"http://localhost:5000/api/validateToken",
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
console.log(response.data);
setIsTokenValid(true);
} catch (err) {
console.log("Errore nella validazione del token");
setIsTokenValid(false);
} finally {
setLoading(false);
}
}
async function logOut(token, setIsTokenValid) {
try {
const response = await axios.get("http://localhost:5000/api/logout", {
headers: {
Authorization: `Bearer ${token}`,
},
});
console.log(response.data);
localStorage.removeItem("token");
setIsTokenValid(false);
} catch (err) {
console.log(err);
}
}
The logout logic works correctly and elements are rendered correctly upon logout.
But after authenticating, however, I am forced to refresh the page to see the updated elements (i.e. the Preferred Cities and Logout buttons).
So I tried this other approach in useEffect hook:
React.useEffect(() => {
async function checkToken() {
if (token) {
await validateToken(token, setIsTokenValid, setLoading);
} else {
setLoading(false);
}
}
checkToken();
}, [token]);
But unfortunately, it still needs refreshing to display the items correctly.
Does anyone know how I can solve this problem? I've been on it for over 4 hours, with no results.
Thanks in advance to anyone who can help me.
Just putting variables in the dependency array is not enough.
useEffectis only executed when the component renders. Here the value intokenwill change, but that change will not trigger a rerender, since it is not part of the components state, or parent state.For
useEffectto notice a change the component needs to render.See the docs for more info. Also would suggest reading through it all. Would be helpful in getting an idea of the usage and pitfalls as well.
So you can maybe store the response in a context (or some state management library) at the top level, for the component tree to rerender and use this value.