React with Firebase - splitting authorisation between CRUD actions - javascript

I am trying to use react with firebase to make an app that allows all users to read everything and some users to write to some functions.
At the moment, I have my firestore security rules set to allow all users to read and write everything, and then I have a withAuthorisation wrapper that I want to put around a component that contains a link to make write a new document. I know this isn't secure, but I'm just trying to get the hang of how to separate the code so that I can build view layers that render the content in line with the permissions I write.
At the moment I have a references list, which is an index of all the references created. At the bottom of that list, I have a component called 'AddReference' which is a link to the form to make a new reference.
The list is not wrapped in my withAuthorisation wrapper. The AddReference component is wrapped.
I am expecting to be logged out and render the list to all users - (everyone can read the index) but a logged out user should not see the AddReference link.
Instead, the entire list is blocked behind an authentication redirect.
My list of all the references has:
import React, { Component } from 'react';
import { Link, withRouter } from 'react-router-dom';
import { compose } from 'recompose';
import { withFirebase } from '../../Firebase/Index';
import * as ROUTES from '../../../constants/Routes';
import { ReferencesList } from './ReferencesList';
import { Layout, Typography, Card, List, Button, Divider } from 'antd';
import {ReferencesForm} from './Form';
import AddReference from './AddReference';
const { Content } = Layout
const { Title, Paragraph, Text } = Typography;
class ReferencesPage extends Component {
render() {
return (
<div>
<Content
style={{
background: '#fff',
padding: 24,
margin: "auto",
minHeight: 280,
width: '90%'
}}
>
<ReferencesList/>
<br/>
<AddReference />
</Content>
</div>
);
}
}
export default ReferencesPage;
My AddReference component has:
import React, { Component } from 'react';
import { Link, withRouter } from 'react-router-dom';
import { compose } from 'recompose';
import { withFirebase } from '../../Firebase/Index';
import * as ROUTES from '../../../constants/Routes';
import { ReferencesList } from './ReferencesList';
import { Layout, Typography, Card, List, Button, Divider } from 'antd';
import {ReferencesForm} from './Form';
import { AuthUserContext, withAuthorization, withEmailVerification } from '../../Session/Index';
const { Content } = Layout
const { Title, Paragraph, Text } = Typography;
const AddReference = () => (
<AuthUserContext.Consumer>
{authUser => (
<div>
<Divider></Divider>
<div style={{
display: "flex",
justifyContent: "center"
}}>
<Link to={ROUTES.REFERENCESFORM}>Add a Reference</Link>
</div>
</div>
)}
</AuthUserContext.Consumer>
);
const condition = authUser => !!authUser;
export default compose(
// withEmailVerification,
withAuthorization(condition),
)(AddReference);
My withAuthorisation wrapper has:
import React from 'react';
import { withRouter } from 'react-router-dom';
import { compose } from 'recompose';
import { withFirebase } from '../Firebase/Index';
import AuthUserContext from './Context';
import * as ROUTES from '../../constants/Routes';
const withAuthorization = condition => Component => {
class WithAuthorization extends React.Component {
// componentDidMount() {
// this.listener =
this.props.firebase.auth.onAuthStateChanged(authUser => {
// if (!condition(authUser)) {
// this.props.history.push(ROUTES.SIGN_IN);
// }
// });
// }
componentDidMount() {
this.listener = this.props.firebase.onAuthUserListener(
authUser => {
if (!condition(authUser)) {
this.props.history.push(ROUTES.SIGN_IN);
}
},
() => this.props.history.push(ROUTES.SIGN_IN),
);
}
componentWillUnmount() {
this.listener();
}
render() {
return (
<AuthUserContext.Consumer>
{authUser =>
condition(authUser) ? <Component {...this.props} /> : null
}
</AuthUserContext.Consumer>
);
}
}
return compose(
withRouter,
withFirebase,
)(WithAuthorization);
};
export default withAuthorization;
Is it possible to have a component that is wrapped inside an authorisation requirement rendered on a page that is not wrapped? I still want to show the content of the list (other than the AddReference component) for users that do not satisfy the authorisation condition.

1> how to implement permissions in an optimize way ?
Answer:
Best practice for React Router user roles (Firebase)
Role-Based Access Control (RBAC) and React Apps.
2> Is it possible to have a component that is wrapped inside an authorisation requirement rendered on a page that is not wrapped? I still want to show the content of the list (other than the AddReference component) for users that do not satisfy the authorisation condition.
Short Answer: Yes, make the page accessible to all users and hide the content of the page which should not be visible to public.
Answer: Well, we need to have a global state ( REDUX, localStorage, etc ) in which we will store user roles ( guest, login etc ) at the time initialization. then when we need to hide an element from component, we just have to check for a value like below
//set state.userRole from globalState in init function.
(this.state.userRole == 'login') ?
<Icon name='md-log-out' style={styles.logoutIcon} button onPress={() =>
Alert.alert(
'Log out',
'Do you want to logout?',
[
{ text: 'Cancel', onPress: () => { return null } },
{
text: 'Confirm', onPress: () => {this.logout()}
},
],
{ cancelable: false }
)
}/>
:
<View />;

Related

How to detect if another component is present in the document?

I have a site built with React Static that has a Header component that is always present. Depending on if the current page has a hero component or not, the Header should be either light or dark.
The Header is rendered outside of the routes and the useEffect is triggered before the children is rendered. This is probably because of the routing.
This is the current code:
// App.js
import React, { useState, useEffect } from 'react'
import { Root, Routes } from 'react-static'
export default () => {
const [useDarkTheme, setUseDarkTheme] = useState(false);
useEffect(() => {
if (typeof document !== "undefined") {
const heroPresent = document.querySelectorAll(".o-hero").length > 0;
console.log("The hero is present: " + heroPresent);
setUseDarkTheme(!heroPresent);
}
})
return (
<Root>
<React.Suspense fallback={ <em>Loading...</em> }>
<Header useDarkTheme={ useDarkTheme } />
<Routes default />
</React.Suspense>
</Root>
);
}
What will be rendered at <Routes default /> is the static pages configured in React Static's static.config.js.
Below is an example of the Hero component:
// Hero.js
import React from "react";
export default () => {
console.log("This is the Hero rendering. If this exist, the Header should be dark.");
return (
<div className="o-hero">
<p>Hero!</p>
</div>
);
}
When I run the application and look at the logs this is what I get:
The hero is present: false
This is the Hero rendering. If this exist, the Header should be dark.
How could I somehow detect the presence of the Hero from the Header although the Hero is in a router and the Header is not? This feels like quite a common use case, but I could not find any info on the interwebs.
Thanks in advance!
So I ended up using useContext to provide all children with a getter and a setter for the Header's theme (dark or light). The solution is very much inspired from this answer. The solution looks like this:
// App.js
import React, { useState, useContext } from 'react'
import { Root, Routes } from 'react-static'
import { HeaderThemeContext } from "./context";
export default () => {
const { theme } = useContext(HeaderThemeContext);
const [headerTheme, setHeaderTheme] = useState(theme);
return (
<Root>
<React.Suspense fallback={ <em>Loading...</em> }>
<HeaderThemeContext.Provider value={ { theme: headerTheme, setTheme: setHeaderTheme } }>
<Header theme={ headerTheme } />
<Routes default />
</HeaderThemeContext.Provider>
</React.Suspense>
</Root>
);
}
// Hero.js
import React from "react";
import { headerThemes, setHeaderTheme } from "./context";
export default () => {
setHeaderTheme(headerThemes.DARK);
console.log("This is the Hero rendering. If this exist, the Header should be dark.");
return (
<div className="o-hero">
<p>Hero!</p>
</div>
);
}
// context.js
import React, { createContext, useContext } from "react";
export const headerThemes = {
LIGHT: "light",
DARK: "dark",
};
export const HeaderThemeContext = createContext({
theme: headerThemes.LIGHT,
setTheme: () => {}
});
// This is a hook and can only be used in a functional component with access to the HeaderThemeContext.
export const setHeaderTheme = theme => useContext(HeaderThemeContext).setTheme(theme);
This gives global access to set and get the header theme, which might not be optional, but it works for now and I think it's fine. Please let me know if there is a better way of doing this.

this.props.match.params passed into child component after authorisation

I have recently started building a big project on React using also a Firebase with authentication and I cannot quite understand the relation between the react-router-dom links and React components.
I am struggling with getting the
this.props.match.params // which is going to be 2018 / 2019 / 2020... etc
in the component, which renders as a dynamic route (like unique post component).
I have tried to use only a simple class component and this works but the problem is, without the authentication everyone can access this admin route and everyone would be allowed to edit and delete data there. I want it to be accessed only by authenticated users. (Admins)
So this is how my piece of code looks like:
Main component: (where the link is)
import React, { Component } from 'react'
import { Link } from 'react-router-dom'
class SeasonBox extends Component {
render() {
return (
<Link className='seasonbox' to={`/adminseason/${this.props.season}`}>
<p className='seasonbox__season'>{this.props.season}/{this.props.season+1}</p>
</Link>
)
}
}
export default SeasonBox;
And the component that renders after the link is clicked:
import React, { Component } from 'react'
import { Link } from 'react-router-dom'
import { connect } from 'react-redux'
import { compose } from 'recompose'
import { withAuthorisation } from '../Session'
import { withFirebase } from '../Firebase'
const AdminMatchesBox = ({authUser}) => (
<div>{authUser ? <AdminMatchesBoxAuth /> : <AdminMatchesBoxNonAuth />} </div>
)
class AdminMatchesBoxAuth extends Component {
render() {
return (
<div>
Hey I am the season {this.props.match.params}!
<Link to={'/adminmatches'}>Wróć</Link>
</div>
)
}
}
const AdminMatchesBoxNonAuth = () => (
<div>
<h1>You do not have permission to visit this page.</h1>
</div>
)
const mapStateToProps = state => ({
authUser: state.sessionState.authUser
});
const condition = authUser => !!authUser
export default compose(withAuthorisation(condition), connect(mapStateToProps),withFirebase)(AdminMatchesBox);
So if I don't use authorisation, and I use only a single class component I can get this.props.match.params -> which is the id of the website and I need it to access data from the database.
However, I want it to not be visible by not logged users and I had to process it through the authorisation process.
I am receiving an error
Cannot read property 'params' of undefined.
I have no clue how to pass match.params into the AdminMatchesBoxAuth component.
Could anyone advice?
By wrapping withRouter you able to access params
Try this
import { withRouter } from "react-router";
import React, { Component } from 'react'
import { Link } from 'react-router-dom'
import { connect } from 'react-redux'
import { compose } from 'recompose'
import { withAuthorisation } from '../Session'
import { withFirebase } from '../Firebase'
const AdminMatchesBox = ({authUser}) => (
<div>{authUser ? <AdminMatchesBoxAuth /> : <AdminMatchesBoxNonAuth />} </div>
)
class AdminMatchesBoxAuth extends Component {
constructor (props){
super(props)
}
render() {
return (
<div>
Hey I am the season {this.props.match.params}!
<Link to={'/adminmatches'}>Wróć</Link>
</div>
)
}
}
const AdminMatchesBoxNonAuth = () => (
<div>
<h1>You do not have permission to visit this page.</h1>
</div>
)
const mapStateToProps = state => ({
authUser: state.sessionState.authUser
});
const condition = authUser => !!authUser
export default compose(withRouter, withAuthorisation(condition), connect(mapStateToProps),withFirebase)(AdminMatchesBox)

React/Redux Saga: Need to trigger a route change after user clicks button on form

I'm working on a project that was built using Redux Saga. I have a component that features a group of offers each with their own button to select that specific offer. This component lives on page 1 of 4 and I'm trying to figure out how to trigger a redirect to the next page after the user clicks a selection, but I have no idea how to do this within the context of Redux or Redux Saga. I've included both the navigation handler and my component below. I started to edit the navigation component with a proposed action UPDATE_PAGE: but I'm not sure I'm even heading down the right path. Any suggestions/explanations or code samples on how to do this would be hugely appreciated
Offer Component
import React, { Fragment, Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { Route, Switch, Redirect } from 'react-router-dom'
import styled from 'styled-components'
import NavigationContainer from '../../containers/NavigationContainer'
import RouteLoader from '../../components/core/RouteLoader'
import { getCustomer } from '../../actions/customer'
import { routerPropTypes, routePropTypes } from '../../propTypes'
class OrdersRoutes extends Component {
static propTypes = {
customer: PropTypes.object,
getCustomer: PropTypes.func.isRequired,
routes: routePropTypes,
...routerPropTypes
}
getAppData = data => {
this.props.getCustomer(data)
}
render() {
const { match, customer } = this.props
const { offers: offersRoute, ...routes } = this.props.routes
return (
<Fragment>
<Navigation>
<Route
path={match.path}
component={NavigationContainer}
/>
</Navigation>
<PageContainer>
<Switch>
<RouteLoader
exact
path={`${match.path}${offersRoute.path}`}
component={offersRoute.component}
shouldLoad={customer !== undefined}
onLoad={this.getAppData}
/>
{Object.values(routes).map(
({
path, id, component, disabled
}) =>
!disabled && (
<Route
exact
key={id}
path={`${match.path}${path}`}
component={component}
/>
)
)}
<Redirect to={`${match.path}${offersRoute.path}`} />
</Switch>
</PageContainer>
</Fragment>
)
}
}
const Navigation = styled.div`
padding: 0 10px;
`
const PageContainer = styled.div`
padding: 0 20px;
margin-top: 10px;
`
const mapStateToProps = state => ({
routes: state.navigation.pages.orders.routes,
customer: state.customer.details
})
const mapDispatchToProps = { getCustomer }
export default connect(
mapStateToProps,
mapDispatchToProps
)(OrdersRoutes)
Navigation Component
import * as pages from '../constants/pages'
export const initialState = {
pages: {
...pages
}
}
function navigationReducer(state = initialState, { type, payload }) {
switch (type) {
UPDATE_PAGE: {
const { id, page } = payload
return {
...state,
pages: {
...state.pages,
[id]: {
...state.pages[id],
...page
}
}
}
}
default: {
return state
}
}
}
export default navigationReducer
The trick is to include react-router's history into the payload of UPDATE_PAGE.
So 1) we wrap a component which triggers this action withRouter. This gives us access to history as a prop; 2) when dispatching UPDATE_PAGE, include history as a payload in addition to id and page 3) in redux-saga perform a redirect on every UPDATE_PAGE:
yield takeEvery(UPDATE_PAGE, function*(action){ action.payload.history.push('/new-route'); })

React does not render correct on page refresh

I am using ant design and server side render in my React project.
My header rendered according to user authentication status. If user is authenticated appHeader is used.
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Layout, BackTop, Button } from 'antd'
import LandingHeader from './_Landing/Header/LandingHeader'
import AppHeader from './Common/AppHeader'
import { withRouter } from 'react-router-dom'
import { bool, object } from 'prop-types'
const { Content, Footer } = Layout;
class AppLayout extends Component {
constructor (props) {
super(props)
this.state = {}
}
render () {
const { children, location } = this.props
const isLoggedIn = this.props.isAuthenticated
let AppHeaderConditional = null
if (isLoggedIn && location.pathname != '/' && location.pathname != '/login' && location.pathname != '/signup') {
AppHeaderConditional = <AppHeader />
} else {
AppHeaderConditional = <LandingHeader />
}
return (
<div className='landing-page-wrapper' data-pathname={`${location.pathname}`}>
{AppHeaderConditional}
<Layout>
<Content>
{children}
</Content>
</Layout>
<BackTop>
<Button type='primary' shape='circle' icon='up-circle-o' size='large' />
</BackTop>
<Footer className='footer' style={{ textAlign: 'center' }} >
© 2017 -
</Footer>
</div>
)
}
}
AppLayout.propTypes = {
isAuthenticated: bool,
children: object,
location: object
}
const mapStateToProps = state => {
return {
isAuthenticated: state.user.isAuthenticated
}
}
export default withRouter(connect(mapStateToProps)(AppLayout))
On full page load (i mean navigating from home to member page with link ) it renders correct with className but on page refresh this class not added to header. And console log gives an error "Warning: Did not expect server HTML to contain a..."
I made research about this console warning but nothing helped me. I tried pure:false (https://github.com/reactjs/react-redux/blob/master/docs/troubleshooting.md), and some other things but i cant solve the issue.
You have to use staticRouter on server and browserRouter on the client - https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/guides/server-rendering.md
I found the solution. All you need is below. Because somehow page renders itself second time on client, i don't know why but this the solution.
componentDidMount () {
this.setState({ mounted: true })
}

Modal dialog auth with react-router

I have react/redux/react-router application with public and private parts.
Login and register form are shown as modal dialogs and don't have their own routes.
Desired flow:
user clicks on link -> modal dialog is shown on current page -> in case of successful auth transition to linked page, else leave user on current page.
If there is no current page-show index page and continue with the flow
I'm tried to archive this using onEnter hook, but as far as i can see transition happens before hook is executed. If I try to use history.goBack() it's causes rerendering of page and looks nasty.
Is there any way to solve this problem without unnecessary redirects and extra render calls?
OK - I think I came up with a way to handle this that covers all the corner cases. It does require that you have some way to access application state from almost any component though. I am using Redux for that. This also assumes that login, register, etc do NOT have routes.
What I did was create two 'wrapper' components. The first one wraps any insecure routes and stores the location to a state value so that we always have a reference to the last insecure route...
import { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { setRoute } from './redux/client/ducks/auth';
function mapDispatchToProps(dispatch) {
return {
setRoute: bindActionCreators(setRoute, dispatch)
};
}
#connect(null, mapDispatchToProps)
export default class InsecureWrapper extends Component {
componentDidMount() {
const { location, setRoute } = this.props;
setRoute(location.pathname);
}
render() {
return (
<div>
{this.props.children}
</div>
)
}
}
The other one wraps all secure routes. It displays the login dialog (can also toggle back and forth between login and register or whatever) and prevents the content from being displayed unless logged into the app...
import { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as authActions from './redux/client/ducks/auth';
function mapStateToProps(state) {
return {
auth: state.auth
};
}
function mapDispatchToProps(dispatch) {
return {
authActions: bindActionCreators(authActions, dispatch)
};
}
#connect(
mapStateToProps,
mapDispatchToProps
)
export default class SecureWrapper extends Component {
componentDidMount() {
const { auth, authActions } = this.props;
//see if user and if not prompt for login
if(!auth.loggedIn) {
authActions.openLogin();
}
}
//close any open dialogs in case the user hit browser back or whatever
componentWillUnmount() {
const { authActions } = this.props;
authActions.resetAuthDialogs();
}
render() {
const { auth, children } = this.props;
return (
<div className="container">
{auth.loggedIn &&
{children} ||
<span>YOU MUST BE LOGGED IN TO VIEW THIS AREA!</span>
}
</div>
);
}
}
Then in the routes, just wrap them in the wrappers as needed...
import App from './containers/App';
import Dashboard from './containers/Dashboard';
import Secure from './containers/Secure';
import AuthWrapper from './common/client/components/AuthWrapper';
import InsecureWrapper from './common/client/components/InsecureWrapper';
export default [
{path: '/', component: App, //common header or whatever can go here
childRoutes: [
{component: InsecureWrapper,
childRoutes: [ //<- ***INSECURE ROUTES ARE CHILDREN HERE***
{path: 'dashboard', component: Dashboard}
]
},
{component: SecureWrapper,
//***SECURE ROUTES ARE CHILDREN HERE***
childRoutes: [
{path: 'secure', component:Secure}
]
}
]}
]
Last but not least... in your Dialogs, you need to handle the cancel by pushing (or replacing) the location to the saved state value. And of course on successful login, just close them...
import { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as authActions from './redux/client/ducks/auth';
import LoginDialog from './common/client/components/dialogs/LoginDialog';
import RegisterDialog from './common/client/components/dialogs/RegisterDialog';
// this could also be replacePath if you wanted to overwrite the history
import { pushPath } from 'redux-simple-router';
function mapStateToProps(state) {
return {
auth: state.auth
};
}
function mapDispatchToProps(dispatch) {
return {
authActions: bindActionCreators(authActions, dispatch),
pushPath: bindActionCreators(pushPath, dispatch),
};
}
#connect(
mapStateToProps,
mapDispatchToProps
)
export default class AuthContainer extends Component {
_handleLoginCancel = (e) => {
const { auth, authActions, pushPath } = this.props;
pushPath(auth.prevRoute); // from our saved state value
authActions.closeLogin();
};
_handleLoginSubmit = (e) => {
const { authActions } = this.props;
// do whatever you need to login here
authActions.closeLogin();
};
render() {
const { auth } = this.props;
return (
<div>
<LoginDialog
open={auth.showLogin}
handleCancel={this._handleLoginCancel}
handleSubmit={this._handleLoginSubmit}
submitLabel="Login"
/>
...
</div>
)
}
}
I am obviously using ES6, Babel, and webpack... but the principles should apply without them as should not using Redux (you could store the prev route in local storage or something). Also I left out some of the intermediate components that pass props down for brevity.
Some of these could be functional components, but I left them full to show more detail. There is also some room for improvement by abstracting some of this, but again I left it redundant to show more detail. Hope this helps!

Categories