LocalStorage values are one login behind - reactjs

I'm having this problem with my AuthContext from a React app. I check the localStorage and the values stored are from the last user logged, so I have to login twice to get the corrent info.
This is the Context code. And the 'useLocalStorage' function. I guess in the useMemo is the problem but I haven't been able to solve it. Is this a bad approach?
import { createContext, useContext, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useLocalStorage } from "./useLocalStorage";
import jwtDecode from "jwt-decode";
const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [token, setToken] = useLocalStorage("accessToken", null);
const [userId, setUserId] = useLocalStorage("currentUserId", null);
const [userEmail, setUserEmail] = useLocalStorage("currentUserEmail", null);
const [userRole, setUserRole] = useLocalStorage("currentUserRole", null);
const [user, setUser] = useState({});
const navigate = useNavigate();
const login = async (credentials) => {
return fetch("https://localhost:7264/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(credentials),
})
.then((res) => res.json())
.then((res) => {
setToken(res);
console.log(res);
const tokenToDecode = localStorage.getItem("accessToken");
setUser(jwtDecode(tokenToDecode));
setUserId(user.sub);
setUserEmail(
user[
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
]
);
setUserRole(
user["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"]
);
});
};
const logout = () => {
setToken(null);
setUserId(null);
setUserEmail(null);
setUserRole(null);
navigate("/", { replace: true });
};
const value = useMemo(
() => ({
token,
login,
logout,
userId,
userEmail,
userRole,
}),
[token]
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
export const useAuth = () => {
return useContext(AuthContext);
};
import { useState } from "react";
export const useLocalStorage = (keyName, defaultValue) => {
const [storedValue, setStoredValue] = useState(() => {
try {
const value = localStorage.getItem(keyName);
if (value) {
return JSON.parse(value);
} else {
localStorage.setItem(keyName, JSON.stringify(defaultValue));
return defaultValue;
}
} catch (err) {
return defaultValue;
}
});
const setValue = (newValue) => {
try {
localStorage.setItem(keyName, JSON.stringify(newValue));
} catch (err) {}
setStoredValue(newValue);
};
return [storedValue, setValue];
};

Related

I need to refresh the page to login | React and Axios

I have a problem when I want to log in to the login by entering the email and password. What happens is that when I enter with the correct email and correct password, the animation appears but it stays cycled, and if I refresh the page and try again, now it lets me enter into the application
Here's my login form code:
import axios from "axios";
import { useRef, useState } from "react";
import { storeToken } from "../utils/authServices";
import { useNavigate } from "react-router-dom";
import { useLoading } from "../context/hooks/useLoading";
import { LoginForm } from "../components";
export const Login = () => {
const API_URL = "https://api.app"; //I hide the API for security reasons
const { run } = useLoading();
const [error, setError] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const navigate = useNavigate();
const correoRef = useRef("");
const passwordRef = useRef("");
const handleSubmit = async (e) => {
e.preventDefault();
const { value: correo } = correoRef.current;
const { value: password } = passwordRef.current;
await axios
.post(`${API_URL}/api/auth/login/`, {
correo,
password,
})
.then((response) => {
storeToken(response.data.token);
run();
setTimeout(() => {
navigate("/nueva-solicitud");
}, 1000);
})
.catch((err) => {
console.log(err.response.data);
setError(true);
setErrorMessage(err.response.data.msg);
});
};
return (
<LoginForm
correoRef={correoRef}
passwordRef={passwordRef}
handleSubmit={handleSubmit}
error={error}
errorMessage={errorMessage}
/>
);
};
import { createContext, useReducer, useContext } from "react";
const initialState = {
loading: false,
alerts: [],
};
const reducers = (state, action) => {
switch (action.type) {
case "LOADING_RUN":
return {
...state,
loading: true,
};
case "LOADING_STOP":
return {
...state,
loading: false,
};
default:
return { ...state };
}
};
const AppContext = createContext();
const AppContextProvider = (props) => {
const [state, dispatch] = useReducer(reducers, initialState);
return <AppContext.Provider value={{ state, dispatch }} {...props} />;
};
const useAppContext = () => useContext(AppContext);
export { AppContextProvider, useAppContext };
import { useMemo } from "react";
import { useAppContext } from "../AppContext";
export const useLoading = () => {
const { dispatch } = useAppContext();
const loading = useMemo(
() => ({
run: () => dispatch({ type: "LOADING_RUN" }),
stop: () => dispatch({ type: "LOADING_STOP" }),
}),
[dispatch]
);
return loading;
};
import jwt_decode from "jwt-decode";
export const storeToken = (token) => {
localStorage.setItem("token", token);
};
export const getToken = (decode = false) => {
const token = localStorage.getItem("token");
if (decode) {
const decoded = jwt_decode(token);
return decoded;
}
return token;
};
export const logout = () => {
localStorage.removeItem("token");
};
How can I log in without refreshing the page?
There's two problems here. One is you're using await with a .then .catch block. Pick one or the other. You're also never calling the stop() dispatch when your async call is complete which appears to be responsible for removing the loader.
Instead of:
const { run } = useLoading();
Use:
const { run, stop } = useLoading();
Then change this:
setTimeout(() => {
navigate("/nueva-solicitud");
}, 1000);
To this:
setTimeout(() => {
navigate("/nueva-solicitud");
stop();
}, 1000);
Although I would just recommend writing the entire promise like this:
try {
run();
const response = await axios
.post(`${API_URL}/api/auth/login/`, {
correo,
password,
});
storeToken(response.data.token);
navigate("/nueva-solicitud");
stop();
} catch (err) {
stop();
console.log(err.response.data);
setError(true);
setErrorMessage(err.response.data.msg);
}

React custom http-hook abortController() bug

I have this custom http hook with abort when you try to go to a different page (I saw it in a tutorial but I am not truly sure I need it). When I fetch data with it and useEffect(), I have this error on the backend but the request is executed and everything is as planned. My question is, how to improve my code so it does not throw this error and do I need this functionality with abortController() ?
http-hook.ts
import { useCallback, useRef, useEffect } from "react";
import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import { selectError, showError } from "src/redux/error";
import { selectLoading, startLoading, stopLoading } from "src/redux/loading";
export const useHttpClient = () => {
const dispatch = useDispatch();
const error = useSelector(selectError);
const loading = useSelector(selectLoading);
const activeHttpRequests: any = useRef([]);
const sendRequest = useCallback(
async (url, method = "GET", body = null, headers = {}) => {
dispatch(startLoading());
const httpAbortCtrl = new AbortController();
activeHttpRequests.current.push(httpAbortCtrl);
try {
const response = await fetch(url, {
method,
body,
headers,
signal: httpAbortCtrl.signal,
});
const responseData = await response.json();
activeHttpRequests.current = activeHttpRequests.current.filter(
(reqCtrl) => reqCtrl !== httpAbortCtrl
);
if (!response.ok) {
throw new Error(responseData.message);
}
dispatch(stopLoading());
return responseData;
} catch (err) {
dispatch(showError(err.message));
dispatch(stopLoading());
throw err;
}
},
[]
);
useEffect(() => {
return () => {
activeHttpRequests.current.forEach((abortCtrl: any) => abortCtrl.abort());
};
}, []);
return { loading, error, sendRequest };
};
UserInfo.tsx
import React, { Fragment, useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { useHttpClient } from "src/hooks/http-hook";
import classes from "./UserInfo.module.css";
const UserInfo = () => {
const { sendRequest } = useHttpClient();
const [currentUser, setCurrentUser] = useState<any>();
const userId = useParams<any>().userId;
useEffect(() => {
const fetchCurrentUser = async () => {
try {
const responseData = await sendRequest(
`http://localhost:5000/api/user/${userId}`
);
setCurrentUser(responseData.user);
console.log("currentUser ", currentUser);
} catch (err) {
console.log(err);
}
};
fetchCurrentUser();
}, [sendRequest ,userId]);
return currentUser ? (
<Fragment>
<div className={classes.cover} />
<div className={classes.user_info}>
<img
alt="user_img"
src={`http://localhost:5000/${currentUser.image}`}
className={classes.user_img}
/>
<div className={classes.text}>
<p>
Name: {currentUser.name} {currentUser.surname}
</p>
<p>Email: {currentUser.email}</p>
<p>Age: {currentUser.age}</p>
</div>
</div>{" "}
</Fragment>
) : (
<p>No current user</p>
);
};
export default UserInfo;
Backend
getCurrentUser.ts controller
const getCurrentUser = async (
req: express.Request,
res: express.Response,
next: express.NextFunction
) => {
const userId = req.params.userId;
let user;
try {
user = await User.findById(userId);
} catch (err) {
const error = new HttpError("Could not fetch user", 500);
return next(error);
}
res.json({ user: user.toObject({ getters: true }) });
};

Blank page after refresh react firebase authentication

I have the above firebase db. I want to extract the displayName value and use it in a greeting message after the user is successfully login (e.g. Hello George!). I manage to achieve this but when I refresh the page everything disappears and in console I get this error "index.esm2017.js:1032 Uncaught TypeError: Cannot read properties of undefined (reading 'indexOf')".
Is this a problem of how I extract the displayName from firebase document?
Can someone explain to me what is the problem, please?
Here is my code:
AuthContext.js
import { createContext, useContext, useEffect, useState } from "react";
import {
onAuthStateChanged,
signInWithEmailAndPassword,
signOut,
} from "firebase/auth";
import { auth } from "../utils/firebase/firebase.utils";
const UserContext = createContext();
export const AuthContextProvider = ({ children }) => {
const [user, setUser] = useState({});
const signIn = (email, password) =>
signInWithEmailAndPassword(auth, email, password);
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
console.log(currentUser);
setUser(currentUser);
});
return () => unsubscribe();
}, []);
const logOut = () => signOut(auth);
return (
<UserContext.Provider value={{ user, signIn, logOut }}>
{children}
</UserContext.Provider>
);
};
export const UserAuth = () => {
return useContext(UserContext);
};
WelcomePage.jsx
import React, { useState, useEffect } from "react";
import { UserAuth } from "../contexts/AuthContext";
import { db } from "../utils/firebase/firebase.utils";
import { doc, Firestore, getDoc } from "firebase/firestore";
const WelcomePage = () => {
const [userDetails, setUserDetails] = useState({});
const { user } = UserAuth();
useEffect(() => {
const docRef = doc(db, "users", user.uid);
const fetchData = async () => {
try {
const docSnap = await getDoc(docRef);
setUserDetails(docSnap.data());
console.log(docSnap.data());
} catch (e) {
console.log(e);
}
};
fetchData();
}, [user]);
return (
<div>
<h1>Hello, {userDetails.displayName}!</h1>
</div>
);
};
export default WelcomePage;
You might only want to fetch documents in the WelcomePage component if there's a truthy uid value to use.
const { user } = UserAuth();
useEffect(() => {
const fetchData = async () => {
const docRef = doc(db, "users", user.uid);
try {
const docSnap = await getDoc(docRef);
setUserDetails(docSnap.data());
console.log(docSnap.data());
} catch (e) {
console.log(e);
}
};
if (user?.id) {
fetchData();
}
}, [user]);

How to make silent token refresh with axios interceptors in React using ContextAPI?

So what I'm trying to do is to refresh access token stored in ContextAPI by refresh token stored in a cookie. I use axios response interceptor to check 401 & 403 status codes and then call refresh function from useGetNewAccessToken hook which sends new access token from the server. The problem is, React app always keeps pushing me back to the login page on page refresh instead of calling this function and adding newly acquired access token to the Authorization header. Here is how I set my interceptors:
import axios from "axios";
import useGetNewAccessToken from "./useGetNewAccessToken";
import { AuthContext } from "./useAuth";
import { useContext, useEffect } from "react";
export const instance = axios.create({
baseURL: "http://localhost:8080",
headers: {
"Content-Type": "application/json",
},
withCredentials: true,
});
export const useSetInterceptors = () => {
const { accessToken } = useContext(AuthContext);
const refresh = useGetNewAccessToken();
useEffect(() => {
const requestInterceptor = instance.interceptors.request.use(
(config) => {
if (!config.headers["Authorization"]) {
console.log("No authorization header is present");
config.headers["Authorization"] = `Bearer ${accessToken}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
const responseInterceptor = instance.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
const prevRequest = error?.config;
console.log("Request failed");
if (
error?.response?.status === 401 &&
error?.response?.status === 403 &&
!prevRequest.sent
) {
prevRequest.sent = true;
const accessToken = await refresh();
prevRequest.headers["Authorization"] = `Bearer ${accessToken}`;
return instance(prevRequest);
}
return Promise.reject(error);
}
);
return () => {
instance.interceptors.request.eject(requestInterceptor);
instance.interceptors.request.eject(responseInterceptor);
};
}, [accessToken, refresh]);
return instance;
};
export default useSetInterceptors;
I handle authentication process in a custom useAuth hook:
import React, { useState, createContext, useContext, useEffect } from "react";
import axios from "axios";
import { instance } from "../hooks/useSetInterceptors";
import { Link, useNavigate, useLocation } from "react-router-dom";
export const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [authed, setAuthed] = useState(false);
const [moderator, setModerator] = useState(false);
const [accessToken, setAccessToken] = useState("");
const [refreshToken, setRefreshToken] = useState("");
const [authorities, setAuthorities] = useState([]);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const navigate = useNavigate();
const location = useLocation();
const from = location.state?.from?.pathname || "/";
const signIn = async (e, username, password) => {
e.preventDefault();
const result = await getTokens(username, password);
if (result) {
console.log("User has signed in");
}
};
const getTokens = async (username, password) => {
const api = `http://localhost:8080/api/v1/public/signIn?username=${username}&password=${password}`;
const res = await instance.get(api, {
withCredentials: true,
params: {
username: username,
password: password,
},
});
const data = await res.data;
setAccessToken(data["access_token"]);
navigate(from, { replace: true });
};
return (
<AuthContext.Provider
value={{
authed,
setAuthed,
moderator,
setModerator,
getTokens,
getAccessTokenAuthorities,
username,
password,
setUsername,
setPassword,
signIn,
isModerator,
accessToken,
setAccessToken,
refreshToken,
setRefreshToken,
}}
>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => useContext(AuthContext);
This is the hook to send new access token by refresh token:
import axios from "axios";
import { AuthContext } from "./useAuth";
import { useContext } from "react";
export const useGetNewAccessToken = async (props) => {
const { accessToken, setAccessToken } = useContext(AuthContext);
const api = `http://localhost:8080/api/v1/public/getNewAccessToken`;
const refresh = async () => {
try {
const refreshToken = document.cookie.slice(14);
const res = await axios.get(api, {
withCredentials: true,
headers: {
Authorization: `Bearer ${refreshToken}`,
"Access-Control-Allow-Origin": "localhost:8080",
},
});
const data = await res.data;
setAccessToken(data["access_token"]);
return res.data["access_token"];
} catch (error) {
console.log(error);
}
};
return refresh();
};
export default useGetNewAccessToken;
Finally this is how I use interceptors inside my Inventory component:
import React, { useContext, useEffect, useState } from "react";
import useSetInterceptors from "../hooks/useSetInterceptors";
import { AuthContext } from "../hooks/useAuth";
import { useNavigate, useLocation } from "react-router-dom";
const Inventory = (props) => {
const [inventoryItems, setInventoryItems] = useState([]);
const { moderator, accessToken } = useContext(AuthContext);
const setInterceptors = useSetInterceptors();
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
const getInventoryItems = async () => {
const api = `http://localhost:8080/api/v1/moderator/inventory`;
try {
const res = await setInterceptors.get(api, {
withCredentials: true,
});
const data = await res.data;
console.log(data);
setInventoryItems(data);
} catch (err) {
console.log("Request failed..." + err);
navigate("/signIn", { state: { from: location }, replace: true });
}
};
getInventoryItems();
}, []);
return (
<div>
<h1>Inventory</h1>
</div>
);
};
Inventory.propTypes = {};
export default Inventory;
Can it have anything to do with the fact that interceptors call themselves multiple times instead of one even though I check that in response interceptor if statement and eject them after usage? Calls are shown in here

React Authentication (Auth0) - what is the right way?

i am a newbie to react but i'm learning and need your help here.
I use Auth0 for Authentication and i have implemented their react sample in parts:
https://auth0.com/docs/quickstart/spa/react/01-login
This are parts of my code:
App.js:
<Auth0Provider
domain={AUTH_CONFIG.domain}
client_id={AUTH_CONFIG.clientId}
redirect_uri={AUTH_CONFIG.callbackUrl}
onRedirectCallback={onRedirectCallback}
>
<Router history={history}>
<RequireAuthentication>
<MyTheme>
<MyLayout />
</MyTheme>
</RequireAuthentication>
</Router>
</Auth0Provider>
Auth0Provider:
import React, { useState, useEffect, useContext } from "react";
import createAuth0Client from "#auth0/auth0-spa-js";
import jwtDecode from "jwt-decode";
import axios from "axios";
import AUTH_CONFIG from "./auth0Config";
import { useDispatch } from "react-redux";
import * as authActions from "app/auth/store/actions";
const DEFAULT_REDIRECT_CALLBACK = () =>
window.history.replaceState({}, document.title, window.location.pathname);
export const Auth0Context = React.createContext();
export const useAuth0 = () => useContext(Auth0Context);
export const Auth0Provider = ({
children,
onRedirectCallback = DEFAULT_REDIRECT_CALLBACK,
...initOptions
}) => {
const [isAuthenticated, setIsAuthenticated] = useState();
const [user, setUser] = useState();
const [auth0Client, setAuth0] = useState();
const [loading, setLoading] = useState(true);
const [popupOpen, setPopupOpen] = useState(false);
const dispatch = useDispatch();
useEffect(() => {
const initAuth0 = async () => {
console.log("initAuth0 start");
const auth0FromHook = await createAuth0Client(initOptions);
setAuth0(auth0FromHook);
const isAuthenticated = await auth0FromHook.isAuthenticated();
console.log("Authenticated from init: " + isAuthenticated);
setIsAuthenticated(isAuthenticated);
setLoading(false);
console.log("initAuth0 end");
};
initAuth0();
// eslint-disable-next-line
}, []);
const loginWithPopup = async (params = {}) => {
setPopupOpen(true);
try {
await auth0Client.loginWithPopup(params);
} catch (error) {
console.error(error);
} finally {
setPopupOpen(false);
}
const user = await getUserData();
setUser(user);
dispatch(authActions.setUserDataAuth0(user));
setIsAuthenticated(true);
};
const handleRedirectCallback = async () => {
if (!auth0Client) {
console.warn("Auth0 Service didn't initialize, check your configuration");
return;
}
setLoading(true);
await auth0Client.handleRedirectCallback();
const user = await getUserData();
setLoading(false);
setIsAuthenticated(true);
setUser(user);
dispatch(authActions.setUserDataAuth0(user));
};
const getAccessToken = async () => {
const accessToken = await auth0Client.getTokenSilently({
audience: AUTH_CONFIG.identity_audience,
scope: "read:allUsers read:UserPermission"
});
return accessToken;
};
const getIdToken = async () => {
if (!auth0Client) {
console.warn("Auth0 Service didn't initialize, check your configuration");
return;
}
const claims = await auth0Client.getIdTokenClaims();
return claims.__raw;
};
const getTokenData = async () => {
const token = await getIdToken();
const decoded = jwtDecode(token);
if (!decoded) {
return null;
}
return decoded;
};
const getUserData = async () => {
console.log("getuserdata");
const tokenData = await getTokenData();
const accessToken = await getAccessToken();
return new Promise((resolve, reject) => {
const { sub: userId } = tokenData;
const UserService =
"https://localhost:44312/api/v1/usermanagement/user/" + userId;
axios
.get(UserService, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Methods": "GET,HEAD,OPTIONS,POST,PUT",
"Access-Control-Allow-Headers":
"Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers",
"Content-Type": "application/json",
Authorization: "Bearer " + accessToken
}
})
.then(response => {
resolve(response.data);
})
.catch(error => {
// handle error
console.warn("Cannot retrieve user data", error);
reject(error);
});
});
};
return (
<Auth0Context.Provider
value={{
isAuthenticated,
user,
loading,
popupOpen,
loginWithPopup,
handleRedirectCallback,
getIdTokenClaims: (...p) => auth0Client.getIdTokenClaims(...p),
loginWithRedirect: (...p) => auth0Client.loginWithRedirect(...p),
getTokenSilently: (...p) => auth0Client.getTokenSilently(...p),
getTokenWithPopup: (...p) => auth0Client.getTokenWithPopup(...p),
logout: (...p) => auth0Client.logout(...p)
}}
>
{children}
</Auth0Context.Provider>
);
};
RequireAuthentication:
import React, { useEffect } from "react";
import { useAuth0 } from "app/auth/AuthProvider";
import { SplashScreen } from "#my";
import history from "#history";
export const RequireAuthentication = ({ children }) => {
const { isAuthenticated, loading } = useAuth0();
useEffect(() => {
console.log("checkAuth");
if (!loading) checkAuth();
// eslint-disable-next-line
}, []);
const checkAuth = () => {
console.log("checkAuth isAuthenticated: " + isAuthenticated);
console.log("checkAuth loading: " + loading);
if (!isAuthenticated && !loading) {
history.push("/login");
}
};
return isAuthenticated ? (
<React.Fragment>{children}</React.Fragment>
) : (
<SplashScreen />
);
};
callback.js:
import React, { useEffect } from "react";
import { SplashScreen } from "#my";
import { useAuth0 } from "app/auth/AuthProvider";
function Callback(props) {
const { isAuthenticated, handleRedirectCallback, loading } = useAuth0();
useEffect(() => {
const fn = async () => {
if (!loading) {
console.log("handleRedirectCallback: " + loading);
await handleRedirectCallback();
}
};
fn();
}, [isAuthenticated, loading, handleRedirectCallback]);
return <SplashScreen />;
}
export default Callback;
The problem is that the RequireAuthentication Component is rendered before the Auth0Provider is completely initialized and therefore i get never the isAuthenticated on "true".
The RequireAuthentication Component is a child of the Auth0Provider. Is it possible to wait for the Auth0Provider is fully initialized before rendering the RequireAuthentication Component???
What is the right way here?? Am I completely wrong?
Thanks
Chris
Depend on loading and isAuthenticated items in useEffect so that component will re render once they change.
useEffect(() => {
console.log("checkAuth");
if (!loading) checkAuth();
// eslint-disable-next-line
}, [loading, isAuthenticated]);

Resources