ReactJS ensure only owner of object can edit/delete - reactjs

I do RBAC and authorization on the backend part of the application (NodeJS) and never really bothered about enforcing authorization on the UI as well.
However, let's say I have the following React Router 4 routes:
<Switch>
<Route exact path="/books/all" component={BookList} />
<Route exact path="/books/new" component={BookNew} />
<Route exact path="/books/:bookId" component={BookDetails} />
<Route exact path="/books/:bookId/edit" component={BookEdit} />
</Switch>
And I want to make sure that if a logged in user visits a book that is not his, he is not able to render the route /books/<not my book>/edit. I am able to do this by implementing a simple check at the ComponentDidMount() function:
checkAuthorisation = userId => {
if (this.props.authenticated._id !== userId) {
this.props.history.push("/books/all");
}
};
But I was wondering whether there is a better approach / design pattern of doing it in ReactJS? I was wondering whether removing the bookId altogether from the route and just push props like edit and bookId:
<Route exact path="/books/edit" component={BookEdit} />

I would recommend to do a conditionnal render in your BookEdit component (especially if you need to do an async operation to determine authorization).
I would not use a private route here in order to keep the role/auth based routing simple.
In your edit component : check authorization, if false handle this as an error and render an error component (error message and back button, can also be your 404 view), else render your edit component.
To be consistent, you must also make sure you do not have links to this error (conditionnal disabled "Edit" button on the book view if not authorized).
Example (using async check, may not be your case here but this is a general idea) :
class EditComponent extends React.Component {
state = {
loading: true,
error: null,
bookProps: null,
};
componentDidMount() {
const { match, userId } = this.props;
getBook(match.params.bookId) // request book props
.then(book => {
if (book.ownerId !== userId) { // authorization check
this.setState({ error: 'Unauthorized', loading: false });
} else {
this.setState({ bookProps: book, loading: false });
}
})
.catch(err => {
this.setState({ error: err.message, loading: false });
});
}
render() {
const { loading, error, bookProps } = this.state;
if (loading) {
return <LoadingSpinner />;
}
if (error) {
return <ErrorComponent message={error} />;
}
return <BookEditComponent book={bookProps} />;
}
}

Related

Problem with Firebase Auth while refrashing a page

I need your ideasssssssss.
renderIt = () => {
if(!this.state.user){
return <Login />
}else {
return <Home />
}
}
here I check is the user is logged in, if he is not logged in <Login /> is displayed, when he logs itself in, then <Home /> is displayed.
Problem is, when I go log out, and refresh, I see <Home/> page for a fraction of a second even if I am not logged in.
componentDidMount = () => {
this.authListener();
}
authListener = () => {
firebase
.auth()
.onAuthStateChanged((user) => user ? this.setState({user}) : this.setState({user: null}))
}
The problem is in your initial state, which is:
class App extends Component {
state = {
user: {}
}
...
Since you then check for this.state.user in the rendering code, you end up comparing (!{}) which is false.
I highly recommend keeping this code as simple as possible, following these rules:
this.state.user represents the current user object.
if there is no current user, this.state.user is null.
In code:
class App extends Component {
state = {
user: null
}
componentDidMount = () => {
this.authListener();
}
authListener = () => {
firebase
.auth()
.onAuthStateChanged((user) => this.setState({user}));
}
...
Also see the updated working code at: https://stackblitz.com/edit/react-1bv4bb?file=src/App.js
it's because !this.state.user true initially and until firebase auth finishes its work it will show you <Home/>.
The solution is to use a loading (some spinner animation) component initially until the firebase auth finished
if(this.state.user===null){
//create Loading component first before using it here
return <Loading />
}else if(!this.state.user){
return <Login />
}else {
return <Home />
}
This might not be a better way to handle user authentication, it's just to answer your question

How to use "<Redirect>" inside a function?

This is my function so that when a user logs in, it checks the user's password and username and if they are both correct it should redirect to the /note url with the user ID passed through it. I have seen people use it on render(), but I would like to keep my App.jsx app simple, I think that's good practice.
I have tried using the useHistory method, but when the user inputs a data, the user needs to refresh the page manually for the new data to show. If I add a useHistory to refresh the page, it says that the location.state value is undefined.
How should I go about this problem?
Thank you
// Checking to see if current user's log info exists in db
function logIn(e) {
e.preventDefault();
const currentUser = users.find(
(users) => users.username === userInput.username
);
if (!currentUser) {
console.log("The Username was not found. Please try again");
} else {
if (currentUser.password === userInput.password) {
// history.push("./note", { userId: currentUser._id });
return <Redirect to={"/note/" + currentUser._id} />;
} else {
console.log("The password is incorrect");
}
}
}
Here's my routing code
import React from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import NoteDashboard from "./NoteDashboard";
import Home from "./Home";
import CreateAccount from "./CreateAccount";
function App() {
return (
<Router>
<div className="container">
<Switch>
<Route exact path="/" component={Home}></Route>
<Route path="/create" component={CreateAccount}></Route>
<Route path="/note" component={NoteDashboard}></Route>
</Switch>
</div>
</Router>
);
}
export default App;
The only thing (I found) you can do to redirect to a page from function is use ReactDOM.hydrate. Here an example. Example works, but only first time. After that, redirection stops to works. Maybe because codesandbox concats to url the symbol # for some technical reason that I don't know. Try to apply this solution locally an let me know if works more tha one time.
Okay I got it to work, but I don't know if this is good practice or not.
Inside the if statement where I check to see that it is the correct user, I call a function which changes a React Hook from false to true, then in my return statement, I say
{redirect ? <Redirect to={"/note/" + currentUserId} /> : null}
It seems to work fine, everything works. Does this look like a good solution or are there some flaws that I am not seeing.
Thank you for all the help.
try something like this:
state = {
username: '',
app_pass: '',
redirectLink: '',
redirect: false
};
componentDidMount() {
if (window.localStorage.getItem('access_token')) {
this.setState({ redirectLink: '/user/dashboard', redirect: true });
}
}
render() {
const { redirect } = this.state;
const { redirectLink } = this.state;
if (redirect) {
return <Redirect to={redirectLink} />;
}
}

Handling redirects on User Validation in React?

I'm wondering how others manage return visitors who are already logged into their site (Proved with cookie token)
Currently, I'm using a redux action in the root component, which triggers an API call to check if the cookie is valid or not.
When this comes back as TRUE of FALSE the next action is dependant on the URL that a user is trying to visit.
Currently:
- If a user is validated and visits / they will be sent to /contacts
If a user is not validated and they visit any protected route (e.g. /contacts) the will be kicked out to /.
Issues:
- A validated user will momentarily see the login page if they visit / before being forwarded.
- If they visit another protect route such as /form they will be kicked over to /contacts
I know that both these items are a result of being temporarily not logged in while the API call validates the user.
Question
I'm wondering how you handle this in your application?
1. Avoid seeing the login page while validation is happening if user is logged in.
2. If user is logged in get them to the page they've requested?
// App.js Router
class App extends Component {
componentWillMount = async () => {
await this.props.checkLogin();
}
render() {
const { loggedIn } = this.props;
const myProtectedRoutes = [
{component: Form, path: "/form", exact: true },
{component: Contacts, path: "/contacts", exact: true },
{component: ContactCard, path: "/contacts/card/:clientID", exact: true },
]
return (
<BrowserRouter basename="/" >
<div>
<NavBar isLoggedIn={loggedIn} />
<main>
<Switch>
<Route exact path="/" component={Login}/>
<Route exact path="/logout" component={Logout }/>
{myProtectedRoutes.map(
(d, i) =>
<ProtectedRoute
key={i}
isAccessible={ loggedIn ? true : false }
exact
redirectToPath={"/"}
path={d.path}
component={d.component}
/>
)}
</Switch>
</main>
<footer>
<Switch>
<Route path="/" component={Footer}/>
</Switch>
</footer>
</div>
</BrowserRouter>
);
}
}
// LoginForm.js component => appears at '/'
class App extends Component {
constructor(props){
super(props);
this.state = {
emailaddress: '',
password: '',
}
}
onLoginSubmit = (e) => {
e.preventDefault();
const { emailaddress, password } = this.state;
if (emailaddress && password) {
this.props.doLogin(emailaddress, password, stayloggedin);
}
}
render() {
let { loggedIn } = this.props
// If logged in is true go to contacts or
// I'd like to be able to get to the typed
// URL if logged in is true.
if ( loggedIn ){
return <Redirect push to={"/contacts"} />
}
return (
<div id="mainloginform" className="container">
<!-- Login form Fields go here -->
</div>
);
}
}
In general, user validation and redirects should happen in the server.
In express, when the user requests any route (/ or /contacts), following things should happen
Check if the user is valid(using cookie token)
If valid, redirect the user to the requested route.
If invalid, redirect the user to /(using req.redirect('/'))
Hope this helps!
In redux it is a common pattern to make three actions.
Consider showing a "in_progress" UI during fetch?
{ type: 'FETCH_REQUEST' }
{ type: 'FETCH_FAILURE', error: 'Oops' }
{ type: 'FETCH_SUCCESS', response: { ... } }
Refer here for more details.

react-router: Not found (404) for dynamic content?

How can react-router properly handle 404 pages for dynamic content in a Universal app?
Let's say I want to display a user page with a route like '/user/:userId'. I would have a config like this:
<Route path="/">
<Route path="user/:userId" component={UserPage} />
<Route path="*" component={NotFound} status={404} />
</Route>
If I request /user/valid-user-id, I get the user page.
If I request /foo, I get a proper 404.
But what if I request /user/invalid-user-id. When fetching the data for the user, I will realize that this user does not exist. So, the correct thing to do seams to be:
Display the 404 page
Return a 404 http code (for server side
rendering)
Keep the url as is (I don't want a redirect)
How do I do that?? It seams like a very standard behaviour. I'm surprised not to find any example...
Edit:
Seams like I'm not the only one to struggle with it. Something like this would help a lot: https://github.com/ReactTraining/react-router/pull/3098
As my app won't go live any time soon, I decided to wait to see what the next react-router version has to offer...
First of create a middleware function for the onEnter callback, so that this is workable for redux promises:
import { Router, Route, browserHistory, createRoutes } from "react-router";
function mixStoreToRoutes(routes) {
return routes && routes.map(route => ({
...route,
childRoutes: mixStoreToRoutes(route.childRoutes),
onEnter: route.onEnter && function (props, replaceState, cb) {
route.onEnter(store.dispatch, props, replaceState)
.then(() => {
cb(null)
})
.catch(cb)
}
}));
}
const rawRoutes = <Route path="/">
<Route path="user/:userId" component={UserPage} onEnter={userResolve.fetchUser} />
<Route path="*" component={NotFound} status={404} />
</Route>
Now in this onEnter function you can work directly with the redux store. So you could dispatch an action that either successes or fails. Example:
function fetch(options) {
return (dispatch) => {
return new Promise((resolve, reject) => {
axios.get('<backend-url>')
.then(res => {
resolve(dispatch({type: `CLIENT_GET_SUCCESS`, payload: res.data}))
})
.catch(error => {
reject(dispatch({type: `CLIENT_GET_FAILED`, payload: error}));
})
}
})
}
}
let userResolve = {
fetchUser: (dispatch, props, replace) => {
return new Promise((next, reject) => {
dispatch(fetch({
user: props.params.user
}))
.then((data) => {
next()
})
.catch((error) => {
next()
})
})
}
}
Whenever the resolve promise now fails, react-router will automatically look for the next component that it could render for this endpoint, which in this case is the 404 component.
So you then wouldn't have to use replaceWith and your URL keeps retained.
If you are not using server side rendering, returning 404 before the page gets rendered would not be possible. You will need to check for the existence of the user somewhere either way (on the server or via AJAX on the client). The first would not be possible without server side rendering.
One viable approach would be to show the 404 page on error of the Promise.
I tried my solution in a project that I am making which uses Server Side Rendering and react-router and it works there, So I'll tell you what I did.
Create a function in which you'll validate an ID. If the ID is valid, Then return the with User page with proper Component, If the ID is invalid then return the with 404 Page.
See the example:
// Routes.jsx
function ValidateID(ID) {
if(ID === GOOD_ID) {
return (
<Route path="/">
<Route path="user/:userId" component={UserPage} />
<Route path="*" component={NotFound} status={404} />
</Route>
);
} else {
return (
<Route path="/">
<Route path="user/:userId" status={404} component={Page404} />
<Route path="*" component={NotFound} status={404} />
</Route>
);
}
// Router.jsx
<Router route={ValidateID(ID)} history={browserHistory}></Router>
This should work with Server Side rendering as it did in my project. It does not uses Redux.
In case of dynamic paths, you can do it like this and you don't have to change the current path.
just import the error404 component and define a property(notfound) in the state to use for conditioning.
import React, { Component } from 'react';
import axios from 'axios';
import Error404 from './Error404';
export default class Details extends Component {
constructor(props) {
super(props)
this.state = {
project: {}, notfound: false
}
}
componentDidMount() {
this.fetchDetails()
}
fetchDetails = () => {
let component = this;
let apiurl = `/restapi/projects/${this.props.match.params.id}`;
axios.get(apiurl).then(function (response) {
component.setState({ project: response.data })
}).catch(function (error) {
component.setState({ notfound: true })
})
}
render() {
let project = this.state.project;
return (
this.state.notfound ? <Error404 /> : (
<div>
{project.title}
</div>
)
)
}
}
I encountered a similar problem while making a blog website. I've been searching for a solution for a while now. I was mapping (using map function) my blog component based on dynamic link.
The initial code snippet was as follows:
import Blog from '../../Components/Blog/Blog.component';
import './BlogPage.styles.scss';
const BlogPage = ({ BlogData, match }) => {
return (
<div className='blog-page'>
{
BlogData.map((item, idx)=>
item.link === match.params.postId?
<Blog
key={idx}
title={item.title}
date={item.date}
image={item.image}
content={item.content}
match={match}
/>
:''
)
}
</div>
)
};
export default BlogPage;
I used a hack where I would use filter function instead of map and store it and then check if it exists (in this case check if length greater than zero for result) and if it does the blog component is rendered with the props for the page else I render the Not Found component (My404Component).
The snippet as follows:
import Blog from '../../Components/Blog/Blog.component';
import My404Component from '../../Components/My404C0mponent/My404Component.component';
import './BlogPage.styles.scss';
const BlogPage = ({ BlogData, match }) => {
const result = BlogData.filter(item => item.link === match.params.postId);
console.log(result);
return (
<div className={result.length>0? 'blog-page': ''}>
{
result.length>0?
<Blog
title={result[0].title}
date={result[0].date}
image={result[0].image}
content={result[0].content}
match={match}
/>
:<My404Component />
}
</div>
)
};
export default BlogPage;
This way the Blog component is not rendered as long as the value of the entered link is not valid as result would be an empty array and it's length would be 0 and instead My404Component would be rendered.
The code is a little raw I havn't refactored it yet.
Hope this helps.

Child route is blocking the parent route`s render

I have a react app which is using react-router. I`m using plain routes, but this is how components represent my routing
<Routes>
<Route component={CoreLayout}>
<Route component={AppLayout}
onEnter={fetchData}>
<Route path='dashboard'
onEnter={fetchStatistics}
component={Dashboard}>
</Route>
</Route>
</Routes>
The situation now
First, the app layout is going to block every render while it is fetching the necessary data from the server (like the User data). Then if we have the data, we can step on the child routes, like in this case the Dashboard route, where we are loading the content of the pages.
The goal
The problem is whit this strategy, we are going to show a blank white page until the onEnter on the main route is resolved.
To avoid this, I would like to load the AppLayout component, but without starting the onEnter function on the child route. To do this, I can show a waiting spinner where the child component would load, and when the data is loaded I can start loading the child`s data.
tl;dr
The question is, how can I make the parent layout to render, while the child route`s onEnter is not loaded.
Instead of using onEnter, you can have your <Dashboard> initiate its data fetching in its component(Will|Did)Mount method. Have it maintain a state.loaded boolean which displays a spinner when state.loaded = false.
class Dashboard extends React.Component {
constructor(props) {
super(props)
this.state = {
loaded: false
}
}
componentWillMount() {
// mock data fetch call that uses a promise
fetchStatistics()
.then(resp => resp.json())
.then(data => {
this.setState({
loaded: true,
data
})
})
}
render() {
// if data hasn't been loaded, render a spinner
if (!this.state.loaded) {
return <Spinner />
}
// data has been loaded, render the dashboard
return (
<div>...</div>
)
}
}
Edit:
It doesn't handle data loading errors, but here is an example of a general purpose data loading HOC that should work (haven't tested it):
/*
* #Component is the component to render
* #fetchData is a function which fetches the data. It takes
* a callback function to trigger once data fetching has
* completed.
*/
const withData = (Component, fetchData) => {
return class WithData extends React.Component {
constructor(props) {
super(props)
this.state = {
loaded: false
}
}
componentWillMount() {
this.props.fetchData(() => {
this.setState({ loaded: true })
})
}
render() {
return this.state.loaded ? (
<Component {...this.props} />
) : (
<Spinner />
)
}
}
}
Usage
function fetchStatistics(callback) {
fetch('/api/dashboard')
.then(resp => resp.json())
.then(data => {
dispatch(dashboardData(data))
callback()
})
})
<Route
path='dashboard'
component={withData(Dashboard, fetchStatistics} />

Resources