How to ensure user is logged in on refresh in firebase? [duplicate] - javascript

When mounting a component, I'd like to render either a log in screen or the app, depending on wether the user in logged in. However, each time I refresh, the user is logged out. How would I keep them logged in?
App Component:
firebase.initializeApp(config); //init the firebase app...
class App extends Component {//this component renders when the page loads
constructor(props){
super(props);
this.state = {user: firebase.auth().currentUser};//current user
}
render(){
if (this.state.user) {//if you are logged in
return (
<Application/>
);
} else {//if you are not logged in
return (
<Authentication/>
);
}
}
}
This is the method I'm using to log in the user (which works fine):
let email = "some";
let password = "thing";
const auth = firebase.auth();
const promise = auth.signInWithEmailAndPassword(email, password);

The user's token is automatically persisted to local storage, and is read when the page is loaded. This means that the user should automatically be authenticated again when you reload the page.
The most likely problem is that your code doesn't detect this authentication, since your App constructor runs before Firebase has reloaded and validated the user credentials. To fix this, you'll want to listen for the (asynchronous) onAuthStateChanged() event, instead of getting the value synchronously.
constructor(props){
super(props);
firebase.auth().onAuthStateChanged(function(user) {
this.setState({ user: user });
});
}

I had the same issue with Firebase in React. Even though Firebase has an internal persistence mechanisms, you may experience a flicker/glitch when reloading the browser page, because the onAuthStateChanged listener where you receive the authenticated user takes a couple of seconds. That's why I use the local storage to set/reset it in the onAuthStateChanged listener. Something like the following:
firebase.auth.onAuthStateChanged(authUser => {
authUser
? localStorage.setItem('authUser', JSON.stringify(authUser))
: localStorage.removeItem('authUser')
});
Then it can be retrieved in the constructor of a React component when the application starts:
constructor(props) {
super(props);
this.state = {
authUser: JSON.parse(localStorage.getItem('authUser')),
};
}
You can read more about it over here.

Personally, I feel this way is the best and the most simple.
state = {authUser: null,show:'none'};
componentDidMount() {
firebase.auth().onAuthStateChanged(user => {
if (user) { this.setState({ authUser: true,show:'block'})}
else { this.setState({ authUser: false,show:'none'})}
})
}
return{ /*important*/ this.state.authuser==false?
<Login/>
:this.state.authuser==true?
<Home style={{display:this.state.show}}/>
:<div>/*show loading/spash page*/
</div>
}
You can also add routing to this with Home as the default route ('/' do not use exact path use path instead) and then make it auto reroute to the login or signup if the user is not logged in.

Related

Updating Vue state during navigation with vue-router

I'm building a Vue 3 app which uses Vue Router and Pinia for state management.
In my global state I'm defining a property which tells me if a user is logged in so the user can navigate through the app. However, the login process is being handled by Cognito, so once I enter the app, I click on the login button, it takes me to a Cognito screen which handles the login process and then redirects me back to my app's home page.
So after the redirect I extract some params from the resulting URL and I want to save them in the global state, for this I was using the beforeEach guard to parse the URL, check for the params, update the store and then reading from it to check the valid session.
But my issue was that the navigation continued even before the state was updated. I ended up using setTimeout just to see if waiting for a bit solved the issue and it did
router.beforeEach((to) => {
const mainStore = useMainStore();
if (to.name === 'Login') {
return;
}
if (to.hash !== '') {
const queryParams = window.location.href.split('&');
const paramValues = queryParams.map(param => {
})
const payload = {
id_token: paramValues.find(param => param.name === 'id_token').value,
access_token: paramValues.find(param => param.name === 'access_token').value,
token_type: paramValues.find(param => param.name === 'token_type').value,
isAuthenticated: true
}
//Here I'm trying to update my global state
mainStore.updateTokens(payload);
}
// Here I use timeout, but before I just had the check for the property
setTimeout(() => {
if (!mainStore.isAuthenticated) return '/login';
}, 3000)
});
How should I handled this case? I've read about the beforeResolve guard but I'm not sure on how to use it; basically I just need to know how should I go about performing some async operation (like fetching data from server) during the navigation, not inside components.

Firebase auth onAuthStateChanged resolving to null

I'm trying to get the current user UID from a Firebase database - a simple task in principle. I'm following the guide for managing users from the firebase documentation but I still am unable to get a solution. Everywhere I see that firebase.auth().currentuser will not be defined during initialisation - ok great. I use a listener instead, and the listener fires as it should. But for some reason when it fires, the value of user is null.
export default class ShoppingCategories extends Component {
constructor(props) {
super(props);
this.state = {
GeoFire: new GeoFire(firebase.database().ref('stores-geo').push()),
stores: exstores,
brands: [],
storesSelected: []
}
}
componentDidMount() {
this.getStoreTile();
firebase.auth().onAuthStateChanged((user) =>{
if(user){
this.locateStores(user);
}else{
console.log("No user found: "+user);
}
});
}
...
Not sure what I'm doing wrong here. I get
No user found: null
printed to the console.
Solved my own problem. My service worker was causing the page to be served from cache. I removed the service worker caching feature which fixed the problem. Not exactly sure how to fix this long term right now but will update once I solve.

How do I integrate the cognito hosted UI into a react app?

I am creating a react app - using create-react-app and amplify - and I am trying to set up authentication. I don't seem to be able to handle the federated logins using the hosted UI.
There are some pages which require no authentication to reach and then some which require a user to be logged in. I would like to use the hosted UI since that's prebuilt. I have been following the getting started docs here: https://aws-amplify.github.io/docs/js/authentication
For background I have the following components:
- Amplify - an amplify client which wraps calls in methods like doSignIn doSignOut etc. The idea is to keep all this code in one place. This is a plain javascript class
- Session - provides an authentication context as a React context. This context is set using the amplify client. It has HOC's for using the context
- Pages - some wrapped in the session HOC withAuthentication which only renders the page if the user has logged in
This structure is actually taken from a Firebase tutorial: https://www.robinwieruch.de/complete-firebase-authentication-react-tutorial/
Maybe this is just not feasible with Amplify? Though the seem similar enough to me that it should work. The basic idea is that the Session provides a single auth context which can be subscribed to by using the withAuthentication HOC. That way any component that requires a user will be rendered as soon as a user has logged in.
Originally I wrapped the entire App component in the withAuthenticator HOC provided by amplify as described in the docs. However this means that no pages are accessible without being authenticated - home page needs to be accessible without an account.
Next I tried calling to the hosted UI with a sign in button and then handling the response. The problem is when the hosted UI has logged a user in then it redirects back to the app causing it to reload - which is not ideal for a single page app.
Then I tried checking if the user is authenticated every time the app starts - to deal with the redirect - but this becomes messy as I need to move a lot of the amplify client code to the Session context so that it can initialise correctly. The only way I can see to get this is using the Hub module: https://aws-amplify.github.io/docs/js/hub#listening-authentication-events The downside is that after logging in, the app refreshes and there's still a moment when you are logged out which makes the user experience weird.
I would have thought that there would be a way to not cause an application refresh. Maybe that's just not possible with the hosted UI. The confusing thing to me is that the documentation doesn't mention it anywhere. In actual fact there is documentation around handling the callback from the hosted UI which as far as I can see never happens because the entire page refreshes and so the callback can never run.
I've tried to trim this down to just what's needed. I can provide more on request.
Amplify:
import Amplify, { Auth } from 'aws-amplify';
import awsconfig from '../../aws-exports';
import { AuthUserContext } from '../Session';
class AmplifyClient {
constructor() {
Amplify.configure(awsconfig);
this.authUserChangeListeners = [];
}
authUserChangeHandler(listener) {
this.authUserChangeListeners.push(listener);
}
doSignIn() {
Auth.federatedSignIn()
.then(user => {
this.authUserChangeListeners.forEach(listener => listener(user))
})
}
doSignOut() {
Auth.signOut()
.then(() => {
this.authUserChangeListeners.forEach(listener => listener(null))
});
}
}
const withAmplify = Component => props => (
<AmplifyContext.Consumer>
{amplifyClient => <Component {...props} amplifyClient={amplifyClient} />}
</AmplifyContext.Consumer>
);
Session:
const provideAuthentication = Component => {
class WithAuthentication extends React.Component {
constructor(props) {
super(props);
this.state = {
authUser: null,
};
}
componentDidMount() {
this.props.amplifyClient.authUserChangeHandler((user) => {
this.setState({authUser: user});
});
}
render() {
return (
<AuthUserContext.Provider value={this.state.authUser}>
<Component {...this.props} />
</AuthUserContext.Provider>
);
}
}
return withAmplify(WithAuthentication);
};
const withAuthentication = Component => {
class WithAuthentication extends React.Component {
render() {
return (
<AuthUserContext.Consumer>
{user =>
!!user ? <Component {...this.props} /> : <h2>You must log in</h2>
}
</AuthUserContext.Consumer>
);
}
}
return withAmplify(WithAuthentication);
};
The auth context is provided once at the top level:
export default provideAuthentication(App);
Then pages that require authentication can consume it:
export default withAuthentication(MyPage);
What I would like to happen is that after the user signs in then I can set the AuthUserContext which in turn updates all the listeners. But due to the redirect causing the whole app to refresh the promise from Auth.federatedSignIn() can't resolve. This causes the user to be displayed with You must log in even though they just did.
Is there a way to block this redirect whilst still using the hosted UI? Maybe launch it in another tab or in a popup which doesn't close my app? Or am I going about this the wrong way? It just doesn't feel very 'Reacty' to cause full page refreshes.
Any help will be greatly appreciated. I can provide more details on request.
Instead of chaining onto the Auth's promise, you can use Amplify's build-in messaging system to listen to events. Here is how I do it in a custom hook and how I handle what gets rendered in Redux.
import { Auth, Hub } from 'aws-amplify';
import { useEffect } from 'react';
function useAuth({ setUser, clearUser, fetchQuestions, stopLoading }) {
useEffect(() => {
Hub.listen('auth', ({ payload: { event, data } }) => {
if (event === 'signIn') {
setUser(data);
fetchQuestions();
stopLoading();
}
if (event === 'signOut') {
clearUser();
stopLoading();
}
});
checkUser({ fetchQuestions, setUser, stopLoading });
}, [clearUser, fetchQuestions, setUser, stopLoading]);
}
async function checkUser({ fetchQuestions, setUser, stopLoading }) {
try {
const user = await Auth.currentAuthenticatedUser();
setUser(user);
fetchQuestions();
} catch (error) {
console.log(error);
} finally {
stopLoading();
}
}

How do you get the value of a data attribute in a component in another component without using event bus?

I have a data attribute 'auth', which holds whether the user is logged in or not. If it is empty then the user is not logged in, if it has 'loggedin' in it then the user is logged in.
This is in one component called 'App.vue' and i have another component called 'DashboardComponent.vue'. If the user isn't authenticated, but types in '/dashboard' URL, i want the app to kick the user back to the login screen. How do i get the 'auth' data from the 'App.vue' component into the 'DashboardComponent.vue' and check if the user is authenticated (before the dashboard renders)?
EDIT:
This is how im currently trying to do it
[DashboardComponent]
EventBus.$on('logged-in', status => {
this.auth = status
})
beforeMount () {
if (this.auth !== 'loggedin') {
router.push({name: 'login'})
}
}
Is this the correct method? If so, why is it not working?
Declare your data into main file where vue initializes like
window.App = new Vue({
el: '#app',
router,
components: { App },
data: function(){
return {
auth:''
}
}
})
after that you may change it in main file as
mounted:function(){
//when logged in then change status
this.status = 'loggedIn'
}
Now access it in any component using App.status

How to access a current state in another component?

I am trying to access this user set in another component. I passed the state down from the highest component(which is app) using this function to change state.
handleRegisterSubmit(e, username, password) {
e.preventDefault();
axios.post('/auth/register', {
username,
password,
}).then(res => {
console.log(res.data.user.id)
this.setState({
auth: res.data.auth,
user: res.data.user,
currentPage: 'selectSources',
});
}).catch(err => console.log(err));
}
After that hits the model in the backend it brings me back a response which is where i change the user state from null to a object with the user information.
I then pass that information to the home component.
renderHomeIfloggedin(){
if (this.state.auth){
console.log(this.state.user)
return <Home auth={this.state.auth} userInfo={this.state.user}/>
}
}
inside the home component i hit this function
renderSelectSources(){
if (!this.state.dataLoaded){
console.log(this.props.userInfo,'djfosidjfodisj')
return (
<div>
{this.props.user}
<SelectSources user={this.props.userInfo} test={this.returnSources}/>
</div>)
}
}
i then try to access the user object from the app component using props. and pas it to the Selectsources component.
inside of the select sources component I want to post the news object along with the user object and send it to the backend. The news object is being sent fine, but im struggling to access the current state. When i look in the react developer tools, the user state is the information that i wanted, but the props information is undefined when i console log it for some reason.
handleClick(source_object) {
console.log(source_object,'ioefjsoejfoi',this.props.user);
axios.post('/news', {
source: source_object,
})
.then(res => {
console.log("Posted"+ source_object.source.name);
console.log(res);
})
.catch(err => console.log(err));
The screenshot shows that the user object is still the current state, so i am not sure where i went wrong.
It seems that your problem is about what this is in the method handleClick,by its name I assume that method is some kind of callback of a button when is pressed, so when that method is called on a click event the this object is no longer pointing to the this of your SelectSources class (Where your userInfo is)
To fix this issue you have to bind your method in the constructor of the SelectSources class
class SelectSources extends Component {
constructor(props){
super(props);
// Make sure the handleClick method is always pointing to the this of this class
this.handleClick = this.handleClick.bind(this);
}
}

Categories

Resources