React - Firebase handling Auth persistence using a separate auth class - javascript

I am building a react app that uses a simple login feature. I am only using google sign in, and am calling the signInWithPopop function to handle that. I have created a separate class to handle all the auth related code. On the navbar of my website I have a login button if the user is not signed in which switches to a profile button when the user has signed in.
This is how I am currently checking if the user is signed in (not working):
console.log(authHandler.getUser());
const[loginState, setLogin] = useState(authHandler.getUser()? true : false);
return(
<div className="navbar">
<div className="nav-options">
<NavItem name="About"></NavItem>
<NavItem name="Listings"></NavItem>
<NavItem name="Dashboard"></NavItem>
{loginState ? <NavItem name="Profile"><DropDown loginState={setLogin}></DropDown></NavItem> : <NavItem name="Login" click={() => authHandler.signIn(setLogin)}></NavItem>}
</div>
</div>
);
This is what I have for my authHandler class:
import firebase from 'firebase';
export default class Auth{
constructor(){
var firebaseConfig = {
...
};
!firebase.apps.length? firebase.initializeApp(firebaseConfig) : firebase.app();
firebase.analytics();
this.provider = new firebase.auth.GoogleAuthProvider();
}
signIn(state){
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.SESSION).then(() => {
return firebase.auth().signInWithPopup(this.provider).then((result) => {
console.log("signed in " + result.user.uid);
this.user = result.user
state(true);
}).catch((error) => {
console.log(error.message);
});
}).catch((error) => {
console.log(error.message);
})
}
getUser(){
return firebase.auth().currentUser;
}
logout(state){
//TODO: logout of firebase
state(false);
}
}
I have tried adding session and local persistence on firebase, but when I refresh the page, the user is signed out. What would be the proper way of maintaining persistence, in a separate class like this? I am trying to build this app with best practices in mind so that the code will be split up properly, and security is maintained.
Thanks!

You're supposed to use an auth state observer to get a callback whenever the user's sign in state changes. When a page first loads, the user is always immediately considered to be signed out. The callback will be invoked some time soon after the user's token has been loaded from persistence and verified. Use this state callback to determine what to render.
firebase.auth().onAuthStateChanged((user) => {
if (user) {
// User is signed in, see docs for a list of available properties
// https://firebase.google.com/docs/reference/js/firebase.User
var uid = user.uid;
// ...
} else {
// User is signed out
// ...
}
});
You might want to show a loading screen until the first callback tells you for sure if the user was previously signed in or is definitely signed out.
I suggest reading this for more information.

The way I implemented the auth state in react :
Auth.provider.tsx
import React, {
FC,
createContext,
useContext,
useEffect,
useState,
} from 'react';
import { User, auth } from 'firebase/app';
interface AuthContext {
user: User | null;
loading: boolean;
}
const defaultAuthContext = { user: null, loading: false };
const AuthUserContext = createContext<AuthContext>({ ...defaultAuthContext });
export const AuthUserProvider: FC = ({ children }) => {
const [authContext, setAuthContext] = useState<AuthContext>({
user: null,
loading: true,
});
useEffect(
() =>
auth().onAuthStateChanged((authUser) =>
setAuthContext({ user: authUser, loading: false }),
),
[],
);
return (
<AuthUserContext.Provider value={authContext}>
{children}
</AuthUserContext.Provider>
);
};
export const useAuthUser = () => useContext(AuthUserContext);
App.tsx
const App: React.FC = () => {
return <AuthUserProvider>
// anything
</AuthUserProvider>;
}
anycomponent.tsx
const { user, loading } = useAuthUser();
return loading ? <Loader /> : !user ? <NotLogged /> : <Logged />;
You could implement the observer in your class but everytime you'll need your user you should implement an useEffect watching the user. Making it global in a provider make it easier to use.
There are many other way but I think this one is the easiest to use.

Related

Prevent Router from Loading Page Briefly before Redirect

I have a session context for my NextJS application where anyone accessing /app/ directory pages have to go through an authorization check prior to allowing the user to access the page.
While my logic works in redirecting users without proper authentication, it is a bit glitchy because when someone navigate to the URL, /app/profile/ the page briefly loads before being redirected by Router.
I am wondering what is the best way to have this check happen prior to router loading the unauthorized page and redirecting them to the /login/ page.
Here are the steps in the authorization check:
Check is the user object has a property, authorized
Query the server for a session token
if the object from the server request comes back with authorized = false, then redirect user to /login/
Here is the code:
import React, { createContext, useContext, useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import axios from 'axios'
export const SessionContext = createContext(null);
const AppSession = ({ children }) => {
const router = useRouter()
const routerPath = router.pathname;
const [user, setUser] = useState({ user_id: '', user_email: '', user_avatar: ''})
useEffect(()=> {
// Check for populated user state if pages are accessed with the path /app/
if (routerPath.includes("/app/")){
if (user){
if(user.authenticated === undefined){
// Check if user session exists
axios.get('/api/auth/session/')
.then(res => {
const data = res.data;
// Update user state depending on the data returned
setUser(data)
// If user session does not exist, redirect to /login/
if (data.authenticated === false){
router.push('/login/')
}
})
.catch(err => {
console.log(err)
});
}
}
}
}, [])
return (
<SessionContext.Provider value={{user, setUser}}>
{children}
</SessionContext.Provider>
)
}
export const getUserState = () => {
const { user } = useContext(SessionContext)
return user;
}
export const updateUserState = () => {
const { setUser } = useContext(SessionContext)
return (user) => {
setUser(user);
}
}
export default AppSession;
Since user.authenticated isn't defined in the initial user state you can conditionally render null or some loading indicator while user.authenticated is undefined. Once user.authenticated is defined the code should either redirect to "/login" or render the SessionContext.Provider component.
Example:
const AppSession = ({ children }) => {
const router = useRouter();
const routerPath = router.pathname;
const [user, setUser] = useState({ user_id: '', user_email: '', user_avatar: ''});
...
if (user.authenticated === undefined) {
return null; // or loading indicator/spinner/etc
}
return (
<SessionContext.Provider value={{ user, setUser }}>
{children}
</SessionContext.Provider>
);
};
Check out getServerSideProps, redirects in getServerSideProps and this article.
In your client-side, if you export the NextJS function definition named getServerSideProps from a page, NextJS pre-renders the page on each request using the data returned by getServerSideProps.
In other words, you can use getServerSideProps to retrieve and check the user while pre-rendering the page and then choose to redirect instead of render if your condition is not met.
Here is an example.
function Page({ data }) {
// Render data...
}
export async function getServerSideProps(context) {
const { req, res } = context;
try {
// get your user
if (user.authenticated === undefined) {
return {
redirect: {
permanent: false,
destination: `/`,
},
};
}
return {
props: {
// any static props you want to deliver to the component
},
};
} catch (e) {
console.error("uh oh");
return;
}
}
Good luck!

Uncaught TypeError: Cannot read properties of null (reading 'uid') : Get this error but application work fine?

I get this error upon logging out: (Uncaught TypeError: Cannot read properties of null (reading 'uid')), but my application works and does what I want it to do.
Within my logout function I am basically deleting an API access token from the user doc in the database when user logouts, because the token does not expire so it is added on login and removed on logout as part of authentication process. This all works fine.
so how do I get rid of this error, is there another way I can structure my code to get rid of this error?
It makes sense that this error appears once user has logged out because the uid is longer available to access due to user not being active (current user).
Many thanks, code below.
import { signOut } from "#firebase/auth";
import { useNavigate } from "react-router";
import { auth, db } from "../../firebase";
import { doc, updateDoc, deleteField } from "firebase/firestore";
export const Logout = () => {
const user = auth.currentUser;
const uid = user.uid;
console.log(uid);
const userRef = doc(db, 'users', uid);
const navigate = useNavigate();
const logoutUser = async () => {
//Deleting mavenlink access token
await updateDoc(userRef, {
accessToken: deleteField()
});
signOut(auth).then(() => {
navigate("/")
})
}
return {logoutUser}
};
For context below is code where authenticated routes are handled in app.js
function App() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [state, setState] = useState({});
const [user, loading, error] = useAuthState(auth);
//Hook to handle authentication
useEffect(() => {
if (loading) {
// maybe trigger a loading screen
return;
}
if (user && !isAuthenticated){
setIsAuthenticated(true);
console.log("logged in");
} else if (!user && isAuthenticated) {
setIsAuthenticated(false);
console.log("logged out");
}
}, [user, loading]);
const unauthenticatedRoutes = useRoutes([
{ path: "/", element: <LoginForm/> },
{ path: "/reset", element: <ResetForm/> },
{ path: "/register", element: <RegisterForm/> },
]);
return (
<AppContext.Provider value={{
isAuthenticated, setIsAuthenticated,
}}>
<div className="App">
{!isAuthenticated ? <>{unauthenticatedRoutes}</> : <Sidebar/>}
</div>
</AppContext.Provider>
);
}
export default App;
Note: the authenticated routes are defined in the sidebar component and rendered there.
You may add privateRoutes to resolve this issue.
What I mean by this is create a file where you specify the condition whether your component should be rendered, or it should redirect to signin page.
And you can convert your normal route to private route. wrap your element in private route in your normal route. When you make your signout function a private component it will redirect the user to signin page as now user is deleted and currentUser no longer exist.
import React from 'react';
import { Navigate} from 'react-router-dom';
//import user from firebase
export default function PrivateRoute({ children }) {
const { currentUser } = // user imported
return currentUser ? children : <Navigate to="/signin" />;
}

firebase snapshot not getting currentUser

i am trying to get current user in console but getting undefined in react-native. firebase 8.3 it is.
this is my firebase init
import firebase from "firebase";
// import { initializeApp } from "firebase/app";
const firebaseConfig = {
//api
};
// Initialize Firebase
const app = firebase.initializeApp(firebaseConfig);
export default firebase;
this is action code , which is same as docs in internet
firebase
.firestore()
.collection("user")
.doc(firebase.auth().currentUser.uid)
.get()
.then((snapshot) => {
if (snapshot.exists) {
console.log(snapshot.data());
dispatch({ type: USER_STATE_CHANGE, currentUser: snapshot.data() });
} else {
console.log("does not exist, console from action ");
}
and here is my redux store code, which is more doubtful in my knowledge
const store = createStore(Reducers, applyMiddleware(thunk))
return (
<Provider style={styles.center} store={store}>
<Main/>
</Provider>
);
and main.js
function Main(props) {
useEffect(() => {
props.fetchUser();
}, []);
if (props.currentUser == undefined) {
return (
<View>
<Text>No Data</Text>
</View>
);
} else {
return (
<View>
<Text>{props.currentUser.name} is logged in now !</Text>
</View>
);
}
}
const mapStateToProps = (state) => {
return {
currentUser: state.user.currentUser,
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchUser: () => dispatch(fetchUser()),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Main);
My guess (it is hard to be certain from the fragments of code you shared) is that the code that loads the user data from Firestore runs when the page/app loads, and that Firebase isn't done restoring the current user when it runs.
When the page/app loads, Firebase automatically restores the user credentials from local storage. This requires it to call the server though (amongst others to see if the account has been disabled), which may take some times. While this call is going on, your main code continues, and the value of firebase.auth().currentUser is null at this point.
So if you don't synchronize your code that loads the user profile to Firebase''s restore actions, you will end up loading the data too early, when the user hasn't been re-signed yet.
The solution is to listen for auth state changes, and respond to those, instead of assuming that firebase.auth().currentUser is always correct. For an example of how to do this, see the first code snippet in the Firebase documentation on getting the current user:
firebase.auth().onAuthStateChanged((user) => {
if (user) {
// User is signed in, see docs for a list of available properties
// https://firebase.google.com/docs/reference/js/firebase.User
var uid = user.uid;
// 🤞 This is where you can load the user profile from Firestore
// ...
} else {
// User is signed out
// ...
}
});

Email verification with Next js and express

I am building a web application in which i need to verify the user's email sent via the client side (React.js and Next.js) and i'm following this youtube tutorial. However, the mentor is using create-react-app CLI and React-Router-Dom for the routing system which doesn't really go with my current needs.
Moreover, I found this method online using HOC :
import React from 'react';
import Router from 'next/router';
const login = '/register?redirected=true'; // Define your login route address.
const checkUserAuthentication = () => {
return { auth: null }; // change null to { isAdmin: true } for test it.
};
export default WrappedComponent => {
const hocComponent = ({ ...props }) => <WrappedComponent {...props} />;
hocComponent.getInitialProps = async (context) => {
const userAuth = await checkUserAuthentication();
// Are you an authorized user or not?
if (!userAuth?.auth) {
// Handle server-side and client-side rendering.
if (context.res) {
context.res?.writeHead(302, {
Location: login,
});
context.res?.end();
} else {
Router.replace(login);
}
} else if (WrappedComponent.getInitialProps) {
const wrappedProps = await WrappedComponent.getInitialProps({...context, auth: userAuth});
return { ...wrappedProps, userAuth };
}
return { userAuth };
};
return hocComponent;
};
The code above helps me to have a private route that the user cannot access unless he's authenticated (currently no programming included), but on the other hand i still need a page in the following route :
'pages/user/activate/[token].js' // the link sent via email from express back end.
What i need now is to create this page using Next routing system in order to get the token and decode it to move forward with the back end and save the user into MongoDB, and in order to accomplish that, i have created my [token].js page with the following code :
import React, {useState, useEffect} from 'react'
import { ToastContainer, toast } from 'react-toastify';
import axios from 'axios';
import jwt from 'jsonwebtoken';
import { authenticate, isAuth } from '../helpers/auth';
import { Link, Redirect } from 'react-router-dom';
const Activate = ({ match }) => {
const [formData, setFormData] = useState({
email: '',
token: '',
show: true
});
const { email, token, show } = formData;
useEffect(() => {
let token = match.params.token;
let { email } = jwt.decode(token);
if (token) {
setFormData({ ...formData, email, token });
}
console.log(token, email);
}, [match.params.token]);
return (
<>
{isAuth() ? <Redirect to="/" /> : null}
<p>Account activated, please log in</p>
</>
)
};
export default Activate;
However, i keep getting this error :
TypeError: Cannot read property 'params' of undefined
at Activate (C:\Users\Hp\Desktop\SMP\client\.next\server\pages\user\activate\[token].js:245:13)
at processChild (C:\Users\Hp\Desktop\SMP\client\node_modules\react-dom\cjs\react-dom-
server.node.development.js:3353:14)
at resolve (C:\Users\Hp\Desktop\SMP\client\node_modules\react-dom\cjs\react-dom-
server.node.development.js:3270:5)
at ReactDOMServerRenderer.render (C:\Users\Hp\Desktop\SMP\client\node_modules\react-dom\cjs\react-
dom-server.node.development.js:3753:22)
at ReactDOMServerRenderer.read (C:\Users\Hp\Desktop\SMP\client\node_modules\react-dom\cjs\react-dom-
server.node.development.js:3690:29)
at renderToString (C:\Users\Hp\Desktop\SMP\client\node_modules\react-dom\cjs\react-dom-
server.node.development.js:4298:27)
at Object.renderPage (C:\Users\Hp\Desktop\SMP\client\node_modules\next\dist\next-
server\server\render.js:53:851)
at Function.getInitialProps (C:\Users\Hp\Desktop\SMP\client\.next\server\pages\_document.js:293:19)
at loadGetInitialProps (C:\Users\Hp\Desktop\SMP\client\node_modules\next\dist\next-
server\lib\utils.js:5:101)
at renderToHTML (C:\Users\Hp\Desktop\SMP\client\node_modules\next\dist\next-
server\server\render.js:53:1142)
I couldn't find a solution because i believe that i'm doing something wrong whether in my code or in the logic implemented.
Is there any way that i can do this properly ?
Thank you in advance !

How to treat global user/user data in react?

I'm building a ReactJS project and I'm using something like this, to provide user data trough the app:
function Comp1() {
const [user, setUser] = useState({});
firebase.auth().onAuthStateChanged(function (_user) {
if (_user) {
// User is signed in.
// do some firestroe queryes to get all the user's data
setUser(_user);
} else {
setUser({ exists: false });
}
});
return (
<div>
<UserProvider.Provider value={{user, setUser}}>
<Comp2 />
<Comp3 />
</UserProvider.Provider>
</div>
);
}
function Comp2(props) {
const { user, setUser } = useContext(UserProvider);
return (
<div>
{user.exists}
</div>
)
}
function Comp3(props) {
const { user, setUser } = useContext(UserProvider);
return (
<div>
{user.exists}
</div>
)
}
//User Provider
import React from 'react';
const UserProvider = React.createContext();
export default UserProvider;
So, in this case, Comp1 provides user data to Comp2 & Comp3. The only problem is that when the user state changes or the page loads, it creates an infinite loop. If I'm not using an useState for storing the user data, then when it changes, the components do not get re-rendered. I also tried to do something like this in the index.js file:
firebase.auth().onAuthStateChanged(function (user) {
if (user) {
ReactDOM.render(<Comp1 user={user} />, document.getElementById('root'));
} else {
ReactDOM.render(<Comp1 user={{exists: false}} />, document.getElementById('root'));
}
});
But this worked a bit weirdly sometimes, and it's kinda messy. What solutions are there? Thanks.
Edit: I'm triening to do it in the wrong way? How should I provide all user data with only one firebase query?
I suggest using some state container for the application to easily manipulate with a user. The most common solution is to use Redux. With redux, you will be able to have a global state of your app. Generally, all user data stored in it. https://redux.js.org/
The other solution is to use MobX with simple store access. It doesn't use Flux pattern if you don't like it.
If you don't want to use a global store you can use HOCs to propagate your user data. Finally, you can use Context to React, but it is bad approach.
Let's, for example, choose the most popular representer of Flux architecture - Redux.
The first layer is the View layer. Here we will dispatch some action to change global, e.g user data.
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { logIn, logOut } from 'actions'
export default class Page extends React.Component {
useEffect(() => {
firebase.auth().onAuthStateChanged((user) => {
logIn(user)
} else {
logOut()
})
}, [])
render () {
...
}
}
const mapDispatchToProps = dispatch => bindActionCreators({
logIn,
logOut
}, dispatch)
export default connect(null, mapDispatchToProps)(App)
The second layer are actions. Here we work we with our data, working with api, format state and so on. The main goal of actions is to create data to pass it to the reducer.
actions.js
export const logIn = user => dispatch => {
// dispatch action to change global state
dispatch({
type: 'LOG_IN',
payload: user
})
}
export const logOut = user => dispatch => {
dispatch({ type: 'LOG_OUT' })
}
The last thing is the reducers. The only goal of them is to change state. We subscribe here for some actions. Reducer always should be a pure function. State shouldn't be mutated, only overwritten.
appReducer.js
const initialState = {
user: null
}
export default function appReducer (state = initialState, action) {
const { type, payload } = action
switch(type) {
case 'LOG_IN':
return {
...state,
user: payload
}
case: 'LOG_OUT':
return {
...state,
user: null
}
}
}
Then, we can work with the global app state whenever we want.
To do it, we should use react-redux library and Provider HOC
const App = () =>
<Provider store={store}>
<Navigation />
</Provider>
Now, we can have access to any stores inside any component though react-redux connect HOF. It works with React Context API inside of it.
const Page2 = ({ user }) => {
//... manipulate with user
}
// this function gets all stores that you have in the Provider.
const mapStateToProps = (state) => {
user: state.user
}
export default connect(mapStateToProps)(Page2)
By the way, you should choose middleware to work with async code in redux. The most popular that is used in my example is redux-thunk.
More information you can find in the official documentation.
There you can find information about how to make initial store configuration
In Comp1, a new onAuthStateChanged observer is added to firebase.Auth on every render.
Put that statement in a useEffect like:
useEffect(() => {
firebase.auth().onAuthStateChanged(function (_user) {
if (_user) {
// User is signed in.
// do some firestroe queryes to get all the user's data
setUser(_user);
} else {
setUser({ exists: false });
}
});
}, []);
Issue:-
The issue for the loop to happen was due to the way Comp1 was written.
Any statement written within the Comp1 functional component will get executed after ever change in prop or state. So in this case whenever setUser was called Comp1 gets re-render and again it subscribes to the auth change listener which again executes setUser on receiving the data.
function Comp1() {
const [user, setUser] = useState({});
firebase.auth().onAuthStateChanged(function (_user) {
if (_user) {
// User is signed in.
// do some firestroe queryes to get all the user's data
setUser(_user);
} else {
setUser({ exists: false });
}
});
return (
<div>
<UserProvider.Provider value={{user, setUser}}>
<Comp2 />
<Comp3 />
</UserProvider.Provider>
</div>
);
}
Solution:-
You can use useEffect to make statements execute on componentDidMount, componentDidUdate and componentWillUnmount react's life cycles.
// [] is passed as 2 args so that this effect will run once only on mount and unmount.
useEffect(()=> {
const unsubscribe = firebase.auth().onAuthStateChanged(function (_user) {
if (_user) {
// User is signed in.
// do some firestroe queryes to get all the user's data
setUser(_user);
} else {
setUser({ exists: false });
}
});
// this method gets executed on component unmount.
return () => {
unsubscribe();
}
}, []);
I created a replica for the above case, you can check it running here
Take a look at this logic:
function Comp1() {
const [user, setUser] = useState({});
firebase.auth().onAuthStateChanged(function (_user) {
if (_user) {
// User is signed in.
// do some firestroe queryes to get all the user's data
setUser(_user);
} else {
setUser({ exists: false });
}
});
return (
<div>
<UserProvider.Provider value={{user, setUser}}>
<Comp2 />
<Comp3 />
</UserProvider.Provider>
</div>
);
}
You are calling setUser in any case. Instead, you should check whether user is already set and if so, whether it matches _user. Set user to _user if and only if _user differs from user. The way it goes, setUser is triggered, which triggers Comp2 and Comp3 change, which triggers the event above which triggers setUser.

Categories