I can't access a private route using firebase - reactjs

I am creating a private route to be accessed only when the firebase listener verifies that the user is logged in, but I cannot access this route the way I am doing.
const PrivateRoute = ({ component: Component, ...rest }) => {
let autenticado = false;
firebase.auth().onAuthStateChanged((user) => {
if (user) {
autenticado = true;
} else {
autenticado = false;
}
});
return (
<Route
{...rest}
render={(props) =>
autenticado ? (
<Component {...props} />
) : (
<Redirect to={{ pathname: "/", state: { from: props.location } }} />
)
}
/>
);
};
const Routes = () => (
<BrowserRouter>
<Switch>
<Route path="/" exact component={Login} />
<PrivateRoute path="/home" component={Home} />
</Switch>
</BrowserRouter>
);
export default Routes;

Alvaro's solution is good, but Robert Terrell brought up a good point:
if you refresh your app, the private route kicks you back to the login page because it takes a few moments for the currentUser to re register
.
To fix this, id wait for the authcontext to load before displaying the site on line 24 in AuthContext.tsx:
import React, { useEffect, useState } from "react";
import {auth, firebase} from "../firebaseSetup";
type ContextProps = {
user: firebase.User | null;
};
export const AuthContext = React.createContext<Partial<ContextProps>>({});
export const AuthProvider = ({ children }: any) => {
const [user, setUser] = useState(null as firebase.User | null);
const [loading, setLoading] = useState(true);
useEffect(() => {
auth.onAuthStateChanged((user: any) => {
setUser(user);
setLoading(false);
});
}, []);
return (
<AuthContext.Provider
value={{user}}>
{!loading && children}
</AuthContext.Provider>
);
}

It's hard to tell where your code is going wrong without seeing other components. I do basically the same approach when I set up firebase auth and set up private routes. I bring my context into my PrivateRoute component like so:
import React, { useContext } from "react";
import { Route, Redirect } from "react-router-dom";
import { AuthContext } from "./Auth";
const PrivateRoute = ({ component: RouteComponent, ...rest }) => {
const {currentUser} = useContext(AuthContext);
return (
<Route
{...rest}
render={routeProps =>
!!currentUser ? (
<RouteComponent {...routeProps} />
) : (
<Redirect to={"/admin/login"} />
)
}
/>
);
};
My Auth.js file is where I create my context like so:
import React, { useEffect, useState } from "react";
import app from "./base.js";
export const AuthContext = React.createContext();
export const AuthProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null);
useEffect(() => {
app.auth().onAuthStateChanged(setCurrentUser);
}, []);
return (
<AuthContext.Provider
value={{
currentUser
}}
>
{children}
</AuthContext.Provider>
);
};
I initiate my firebase instance in a file called base.js that looks like so :
import * as firebase from "firebase/app";
import "firebase/auth";
console.log(process.env.REACT_APP_FIREBASE_DATABASE)
const app = firebase.initializeApp({
apiKey: process.env.REACT_APP_FIREBASE_KEY,
authDomain: process.env.REACT_APP_FIREBASE_DOMAIN,
databaseURL: process.env.REACT_APP_FIREBASE_DATABASE,
projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.REACT_APP_FIREBASE_SENDER_ID
});
export default app;
In your routes you would then add the AuthProvider
const Routes = () => (
<BrowserRouter>
<Switch>
<AuthProvider>
<Route path="/" exact component={Login} />
<PrivateRoute path="/home" component={Home} />
</AuthProvider>
</Switch>
</BrowserRouter>
);
export default Routes;

Related

Is there any sense to useContext in React?

Hello i need help with my app.
What sense is to put my User data to useContext if when i refresh page all data disapear?
How to make this context permament?
I have been sitting with this all day, and when i try to get data in HomePage, everything is ok until i refresh page.
The second question is about JWT. It's used only on server side, right? It's verifing the token when I'm making server reqest only?
below is my code
App.js
import Navbar from "./components/Navbar";
import AddNewAnnoucement from "./components/pages/AddNewAnnoucement/AddNewAnnoucement";
import { Route, Routes } from 'react-router-dom';
import Home from "./components/pages/Home";
import Annoucements from "./components/pages/Annoucements/Annoucements";
import Register from "./components/pages/Register/Register";
import Login from "./components/pages/Login/Login";
import { useState, useMemo, useEffect, createContext } from "react";
import { getProfile, tokenIsValid } from './service/userService.js'
export const UserContext = createContext();
function App() {
const [userData, setUserData] = useState({
token: undefined,
user: undefined,
})
useEffect(() => {
const isLoggedIn = async () => {
let token = localStorage.getItem("token")
if (token == null) {
localStorage.setItem("token", "")
token = ""
}
const tokenResponse = tokenIsValid(token);
tokenResponse.then((res) => {
if (res.data) {
const userResponse = getProfile(token);
userResponse.then((res) => {
setUserData({
token: token,
data: res.data
})
})
}
}).catch((err) => {
console.log(err);
}
)
}
isLoggedIn();
}, [])
return <>
<UserContext.Provider value={{ userData, setUserData }}>
<Navbar />
<div className='container'>
<Routes>
<Route path='/' element={<Home />} />
<Route path='/annoucements' element={<Annoucements />} />
<Route path='/annoucements/add' element={<AddNewAnnoucement />} />
<Route path='/register' element={<Register />} />
<Route path='/login' element={<Login />} />
</Routes>
</div>
</UserContext.Provider>
</>
}
export default App;

Login redirect rendering twice

I'm using reach router for my routes. I was able to protect the dashboard and expose the login page and redirect if the user is not logged in but now if I enter a url it will do a quick redirecto to login and then to home instead of the page actually entered.
I noticed because of the useEffect to fetch the user the component renders twice: 1 without user (redirects to login) the other one with user and redirects to home.
Routes file
const AdminRoutes = () => {
return (
<Router>
<MainLayout path="/admin">
<HomePage path="/" />
<CarTransfer path="/cartransfers">
<CarTranserList path="/list" />
<CarTransferCreate path="/new" />
</CarTransfer>
<User path="/users">
<UserList path="/list" />
<UserCreate path="/new" />
</User>
</MainLayout>
<LoginPage path="/login" />
</Router>
);
};
Layout file
import { useState, useEffect } from "react";
import { Redirect, useNavigate } from "#reach/router";
import { Layout } from "antd";
import SiderMenu from "./SiderMenu";
import LayoutBanner from "./LayoutBanner";
import { useSelector, useDispatch } from "react-redux";
import {
userSelector,
fetchUserBytoken,
clearState,
} from "../../features/authSlice";
const { Content } = Layout;
const MainLayout = ({ children }) => {
const user = useSelector(userSelector);
const [collapsed, setCollapsed] = useState(false);
const navigate = useNavigate();
const dispatch = useDispatch();
const { isFetching, isError } = useSelector(userSelector);
useEffect(() => {
dispatch(
fetchUserBytoken({
token: localStorage.getItem("token"),
id: localStorage.getItem("id"),
})
);
}, []);
useEffect(() => {
if (isError) {
dispatch(clearState());
navigate("/login");
}
}, [isError]);
const handleOnCollapse = () => {
setCollapsed((prevState) => !prevState);
};
if (isFetching) {
return <div>Loading</div>;
}
if (user.id === "") {
return <Redirect noThrow to="/login" />;
}
return (
<Layout>
<SiderMenu collapsed={collapsed} handleOnCollapse={handleOnCollapse} />
<Layout>
<LayoutBanner
collapsed={collapsed}
handleOnCollapse={handleOnCollapse}
/>
<Content>{children}</Content>
</Layout>
</Layout>
);
};
export default MainLayout;
The second question would be how to get to the same page you were before the login redirect after login.
Thanks

React PrivateRoute is caught in a Route loop

I have a PrivateRoute component that protects any route requiring a valid login in the app. I have a route called Spec.tsx that is called from App.tsx. PrivateRoute will spit you out on the Login page if you're not logged in, which is great. And Login will send you directly to the Home page if you are logged in. Also great.
Right now, I'm caught in a loop where when I go to Spec, the app thinks I'm not logged in, and sends me to Login, which does think I'm logged in, and so sends me back to Home. currentUser is always calculated the same was, like const {currentUser} = useContext(AuthContext);
When I log the currentUser in PrivateRoute as I navigate to /spec/:id it says null once, and then gives the correct answer 3 more times. I suspect the null causes me to get booted to Login and then currentUser must be assigned correctly as I'm sent back to Home immediately. I don't even ever get to Spec.tsx, nothing I try to log there gets logged. Can anyone point out what I'm doing wrong? Thanks
//In App.tsx
<AuthProvider>
<Router history={history}>
<Navbar />
<Switch>
<PrivateRoute exact path="/" component={Home} />
<PrivateRoute exact path="/spec/:id" render={() => (
<Spec isEdit={true}/>
)}/>
<Route exact path="/login" component={Login} />
<Route exact path="/signup" component={Signup} />
</Switch>
</Router>
</AuthProvider>
//PrivateRoute.tsx
import React, { useContext } from "react";
import { Route, Redirect } from "react-router-dom";
import { AuthContext } from "../Util/Auth";
const PrivateRoute = ({ component: RouteComponent, ...rest }: any) => {
const {currentUser} = useContext(AuthContext);
console.log(currentUser)
return (
<Route
{...rest}
render={(routeProps: any) =>
!!currentUser ? (
<RouteComponent {...routeProps} />
) : (
<Redirect to={"/login"} />
)
}
/>
);
};
export default PrivateRoute
//Login.tsx
const Login = ({ history }: any) => {
const { currentUser } = useContext(AuthContext);
if (currentUser) {
return <Redirect to="/" />;
}
return (
//My JSX
)
My Auth provider is below, which handles all changes to currentUser
//Auth.tsx
import React, { useEffect, useState } from "react";
import { createNewUser } from "../Models/User";
import app from "./firebase";
const initialUser: any = null;
export const AuthContext = React.createContext(initialUser);
export const AuthProvider: React.FC = ({ children }) => {
const [currentUser, setCurrentUser] = useState<any>(null);
const [currentDBUser, setCurrentDBUser] = useState<any>(null);
const [pending, setPending] = useState<boolean>(true);
const [pending2, setPending2] = useState<boolean>(true);
useEffect(() => {
//Auth user
app.auth().onAuthStateChanged((user) => {
setPending(false)
setCurrentUser(user);
});
//Grab database data for user
async function fetchData() {
if (currentUser && !currentDBUser && pending2) {
const newUser: User = {fbUser: currentUser.uid, email: currentUser.email}
const userDBObject = await createNewUser(newUser);
if (userDBObject) {
setCurrentDBUser(userDBObject);
setPending2(false);
}
}
}
if (pending2) {
fetchData();
}
}, [currentUser, currentDBUser, pending2]);
if (pending) {
return <>Loading...</>
}
return (
<AuthContext.Provider value= {{ currentUser, currentDBUser }}>
{ children }
</AuthContext.Provider>
);
};

React context undefined when refreshing page

I want to create a protected route that only authenticated users can access. It works BUT when im refreshing the page the context is undefined and redirects the user to the landing page. I dont really see why this is happening.
Console log after refreshing page
App.js
import React, { useState, useEffect } from "react";
import { BrowserRouter, Switch, Route } from "react-router-dom";
import Axios from "axios";
import Home from "./components/pages/Home";
import LandingPage from "./components/pages/LandingPage";
import { ProtectedRoute } from "./components/auth/ProtectedRoute";
import UserContext from "./context/UserContext";
import "./style.css";
export default function App() {
const [userData, setUserData] = useState({
token: undefined,
user: undefined,
});
const checkLoggedIn = async () => {
let token = localStorage.getItem("auth-token");
if (token === null) {
localStorage.setItem("auth-token", "");
token = "";
}
const tokenRes = await Axios.post(
"http://localhost:5000/users/tokenIsValid",
null,
{ headers: { "x-auth-token": token } }
);
if (tokenRes.data) {
const userRes = await Axios.get("http://localhost:5000/users/", {
headers: { "x-auth-token": token },
});
setUserData({
token,
user: userRes.data,
});
}
};
useEffect(() => {
console.log('useEffect called');
checkLoggedIn();
}, []);
return (
<>
<BrowserRouter>
<UserContext.Provider value={{ userData, setUserData }}>
<Switch>
<Route exact path="/" component={LandingPage} />
<ProtectedRoute exact path="/home" component={Home} />
</Switch>
</UserContext.Provider>
</BrowserRouter>
</>
);
}
ProtectedRoute.js
import React, { useContext } from "react";
import UserContext from "../../context/UserContext";
import { Route, Redirect } from "react-router-dom";
export const ProtectedRoute = ({
component: Component,
...rest
}) => {
const { userData } = useContext(UserContext);
return (
<Route
{...rest}
render={props => {
console.log("USERDATA " + userData.user)
console.log("TOKEN " + localStorage.getItem("auth-token"))
if (userData.user) {
return <Component {...props} />;
} else {
return (
<Redirect
to={{
pathname: "/",
state: {
from: props.location
}
}}
/>
);
}
}}
/>
);
};
EDIT
After a while i figured out the problem, which is explained in this question REACT - Check authentication before render APP
After a while i figured out the problem, which is explained in this question REACT - Check authentication before render APP
This is my solution, which works fine.
App.js
export default function App() {
const [userData, setUserData] = useState({
isLoggedIn: false,
isLoading: true,
token: undefined,
user: undefined
});
const checkLoggedIn = async () => {
const tokenRes = await validateToken();
if (tokenRes.data) {
const userRes = await getUser();
let token = localStorage.getItem("auth-token");
setUserData({
token,
user: userRes.data,
isLoggedIn: true,
isLoading: false
});
}
};
useEffect(() => {
console.log('useEffect called');
checkLoggedIn();
}, []);
console.log("APPPPPP")
return (
<>
<BrowserRouter>
<UserContext.Provider value={{ userData, setUserData }}>
<Switch>
<Route exact path="/" component={LandingPage} />
<ProtectedRoute exact path="/home" component={Home} />
</Switch>
</UserContext.Provider>
</BrowserRouter>
</>
);
}
ProtectedRoute.js
import React, {useContext} from "react";
import { Route, Redirect } from "react-router-dom";
import UserContext from "../../context/UserContext";
const ProtectedRoute = ({ component: Comp, path, ...rest }) => {
const { userData } = useContext(UserContext);
console.log(userData.isLoggedIn);
return (
<Route path={path} {...rest}
render={props => {
return userData.isLoggedIn ? (<Comp {...props} />) : (userData.isLoading ? 'Loading...' : <Redirect to="/" />)
}}
/>
);
};
export default ProtectedRoute;

Firebase onAuthStateChanged with React Context Not Updating App

The problem I'm running into is that currentUser in App.js is null when <PrivateRoute /> renders, which will cause it to render a <Redirect /> and navigate to /login.
However, if I view the React Dev Tools I can see that the value field in Context.Provider does correctly have a user object set.This prop should rerender the rest of the app but it does not.
Why doesn't currentUser updating update the routes?
Auth.js
import React, { useEffect, useState } from 'react';
import firebase from 'firebase';
export const AuthContext = React.createContext();
export const AuthProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null);
useEffect(() => {
firebase.auth().onAuthStateChanged(setCurrentUser);
}, []);
return (
<AuthContext.Provider value={{ currentUser }}>
{children}
</AuthContext.Provider>
);
};
App.js
import { AuthProvider } from './Auth';
import { AuthContext } from './Auth';
function App() {
const PrivateRoute = ({ component: Component, ...rest }) => {
const { currentUser } = useContext(AuthContext);
return (
<Route
{...rest}
render={props =>
currentUser ? (
<Component {...props} {...rest} />
) : (
<Redirect to={{ pathname: '/login' }} />
)
}
/>
);
};
return (
<AuthProvider>
<Router history={createBrowserHistory()}>
<Switch>
<PrivateRoute path="/" exact component={HomeScreen} />
<Route path="/login" exact component={LogIn} />
</Switch>
</Router>
</AuthProvider>
);
}

Resources