Wait for localStorage update before dispatching fetch on useEffect - reactjs

I have a login page which returns a response , after which im setting a token in localStorage.
const login = async (e) => {
e.preventDefault();
try {
const loginRes = await axios.post(`${url}/users/login/`, {
username,
password,
});
if (loginRes.data?.success === 1) {
clearInputs();
localStorage.setItem("user_id", loginRes.data.user_id);
localStorage.setItem("token", loginRes.data.token);
history.push("/home");
} else {
toast(loginRes.data?.data);
}
} catch (error) {
toast("An error occurred, please try again");
}
};
Im redirecting to home page after the response as:
history.push("/home");
Inside home page im dispatching the action to get data from api,
useEffect(() => {
if (!localStorage.getItem("token")) {
history.push("/");
return;
}
dispatch(getItems())
}, [history, dispatch]);
export const getItems = () => {
return async(dispatch) => {
dispatch(fetchItemsRequest());
await api.fetchItems().then(
(res) => {
dispatch(fetchSuccess(res.data.items));
},
(error) => {
dispatch(fetchFailed(error));
}
);
}
};
For the fetchItems() request, it requires the token from localStorage.
The problem is that when i click login , it is redirecting the home page and on the first load, token is passed as null , and no items are loaded in the home page as token is invalid. When i refresh the page, the data loads correctly. How do i wait for the localStorage to be set before the useEffect is called from home page?

Related

Why is my Azure redirect running twice and not stopping the fuction

I want to add the functionality for admins to disable end users access if necessary. It works just fine with non-SSO users. The check will prevent the user from logging in and show them a 'user is not active error'. When a non-active user tries to use Azure SSO to log in, the Azure SSO is still successful and displaying a spinner because there is not an active user. It should not allow them to 'log in' and redirect them to the home page with a displayed error that says 'user is not active'
Here is the function to change the user's isActive status on the backend
const changeUserStatus = asyncHandler(async (req, res) => {
const currentUser = await User.findById(req.user._id);
if (!currentUser) {
res.status(401);
throw new Error('User not found');
}
const user = await User.findByIdAndUpdate(req.params.id, req.body, {
new: true,
});
console.log(user);
res.status(201).json(user);
});
From the backend as well, here is the check for a user's isActive status in the normal login function
//check isActive status
if (user.isActive === false) {
res.status(400);
throw new Error('Not an active user');
}
Here is the check in the Azure SSO log in
if (!user.isActive) {
errors.azure = 'User is no longer permitted to access this application';
res.status(400);
throw new Error(errors.azure);
// console.log(errors);
// return res.status(401).json(errors);
}
Here is my authService.js
// Login user
const login = async (userData) => {
const response = await axios.post(API_URL + 'login', userData);
if (response.data) {
localStorage.setItem('user', JSON.stringify(response.data));
}
return response.data;
};
const azureLogin = async () => {
const response = await axios.get(API_URL + 'az-login');
return response.data;
};
Here is my authSlice
// Login user
export const login = createAsyncThunk('auth/login', async (user, thunkAPI) => {
try {
return await authService.login(user);
} catch (error) {
return thunkAPI.rejectWithValue(extractErrorMessage(error));
}
});
// Login user using AAD - this action sends the user to the AAD login page
export const azureLogin = createAsyncThunk(
'users/azureLogin',
async (thunkAPI) => {
try {
return await authService.azureLogin();
} catch (error) {
return thunkAPI.rejectWithValue(extractErrorMessage(error));
}
}
);
// Login user using AAD - this action redirects the user from the AAD login page
// back to the app with a code
export const azureRedirect = createAsyncThunk(
'users/azureRedirect',
async (code, thunkAPI) => {
try {
return await authService.azureRedirect(code);
} catch (error) {
return thunkAPI.rejectWithValue(extractErrorMessage(error));
}
}
);
And here is the AzureRedirect.jsx component. This is the component that receives the flow from the Microsoft/AAD login page. It is the re-entry point of the application, so to speak.
useEffect(() => {
const code = {
code: new URLSearchParams(window.location.search).get('code'),
};
if (user) {
toast.success(`Logged in as ${user.firstName} ${user.lastName}`);
navigate('/');
} else if (code) {
// This CANNOT run more than once
const error = dispatch(azureRedirect(code));
console.log(error);
} else {
console.log('No code found in URL');
}
}, [dispatch, navigate, user]);
if (!user) {
displayedOutput = <Spinner />;
} else {
displayedOutput = (
<div>
An error has been encountered, please contact your administrator.
<br />
<Link to='/login'>Return to Login</Link>
</div>
);
}
return <div className='pt-4'>{displayedOutput}</div>;

Having to press log out twice to actually destory my user's session - react + express

I've got a react front end that performs some actions. The relevant axios requests look like so:
const login = async () => {
await Axios.post('http://localhost:8000/login', {
username: username,
password: password,
}).then((response) => {
console.log("login response: ", response);
window.location.href = "http://localhost:3000/";
}).catch(err => {
alert(err.response.data);
});
};
// run on first render to see if user session is still active - remove console log later
useEffect(() => {
Axios.get("http://localhost:8000/isLoggedIn").then((response) => {
console.log("isLoggedIn resonse: ", response);
if (response.data.loggedIn === true) {
setLoginStatus(`Logged in as ${response.data.user}`);
}
})
}, [])
const Logout = async () => {
try {
await Axios.get('http://localhost:8000/logout').then((response) => {
console.log(response);
window.location.href = "http://localhost:3000/login";
}).catch(err => {
alert(err);
});
} catch (error) {
alert(error)
}
};
I keep having to press log out twice to actually log my user out. The logout route runs before the "isLoggedIn" route, according to my network tab. And it's successful, too. Here are the isLoggedIn and logout routes in my express backend:
export function isLoggedIn( req: any, res: any) {
if (req.session.user) {
// if our session already has a user, send true to the frontend
// frontend runs this get login on first render, so will have user data if cookie has not expired.
res.send({loggedIn: true, user: req.session.user})
} else {
res.send({loggedIn: false});
}
}
export function logout(req: any, res: any) {
if (req.session) {
req.session.destroy( (err: any) => {
if (err) {
res.status(400).send('Unable to log out');
} else {
res.send("Logout successful");
}
});
} else {
res.end();
}
}
I'm getting a successful logout. I just cannot figure out why I need to hit the logout button twice on the frontend to actually destroy the session and log the user out? Is there something timing related that I may be missing here?

Firebase Passwordless Email Authentication Error in Expo App

I am setting up passwordless Auth in my Expo app using the Firebase SDK. I've gotten to the point where emails are being sent to the user's desired address with a redirect link back to the app. When the user clicks the link, they are indeed redirected but they are not being authenticated. I am receiving a generic error in the console :
ERROR: [Error: An internal error has occurred.]
But I know that my credentials are passing through properly as I have logged them out when the function runs:
isSignInWithEmailLink:true, url: exp://10.0.0.27:19000?apiKey=AIzaSyAmpd5DdsjOb-MNfVH3MgF1Gn2nT3TBcnY&oobCode=7FJTfBjM28gkn6GfBSAdgAk7wOegg9k4D5poVcylhSYAAAF8BO5gHQ&mode=signIn&lang=en
I am calling useEffect on this function:
useEffect(() => {
signInWithEmailLink();
}, []);
Send Link To Email (WORKING)
const sendSignInLinkToEmail = (email) => {
return auth
.sendSignInLinkToEmail(email, {
handleCodeInApp: true,
url: proxyUrl,
})
.then(() => {
return true;
});
};
User clicks on a link from the email to redirect to the app to Authenticate (NOT WORKING)
const signInWithEmailLink = async () => {
const url = await Linking.getInitialURL();
if (url) {
handleUrl(url);
}
Linking.addEventListener('url', ({ url }) => {
handleUrl(url);
});
};
(RETURNING ERROR)
const handleUrl = async (url) => {
const isSignInWithEmailLink = auth.isSignInWithEmailLink(url);
console.log('isSignInWithEmailLink: ', isSignInWithEmailLink, 'url', url);
if (isSignInWithEmailLink) {
try {
await auth.signInWithEmailLink(email, url);
} catch (error) {
console.log('ERROR:', error);
}
}
};
Have you enabled email sign in in your firebase console?
Are you storing the email in localStorage? It looks undefined in your logic.
Your listener should be in the useEffect hook.
I've code my code working looking like this:
const handleGetInitialURL = async () => {
const url = await Linking.getInitialURL()
if (url) {
handleSignInUrl(url)
}
}
const handleDeepLink = (event: Linking.EventType) => {
handleSignInUrl(event.url)
}
useEffect(() => {
handleGetInitialURL()
Linking.addEventListener('url', handleDeepLink)
return () => {
Linking.removeEventListener('url', handleDeepLink)
}
}, [])
You should use the onAuthStateChanged within useEffect rather than try and log the user in at that point in time. useEffect is used when you need your page to re-render based on changes.
For example:
useEffect(() => {
// onAuthStateChanged returns an unsubscriber
const unsubscribeAuth = auth.onAuthStateChanged(async authenticatedUser => {
try {
await (authenticatedUser ? setUser(authenticatedUser) : setUser(null));
setIsLoading(false);
} catch (error) {
console.log(error);
}
});
// unsubscribe auth listener on unmount
return unsubscribeAuth;
}, []);
You should invoke the user sign in method through other means such as a button to sign in, or validate user credentials at some other point within your app.
custom function:
const onLogin = async () => {
try {
if (email !== '' && password !== '') {
await auth.signInWithEmailAndPassword(email, password);
}
} catch (error) {
setLoginError(error.message);
}
};
Source: https://blog.jscrambler.com/how-to-integrate-firebase-authentication-with-an-expo-app

Persist auth state in react/react native for Firebase

I am using react native for an ios app and firebase for authentication. Every time I leave the app and come back, it asks for a login. I want to persist the firebase login but don't really know where to put it.
I know I need to put this in:
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.LOCAL)
I have the following signIn function that runs when the login button is pressed on the signInScreen:
const signIn = async () => {
setLoading(true);
try {
await firebase.signIn(email, password);
const uid = firebase.getCurrentUser().uid;
const userInfo = await firebase.getUserInfo(uid);
const emailArr = userInfo.email.split("#");
setUser({
username: emailArr[0],
email: userInfo.email,
uid,
isLoggedIn: true,
});
} catch (error) {
alert(error.message);
} finally {
isMounted.current && setLoading(false);
}
};
I have the following signIn stuff in my firebaseContext:
const Firebase = {
getCurrentUser: () => {
return firebase.auth().currentUser;
},
signIn: async (email, password) => {
return firebase.auth().signInWithEmailAndPassword(email, password);
},
getUserInfo: async (uid) => {
try {
const user = await db.collection("users").doc(uid).get();
if (user.exists) {
return user.data();
}
} catch (error) {
console.log("Error #getUserInfo", error);
}
},
logOut: async () => {
return firebase
.auth()
.signOut()
.then(() => {
return true;
})
.catch((error) => {
console.log("Error #logout", error);
});
},
};
Where do I put the persist code I listed above from the docs?
Thanks!
When do you check if someon is signed in or not?
From the code shown it looks like you check it manuelly by calling currentUser. You have to consider that the persistance of auth state is asynchronous. That means if you call currentUser on auth before the localy saved auth state is loaded you would get there null and thing that the user is not signed in.
To get the auth state Firebase recommend to use the onAuthStateChanges event listener. With that you can listen to auth state changes no matter if you logged in or the persistet auth state is loaded.
The usage is very simple:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
// User is signed in.
} else {
// No user is signed in.
}
});
That is the reson I asked where you check if someon is signed in or not. If I could see that code I could help you adopt it to use that event listener.

React this.props.history.push stops working when I place it in a promise. Why?

When I call this function I get redirected to the page I request:
submit = () => {
if(!this.state.sell){
this.props.history.push({pathname: "/events"})
}
else {
this.props.history.push({pathname: "/stripeConnectSignUp"})
}
}
However, when I change the function to update data in the database before redirecting, using this code:
submit = () => {
if(!this.state.sell){
this.props.history.push({pathname: "/events"})
}
else {
let token = localStorage.getItem("token");
axios.patch(`${process.env.REACT_APP_API}/updateUser`, {name: this.state.name, token: token}).then(res => {
this.props.history.push({pathname: "/stripeConnectSignUp"})
}).catch(err => {
console.log('axios err', err)
})
}
}
It redirects to the page I want for a split second before redirected back to the original page. There are no errors at either the front end or back end.
For completeness, this is the backend controller that I am posting data to:
module.exports = (req, res) => {
let user = jwt.verify(req.body.token, process.env.SECRET)
User.findByIdAndUpdate(user._id, {name: req.body.name}).then(data=>{res.send({})})
}
This happens regardless of if I use
import { withRouter } from 'react-router-dom'
What am I missing?

Resources