Pass object to child then access object methods - reactjs

So I have a file routes.jsx which handles the routing of my application. Following the docs for react-router-dom, I wanted to start working on login/logout.
For now, I have implemented a fakeAuth object which will authenticate against the backend and set an isAuthenticated var. However, I think I need to be able to pass this auth around to different components, such as the Login.jsx to actually change the state of isAuthenticated, and to let's say a NavBar.jsx so I can change a login button to a logout button (to clarify, all my components are functional components (trying) using hooks and do not extend React.Component in a class manner).
However, how do I pass the fakeAuth as a prop and still have access to the methods inside? If I declare fakeAuth inside the Login.jsx component, fakeAuth is defined. If I pass it in as a prop, it is considered not defined.
routes.js
export default (
<BrowserRouter>
<Route path='/'>
<LandingPage fakeAuth={fakeAuth}/> // Landing page has the NavBar component as a child.
</Route>
<Switch>
<Route path="/login">
<Login fakeAuth={fakeAuth}/> // I want the Login component to have access to fakeAuth.whatever
</Route>
<PrivateRoute path="/home">
<UserApp/>
</PrivateRoute>
</Switch>
</BrowserRouter>
);
const fakeAuth = {
isAuthenticated: false,
authenticate(cb) {
fakeAuth.isAuthenticated = true;
setTimeout(cb, 100); // fake async
},
signout(cb) {
fakeAuth.isAuthenticated = false;
setTimeout(cb, 100);
}
};
Login.jsx
const Login = ({fakeAuth}) => {
let history = useHistory();
let location = useLocation();
let { from } = location.state || { from: { pathname: "/home" } };
let login = () => {
fakeAuth.authenticate(() => { // Need to access authenticate.
history.replace(from);
});
};
return (
// login modal etc
<button onClick={login}>Log in</button>
);
}
The above code is the code that returns fakeAuth is undefined.
Any advice would be great!

Related

Following a 301 Redirect from an API in a React App

I've written a custom ProtectedRoute component for my React app that redirects a user to a /api/login route on my Express API if that user is not authenticated. The /api/login route returns a 301 Redirect to the Auth0 Universal Login UI.
I know the /api/login route on the Express API works because I can hit it directly I get redirected to the Auth0 Universal Login (see the last code snippet).
I also know the ProtectedRoute is redirecting correctly because it redirects to localhost:3000/api/login, which is the correct route on the Express API to trigger the Auth0 Universal Login redirect.
What actually happens though is that localhost:3000/api/login shows up in the address bar but the redirect to the Auth0 Universal Login doesn't happen. That being said if I refresh the page then the redirect to the Universal Login UI works.
I'm not exactly sure why the Redirect returned from /api/login isn't followed in the Browser. I think it has something to do with how React is navigating to the route.
Here's the relevant code snippets. If more are needed let me know.
Protected Route Component
import { Navigate, useLocation } from 'react-router-dom';
interface ISession {
userId: string;
role: string;
details: any;
}
type RouteProps = {
children?: JSX.Element;
session: ISession;
loading: boolean;
};
const ProtectedRoute = ({ session, children, loading }: RouteProps) => {
const location = useLocation();
if (loading) return null;
else if (!!session.userId) {
return children ? children : <Outlet />;
}
else {
return <Navigate replace to="/api/login" state={{ redirectTo: location.pathname }} />;
}
};
export default ProtectedRoute;
How the ProtectedRoute Component is used with React Router
import { useContext } from 'react';
import { Route, Routes } from 'react-router-dom';
import Home from './pages/home';
import 'bootstrap/dist/js/bootstrap.bundle.min';
import { SessionContext } from './context/SessionContext';
import ProtectedRoute from './middleware/protectedRoute';
const App = () => {
const { session, loading } = useContext(SessionContext);
console.log('Session', session);
return (
<Routes>
<Route element={<ProtectedRoute loading={loading} session={session} />}>
<Route path="/" element={<Home />} />
<Route path="/dne" element={ <p>Stuff</p> } />
</Route>
</Routes>
);
};
export default App;
NOTE: I'm excluding the code from the SessionContext component for brevity. Since the user isn't able to login because the redirect to the Auth0 Universal Login UI doesn't work no session is ever created.
The "/api/login" route handler on the Express API
const login = (req: Request, res: Response) => {
const { redirectTo } = req.query;
const domain = config.get('auth0.domain');
const clientId = config.get('auth0.clientId');
const host = config.get('host');
// These 301 Responses are the Redirect to Auth0's Universal Login UI
if (redirectTo) {
const encodedRedirect = base64.urlEncode(redirectTo as string); // A Custom Base64 encoder that is URL Safe
res.status(301).redirect(`${domain}/auth/authorize?response_type=code&scope=openid&client_id=${clientId}&redirect_uri=${host}/api/auth/callback&state=${encodedRedirect}`)
} else {
res.status(301).redirect(`${domain}/auth/authorize?response_type=code&scope=openid&client_id=${clientId}&redirect_uri=${host}/api/auth/callback`);
}
};
It's not really an answer for what is going on but it is a solution.
The problem was <Navigate to="/api/login" /> would cause React to Rerender the page and would change the URL in the address bar, but it would not cause the browser to make a GET request to the new address.
To solve this I just overwrote the window.location.href with /api/login in the ProtectedRoute component.
Here's the new version of the ProtectedRoute component.
const ProtectedRoute = ({ session, children, loading }: RouteProps) => {
const location = useLocation();
if (loading)
return null;
if (!!session.userId) {
return children ? children : <Outlet />;
}
else {
window.location.href = '/api/login';
return <Navigate replace={true} to='/api/login' />
}
};

React keep a state in the highest component

I am trying to create a React RBAC system where my backend has a field called role: admin for example which tells the access the user has. After a successful sign in, I direct the user to a specific route (using Protected Route) but I want to check that if the user has the clearance level (if role is admin and not general). I thought that if I keep a state where I am routing which stores the role of the user, I can check if the user has the required access and send him accordingly but I am not sure whether this is a good approach and how to do it.
App.js - RequireAuth just checks if the user session exists or not (it then redirects it to login)
class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<Router>
<Switch>
<Route exact path = '/'
component = {LandingPage}
/>
<Route exact path = '/register'
component = {Register}
/>
<Route exact path = '/addBill'
component = {RequireAuth(AddBill)}
/>
<Route exact path = '/addItem'
component = {RequireAuth(AddItem)}
/>
<Route exact path = '/deleteItem'
component = {RequireAuth(DeleteItem)}
/>
</Switch>
</Router>
</div>
);
}
}
export default withRouter(App);
SignIn.js (I just route the user to the endpoint if it is a successful login or else display an error message)
if(status === 200) {
this.props.history.push('/addItem')
}
RequireAuth does not have access the role of the user but I wanted to implement RBAC on this.
Add your permission data eg : Roles to global App State/Store, you could do this easily with React context API.
//these will probably go in a file AppProvider.js
const AppContext = React.createContext({userRole:'general', setUserRole: ()=>{}});
const AppProvider = ({children}) => {
const [userRole,setUserRole] = React.useState('general');
return <AppContext.Provider value={{ userRole, setUserRole }}>{children}</AppContext.Provider>
}
//end AppProvider.js
const RequireAuth = (component) => {
const {userRole} = React.useContext(AppProvider);
const Component = () => {
//check your RBAC logic here now that you have access to userRole
}
return Component;
}

Testing navigation in React component

I'd like to test that the url changes, when a submit button is pressed. As part of the test, I'm checking that the initial url is "/auth" and the url becomes "/".
A simpler test is failing, though, with the initial url test.
Test:
it("displays an authcode and submit button", async() => {
history = createMemoryHistory();
const root = document.createElement('div');
document.body.appendChild(root);
render(
<MemoryRouter initialEntries={["/auth"]}>
<App />
</MemoryRouter>,
root
);
expect(screen.queryByTestId('bad-code-message').classList.contains('hidden')).toBe(true);
expect(screen.getByLabelText('Auth code:')).toBeVisible();
expect(screen.getByRole('button')).toBeVisible();
expect(location.pathname).toBe("/auth");
});
App component:
import React from "react";
import { Route } from "react-router-dom";
import { ProtectedRoute } from './ProtectedRoute';
import { CreateProfileWithRouter } from './CreateProfileComponent';
import { ActivityList } from './ActivityListComponent';
import { TokenEntryWithRouter } from './TokenEntryComponent';
export class App extends React.Component {
render() {
return (
<div>
<ProtectedRoute exact path="/" component={ActivityList} />
<Route path="/login" component={CreateProfileWithRouter} />
<Route path="/auth" component={TokenEntryWithRouter} />
</div>
);
}
}
Result:
expect(received).toBe(expected) // Object.is equality
Expected: "/auth"
Received: "/"
After some more trial and error, I figured something out. "/" is the initial url, but I don't know how to change that. I'm passing the url that the component will navigate to and asserting that "/" is the url, at the beginning, and, when navigation is tested, I assert the url has changed to the passed in url.
I'm also using Router instead of MemoryRouter. I had a hunch from the docs that the history prop, which is passed into the component (with "withRouter"), gets changed in a way that could be tested.
Before all tests:
beforeEach(() => {
jest.resetAllMocks();
createPermanentAuthSpy = jest.spyOn(yasClient, "createPermanentAuth");
history = createMemoryHistory();
const root = document.createElement('div');
document.body.appendChild(root);
render(
<Router history={history}>
<TokenEntryWithRouter navigateToOnAuthentication="/dummy" />
</Router>,
root
);
token = screen.getByLabelText('Auth code:');
expect(screen.queryByTestId('bad-code-message').classList.contains('hidden')).toBe(true);
expect(history.location.pathname).toBe("/");
});
Testing navigation:
it("navigates to '/', when a good token is entered.", async() => {
createPermanentAuthSpy.mockImplementationOnce(() => Promise.resolve(true));
await act(async() => {
fireEvent.change(token, { target: { value: '1' } });
fireEvent.submit(screen.getByTestId('create-permanent-auth'));
});
expect(createPermanentAuthSpy).toHaveBeenCalledTimes(1);
expect(token.classList.contains('valid-data')).toBe(true);
expect(screen.queryByTestId('bad-code-message').classList.contains('hidden')).toBe(true);
expect(history.location.pathname).toBe("/dummy");
});

How to maintain the authenticated user state on page refresh with react

I have some protected routers but I need to maintain the state on page refresh.
import React from 'react'
import { Route, Redirect, Switch } from 'react-router-dom'
const auth = {
isAuthenticated: true // this would be an http call to get user data
}
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={(props) => (
auth.isAuthenticated === true
? <Component {...props} />
: <Redirect to="/login" />
)} />
)
const Main = () => {
return (
<Switch>
<Route path="/login" exact strict component={Login}/>
<Route path="/logout" exact strict component={Logout}/>
<PrivateRoute path="/profile" exact strict component={Profile}/>
</Switch>
)
}
export default Main
where should I make the service call? in the Main app? in a context?
update: I added a check in Main that makes a call to the api sending the token that I have stored. that call could return 200 with data or 401
const auth = {
isAuthenticated: Api.status() // this is just a fetch to /status endpoint
.then(
status => {
console.log(status);
return true;
},
error => {
console.log(error);
return false;
}
)
}
but when i hit /profile it redirects me immediately to login (because isAuthenticated is false)
my question is based entirely on the scenario where the user refresh the page (F5) other scenarios are working fine. sorry If I´m not clear enough happy to clarify anything thanks again!
the best way in my opinion is the local storage save the token in there dispatch an action in componentDidMount() in the root component of you app and store the token in readucer state
You can implement an auth-context like shown here: https://usehooks.com/useAuth/
And in addition store user in localStorage (or storage of your preference) and when the auth-context mounts read from localStorage and set the initial value like so:
const [user, setUser] = useState(JSON.parse(localStorage.getItem('user')));
This way the protected routes will automatically redirect (or whatever they're supposed to do in an authenticated state) when reloading the page.
In this case I'd make an AuthContext component. By using the context provider, you can access the user logged in state everywhere in your app.
import { createContext } from 'react';
const AuthContext = createContext({
token: null,
userId: null,
login: () => {},
logout: () => {}
});
export default AuthContext;
And provide this at the highest level in your app.
import React, { useState } from 'react';
import { Route, Switch } from 'react-router-dom';
import AuthContext from './context/AuthContext';
function App() {
const [token, setToken] = useState(null);
const [userId, setUserId] = useState(null);
const login = (token, userId) => {
setToken(token);
setUserId(userId);
};
const logout = () => {
setToken(null);
setUserId(null);
};
return (
<Switch>
<Route path="/login" exact strict component={Login}/>
<Route path="/logout" exact strict component={Logout}/>
<PrivateRoute path="/profile" exact strict component={Profile}/>
</Switch>
)
}
export default App;
And in a login/logout form you make the call to a database or localstorage.
import React, { useContext } from 'react';
import AuthContext from '../context/AuthContext';
function Auth() {
const { login } = useContext(AuthContext);
const submitHandler = () => {
// Make the call here
};
return (
<Form onSubmit={submitHandler}>
// Put your login form here
</Form>
);
}
export default Auth;

In a CRA app, how to wait for some action(redux) to get complete first and then only proceed with the App.js render() function?

I am trying to figure out a way to store the authentication state of a user inside the redux store. Suppose isAuthenticated store the state of user if they are logged-in or not. Now, I have a cookie(httpOnly) sent by the server which remembers the user, so that they don't need to enter there credentials every time they visit the app.
Flow: User some day logged in to the application and didn't logged out and closed the browser. Now, he returns and visit my app. Since, the cookie was there in browser, this will be sent automatically(without user interaction) by the application and if the cookie is valid, the isAuthenticated: true. Very simple requirement.
Tracking the authentication status should be the first thing done by the application, so I put that logic at very first, before the App.js renders.
class App extends Component {
store = configureStore();
render() {
return (
<Provider store={this.store}>
<ConnectedRouter history={history}>
<>
<GlobalStyle />
<SiteHeader />
<ErrorWrapper />
<Switch>
<PrivateHomeRoute exact path="/" component={Home} />
<Route exact path="/login" component={LoginPage} />
<PrivateHomeRoute path="/home" component={Home} />
........code
}
This is the configureStore()
export const history = createBrowserHistory();
const configureStore = () => {
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
rootReducer(history),
composeEnhancers(applyMiddleware(sagaMiddleware, routerMiddleware(history)))
);
sagaMiddleware.run(rootSaga);
store.dispatch({ type: AUTH.AUTO_LOGIN });
return store;
};
store.dispatch({ type: AUTH.AUTO_LOGIN }); is the code where I am trying the application to do the auto-login as the first operation in the application. This action is handled by a redux-saga
function* handleAutoLogin() {
try {
const response = yield call(autoLoginApi);
if (response && response.status === 200) {
yield put(setAuthenticationStatus(true));
}
} catch (error) {
yield put(setAuthenticationStatus(false));
}
}
function* watchAuthLogin() {
yield takeLatest(AUTH.AUTO_LOGIN, handleAutoLogin);
}
autoLoginApi is the axios call to the server which will carry the cookie with it. setAuthenticationStatus(true) is action creator which will set the isAuthenticated to true false.
So, yes this is working BUT not as expected. Since, the app should first set the isAuthenticated first and then proceed with the App.js render(). But, since setting the isAuthenticated take some seconds(api call), the application first renders with the isAuthenticated: false and then after the AUTH.AUTO_LOGIN gets completed, then the application re-render for authenticaed user.
What's the problem then? For the normal component it may not be the problem, e.g this SiteHeader component
class SiteHeader extends React.Component {
render() {
const { isLoggedIn } = this.props;
if (isLoggedIn === null) {
return "";
} else {
if (isLoggedIn) {
return (
<LoggedInSiteHeader />
);
} else {
return (
<LoggedOutSiteHeader />
);
}
}
}
}
const mapStateToProps = ({ auth, user }) => ({
isLoggedIn: auth.isLoggedIn,
});
export default connect(
mapStateToProps,
null
)(SiteHeader);
But, this solution doesn't work for the Custom routing.
const PrivateHomeRoute = ({ component: ComponentToRender, ...rest }) => (
<Route
{...rest}
render={props =>
props.isLoggedIn ? (
<ComponentToRender {...props} />
) : (
<Redirect to="/login" />
)
}
/>
);
const mapStateToProps = auth => ({
isLoggedin: auth.isLoggedIn
});
export default connect(
mapStateToProps,
null
)(PrivateHomeRoute);
PrivateHomeRoute gets resolved before the redux store gets updated, hence the Route always goes to "/login".
I am looking for a solution, where the application doesn't proceed further until the authentication action doesn't complete. But, I am no clue what and where to put that code?
Few things I tried:
async await on configureStore() - Error came
async await on App.js - Error
PS: Libraries I am using redux, redux-saga,react-router-dom, connected-react-router, axios
One way I figured out:
Create a separate component MyRouteWrapper which will return the routes based on the isLoggedIn status. To, resolve the issue I stop the routes to render until the auto-login changes the isLoggedIn state.
I set the default state of isLoggedIn to null. Now, if the state is null the MyRouteWrapper will return an empty string, and once the state gets changes to true/false, it will return the routes, and then respective components get rendered.
I changed my App.js
const store = configureStore();
class App extends Component {
render() {
return (
<Provider store={store}>
<ConnectedRouter history={history}>
<MyRouteWrapper />
</ConnectedRouter>
</Provider>
);
}
}
export default App;
The component which make sure to return the Route only when the state gets changed to true/false
const MyRouteWrapper = props => {
if (props.isLoggedIn === null) {
return "";
} else {
return (
<>
<GlobalStyle />
<SiteHeader />
<ErrorWrapper />
<Switch>
<ProtectedHomeRoute
exact
path="/"
component={Home}
isLoggedIn={props.isLoggedIn}
/>
<Route path="/profile/:id" component={Profile} />
<Route path="/login" component={LoginPage} />
</Switch>
</>
);
}
};
const mapStateToProps = ({ auth }) => ({
isLoggedIn: auth.isLoggedIn
});
export default connect(mapStateToProps)(MyRouteWrapper);
This solved the issue.
I am still curious to know the solutions(better) anyone have in there mind.

Resources