I'm following a tutorial for authenticating react routes and I can now allow users to access a route if authenticated is true.
routes.js
import React from 'react';
import { Route, IndexRoute,Redirect } from 'react-router';
import App from './app';
import requireAuth from '../Authentication/require_authentication';
import Dashboard from '../Dashboard';
import Managers from '../Managers';
import Guests from '../Guests';
import Login from '../Login';
export default (
<Route path="/" component={App}>
<IndexRoute component={requireAuth(Dashboard)} />
<Route path="dashboard" component={requireAuth(Dashboard)} />
<Route path="advertisers" component={requireAuth(Managers)} />
<Route path="properties" component={requireAuth(Guests)}/>
<Route path="login" component={Login} />
</Route>
);
require_authentication.js
/*HIGHER ORDER COMPONENT */
import React, { Component } from 'react';
import { connect } from 'react-redux';
export default function(ComposedComponent) {
class Authentication extends Component {
static contextTypes = {
router: React.PropTypes.object
}
componentWillMount() {
if (!this.props.authenticated) {
this.context.router.push('/login');
}
}
componentWillUpdate(nextProps) {
if (!nextProps.authenticated) {
this.context.router.push('/login');
}
}
render() {
return <ComposedComponent {...this.props} />
}
}
function mapStateToProps(state) {
return { authenticated: state.auth.authenticated };
}
return connect(mapStateToProps)(Authentication);
}
When a user logs in they have this as the state in the localStorage:
userId: 1,
userName:"Bob",
roleName:"Manager"
I'm still fairly new to composed components but I'm wondering if there's a way to not only authenticate but prevent users with specific roles from accessing routes. Eg. Managers should only be able to see the Manager route and Dashboard. Guests can only see Dashboard and Guests.
It's definitely possible, you simply need to add a condition after checking authorization that would check if user's role allows to see the route it's trying to render, and if not it may redirect the user to a default route or a page that displays an error message.
Following the concept of your Higher Order Component in require_authentication.js, here's a modified version that should do what you want:
const checkPermissions = (userRole, router) => {
if (userRole === "Manager") {
if (!(router.isActive('/dashboard') ||
router.isActive('/manager'))) {
return false;
}
}
return true;
};
export default function(ComposedComponent) {
class Authentication extends Component {
static contextTypes = {
router: React.PropTypes.object
}
componentWillMount() {
if (!this.props.authenticated) {
this.context.router.push('/login');
} else if (!checkPermissions(this.props.user.role, this.context.router)) {
this.context.router.push('/');
}
}
componentWillUpdate(nextProps) {
if (!nextProps.authenticated) {
this.context.router.push('/login');
} else if (!checkPermissions(this.props.user.role, this.context.router)) {
this.context.router.push('/');
}
}
render() {
return <ComposedComponent {...this.props} />
}
}
function mapStateToProps(state) {
return {
authenticated: state.auth.authenticated,
user: state.auth.user
};
}
return connect(mapStateToProps)(Authentication);
}
An implementation of the checkPermission function may be different and more configurable, the one from the example is just to demonstrate the idea.
I assumed that you have an authorized user data in your state inside the auth object. It may be changed depend on where user's data is stored in fact.
Related
In my App I have some public screens that are accessible even if the user is not logged in, and some screens are protected (you must be logged in to access them).
My solution to the problem is to check the component willFocus Listener and if not logged in, the user should be redirected to the loginPage.
export async function ProtectRoute(navigation){
//if page will enter and the user is not authenticated return to login
navigation.addListener(
'willFocus',
async () => {
let token = await getTokenAsync();
if(!token){
navigation.navigate('Login');
}
})
}
In my screen I Call this function in ComponentWillMount lifecycle.
The issue is that it takes like a second to verify the token and the page is displayed briefly.
How can I make it so that he goes directly to the Login Page without that lag ?
I wrote a quick example below. You can examine and use it.
import React, { Component } from "react";
import { Text, View } from "react-native";
const withAuth = WrappedComponent => {
class AuthenticationScreen extends Component {
constructor(props) {
super(props);
this.state = {
isAuthenticated: false
};
props.navigation.addListener("willFocus", async () => {
await this.checkAuth();
});
}
remoteReuqest = async () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(true);
}, 2000);
});
};
checkAuth = async () => {
const result = await this.remoteReuqest();
if (result) {
this.setState({
isAuthenticated: true
});
} else {
this.props.navigation.navigate("Login");
}
};
render() {
if (!this.state.isAuthenticated) {
return <Text>Waiting...</Text>;
}
return <WrappedComponent {...this.props} />;
}
}
return AuthenticationScreen;
};
export default withAuth;
You can use it as follows.
import React, { Component } from "react";
import { Text, StyleSheet, View } from "react-native";
import withAuth from "./withAuth";
class ContactScreen extends Component {
render() {
return (
<View>
<Text> Contact Screen </Text>
</View>
);
}
}
const styles = StyleSheet.create({});
const extendedComponent = withAuth(ContactScreen);
extendedComponent.navigationOptions = {
title: "Contact"
};
export default extendedComponent;
The issue is that it takes like a second to verify the token and the page is displayed briefly.
The reason is because reading/writing from/to AsyncStorage is an asychronous operation.
In my screen I Call this function in ComponentWillMount lifecycle.
I suggest you to not use ComponentWillMount lifecycle because it's deprecated and it will be removed from React (https://reactjs.org/docs/react-component.html#unsafe_componentwillmount)
After this introduction, now i show you how I have achieved this in my app: CONTEXT API! (https://reactjs.org/docs/context.html)
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
How to implement context api:
the context will be the 'state' of your App.js file. You root App.js will be the provider of the context, while other views which will need the context are called the consumers of the context.
First of all, you need to create a 'skeleton' of your context into a separate file, something like this:
// AuthContext.js
import React from 'react'
const AuthContext = React.createContext({
isLogged: false,
login: () => {},
logout: () => {}
})
export default AuthContext
Your App.js will import, contain and initialize the context:
// App.js
// all necessary imports
import AuthContext from '....'
export default class App extends React.Component {
constructor (props) {
super(props)
this.state = {
isAuth: false,
login: this.login,
logout: this.logout
}
login = async userData => {
// do your login stuff and then
this.setState({ isAuth: true })
}
logout = async () => {
// do your logout stuff and then
this.setState({ isAuth: false })
}
async ComponentDidMount () {
// check the asyncStorage here and then, if logged:
this.setState({ isAuth: true })
}
render () {
return (
<AuthContext.Provider value={this.state}>
<AppContainer />
</AuthContext.Provider>
)
}
Then, into the View contained into AppContainer, you could access context like this:
import AuthContext from '.....'
// all necessary imports
export default class YourView extends React.Component<Props, State> {
constructor (props) {
super(props)
this.props = props
this.state = { ... }
}
// THIS IS IMPORTANT
static contextType = AuthContext
// with this, you can access the context through 'this.context'
ComponentDidMount () {
if (!this.context.isAuth) this.props.navigation.navigate('login')
}
Advantages of this approach:
Checking a boolean is so fast that you will not notice a blank screen.
Sharing an Authentication Context everywhere in you app
Making the access to asyncstorage only the first time that app mounts and not everytime you need to check if the user is logged
Sharing methods to login/logout everywhere in your app
I have been using React Native for a few years and have only recently needed to utilise Redux on a new, more complex project. I am currently in the process of following a number of tutorials trying to work my way through the basics.
I am currently stuck with the following error:
Invariant Vilation: Could not find "store" in either the context of props of "Connect(App)"
I have found a number of posts with information about this error but because of the low amount of knowledge I currently have, I am unsure as to how to correctly implement a fix.
This project was created with create-react-native-app and I am using the Expo app to test.
In my eyes this should work because the root element of App is a Provider element passing the store as a prop which seems to contradict what the error is saying.
configureStore.js
import { createStore, applyMiddleware } from 'redux';
import app from './reducers';
import thunk from 'redux-thunk';
export default function configureStore() {
return createStore(app, applyMiddleware(thunk));
}
App.js:
import React from 'react';
import { Text } from 'react-native';
import { Provider, connect } from 'react-redux';
import configureStore from './configureStore';
import fetchPeopleFromAPI from './actions';
const store = configureStore();
export class App extends React.Component {
componentDidMount() {
props.getPeople()
}
render() {
const { people, isFetching } = props.people;
return (
<Provider store={store}>
<Text>Hello</Text>
</Provider>
);
}
}
function mapStateToProps(state) {
return {
people: state.people
}
}
function mapDispatchToProps(dispatch) {
return {
getPeople: () => dispatch(fetchPeopleFromAPI())
}
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
You are trying to access the store in the App component even before it has been passed. Therefore it is not able to find the store.
You need to make a separate component and connect that using react-redux such as
<Provider store={store}>
<ConnectedComponent />
</Provider>
...
class ConnectedComponent extends React.Component {
componentDidMount () {
this.props.getPeople()
}
render() {
return (
<View>
<Text> ... </Text>
</View>
)
}
}
function mapStateToProps(state) {
return {
people: state.people
}
}
function mapDispatchToProps(dispatch) {
return {
getPeople: () => dispatch(fetchPeopleFromAPI())
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ConnectedComponent);
I have been looking for a solution to redirect to specific url after successful authentication in react-admin,
when I paste http://localhost:1234/#/students/sdf2343afs32 on the url if already signed-in then I'm getting the user detail page but if not singed-in and after singing-in it shows home page instead
You can customize the redirect URL after login inside the authProvider as explained in the Checking Credentials During Navigation part of the documentation:
// in authProvider.js
import { AUTH_CHECK } from 'react-admin';
export default (type, params) => {
// ../
if (type === AUTH_CHECK) {
return isLogged
? Promise.resolve({ redirectTo: '/custom-url' })
: Promise.reject({ redirectTo: '/no-access' });
}
// ...
};
Based on https://stackoverflow.com/a/35715159/986160 using react-admin 2.6.2
What worked for me is a custom Dashboard like that (assuming this is your default landing page):
import React, { Component } from 'react';
import { Redirect } from 'react-router';
import Card from '#material-ui/core/Card';
import CardContent from '#material-ui/core/CardContent';
import CardHeader from '#material-ui/core/CardHeader';
export default class Dashboard extends Component {
render() {
if (localStorage.getItem("user_role") !== "special_role") {
return <Card>
<CardHeader title="Welcome to Dashboard" />
<CardContent></CardContent>
</Card>
}
else {
return (<Redirect to="/route/to/redirect" />);
}
}
}
In my react native app I'm using redux to handle state transition of a Post object -- the state is changed by couple of child components. The Post object has properties like title, name, description which the user can edit and Save.
In the reducer Im using React.addons.update return new state object.
The main container view has 2 custom child components (wrapped in TabBarNavigator).
One of the child component has few TextInputs which is updating a state.
Using the logger middleware and console.log() I see the new state value in the parent view's render() (via this.props.name) but not in the child view.
I'm trying to figure out why the updated state is not propagated to the child container. Any suggestion is much appreciated.
Im at a point where Im thinking of subscribeing to the redux store manually in the child container but it feels wrong
my code looks like this:
MainView
Reducer
configure store etc
The MainView
const React = require('react-native');
const {
Component,
} = React;
const styles = require('./../Styles');
const MenuView = require('./MenuView');
import Drawer from 'react-native-drawer';
import TabBarNavigator from 'react-native-tabbar-navigator';
import BackButton from '../components/BackButton';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as PostActions from '../actions/Actions';
import {Details} from './Article/Details';
import {ArticleSecondary} from './Article/Secondary';
var update = require('react-addons-update');
import configureStore from '../store/configureStore';
class ArticleMainView extends Component {
constructor(props){
super(props);
//var store = configureStore(props.route.post);
this.state = {
};
}
componentDidMount(){
}
savePost() {
console.log(this.props.post.data);
this.props.navigator.pop();
}
render(){
console.log("ArticleMainView: render(): " + this.props.name);
return(
<TabBarNavigator
ref="navComponent"
navTintColor='#346293'
navBarTintColor='#94c1e8'
tabTintColor='#101820'
tabBarTintColor='#4090db'
onChange={(index)=>console.log(`selected index ${index}`)}>
<TabBarNavigator.Item title='ARTICLE' defaultTab>
<Details ref="articleDetail"
backButtonEvent={ () => {
this.props.navigator.pop();
}}
saveButtonEvent={ () => {
this.savePost();
}}
{...this.props}
/>
</TabBarNavigator.Item>
<TabBarNavigator.Item title='Secondary'>
<ArticleSecondary ref="articleSecondary"
{...this.props}
backButtonEvent={ () => {
this.props.navigator.pop();
}}
saveButtonEvent={ () => {
this.savePost();
}}
/>
</TabBarNavigator.Item>
</TabBarNavigator>
);
}
}
function mapStateToProps(state) {
return {
post: state,
text: state.data.text,
name: state.data.name,
description: state.data.description
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(PostActions, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(ArticleMainView);
The Reducer:
import {Constants} from '../api/Constants';
var update = require('react-addons-update');
export default function postReducer(state, action) {
switch(action.type) {
case Constants.SET_POST_TEXT:
if( state.data.text){
return update(state, {
data: { $merge: {text: action.text }}
});
}else{
return update(state, {
data: { $merge: {text: action.text }}
});
}
break;
case Constants.SET_POST_NAME:
return update(state, {
data: { name: { $set: action.text }}
});
return newO;
break;
case Constants.SET_POST_DESCRIPTION:
return update(state, {
data: { description: { $set: action.text }}
});
break;
default:
return state;
}
}
render scene of the app:
renderScene(route, navigator) {
switch (route.id) {
case "ArticleMainView":
let store = configureStore(route.post);
delete route.post; // TODO: not sure if I should remove this
return (
<Provider store={store}>
<ArticleMainView navigator={navigator} {...route}/>
</Provider>
);
default:
return <LandingView navigator={navigator} route={route}/>
}
}
configureStore:
import { createStore,applyMiddleware,compose } from 'redux'
import postReducer from '../reducers/SocialPostReducer';
import createLogger from 'redux-logger';
const logger = createLogger();
export default function configureStore(initialState){
return createStore(
postReducer,
initialState,
compose(applyMiddleware(logger))
);
}
If anyone stumbles on this question this is how I solved it. In each of the child components I declared a contextTypes object like so
ChildComponentView.contextTypes = {
store: React.PropTypes.object
}
to access the current state in the child component
let {store} = this.context;
store.getState();
I don’t know React Native well but something that threw me off is that you’re effectively creating a store on every render:
case "ArticleMainView":
let store = configureStore(route.post);
delete route.post; // TODO: not sure if I should remove this
return (
<Provider store={store}>
<ArticleMainView navigator={navigator} {...route}/>
</Provider>
);
Store should only be created once per application lifetime. It never makes sense to create it inside render() or renderScene() or similar methods. Please check the official Redux examples to see how the store is typically created.
Another problem is that you don’t show how you update the data, which child component doesn’t get updated, when you expect it to get updated, and so on. This is a lot of code, and it is very hard to help because it is incomplete, and most of it is not relevant to the problem. I would suggest you to remove all the irrelevant code until you can reproduce the problem with a minimal possible complete example. Then you can amend your question to include that example.
So i started supporting redux application state in Navigator which is great. This gives you the flexibility to change a state based on an action ex: getSessionToken token doesn't exist change state to start walkthrough if token is invalid take them to loginState. However this becomes a problem when you want pages that aren't stateful or dependent on actions on redux. An example of this would be on the login page you have a link to the register page and use this.props.navigator.push() with the code posted below this would work but the problem lies when you use this.props.navigator.push and then try to have a stateful action happen. route.component seems to persist and never gets discarded.. This means the stateful routes can never be hit since route.component still holds a value. I was wondering if anyone has came up with a solution for this? Or has any ideas on how to make this code better.
'use strict';
import React from 'react-native'
let {Navigator,View} = React
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux/native';
import {Map} from 'immutable';
/**
* Components
**/
import LoginContainer from './LoginContainer'
import MatchContainer from './MatchContainer'
import WalkthroughContainer from './WalkthroughContainer'
/**
* Actions
**/
import * as authActions from '../reducers/auth/authActions';
import * as globalActions from '../reducers/global/globalActions';
const actions = [authActions, globalActions]
// Save that state
function mapStateToProps(state) {
return {
...state
};
};
/**
* Bind all the functions from `actions` and bind them with `dispatch`
**/
function mapDispatchToProps(dispatch) {
const creators = Map()
.merge(...actions)
.filter(value => typeof value === 'function')
.toObject();
return {
actions: bindActionCreators(creators, dispatch),
dispatch
};
}
/**
* App Class
**/
let styles = StyleSheet.create({
navigator: {
flex: 1
}
});
let App = React.createClass({
getInitialState() {
return {
currentRoute: null,
}
},
/**
* change the route to match the current redux state
**/
componentWillReceiveProps(props) {
this.setState({
currentRoute: props.auth.form.state
});
},
componentDidMount() {
this.props.actions.getSessionToken();
},
renderScene(route, navigator) {
// This part is buggy
if (route && route.component) {
let Component = route.component;
return (
<Component navigator={navigator} />
)
}
switch (this.state.currentRoute) {
case 'LOGIN_STATE_LOGOUT':
return <MatchContainer navigator={navigator} />
case 'LOGIN_STATE_LOGIN':
return <LoginContainer navigator={navigator} />
case 'LOGIN_STATE_REGISTER':
return <RegisterContainer navigator={navigator} />
case 'WALKTHROUGH_STATE_START':
return <WalkthroughContainer navigator={navigator} />
default:
return <View />
}
},
render() {
return (
<Navigator
style={styles.navigator}
renderScene={this.renderScene}
initialRoute={{ route: this.state.currentRoute }}
configureScene={(route) => {
if (route.sceneConfig) {
return route.sceneConfig;
}
return Navigator.SceneConfigs.FloatFromBottomAndroid;
}} />
)
}
});
export default connect(mapStateToProps, mapDispatchToProps)(App);