Please participate in this survey to voice your opinion as a developer for an upcoming article!

In the first part of this tutorial, we went over how to set up a MongoDB Realm app with sample data, generate the schema, create and restrict roles, and then integrated it with a React app, implementing an authentication system.

In this tutorial, we'll go over how to ensure only logged-in users by email and password can add reviews, and we'll test adding reviews by users that are not logged in to see MongoDB Realm Roles and data access rules in action.

You can find the code for this tutorial here.


Add Reviews Form

We'll start with the add reviews form. This form will be accessed through a link in the card of the restaurant that's showing on the home page. The restaurant ID will be passed as a URL parameter, then whatever review the user enters will be saved to that restaurant. At first, we'll allow all users to access the page to test the difference between the logged-in user and anonymous user, then we'll restrict access to the page to logged-in users only.

Create the component src/pages/AddReview.js with the following content:

function AddReview() {
    
}

export default AddReview

Just like in the Authentication form, we'll use yup for validation and formik to make creating a form easier:

const reviewSchema = yup.object().shape({
    review: yup.number().required()
})

function AddReview() {
    const [loading, setLoading] = useState(false)
    
    function submitHandler (values) {
     	//TODO add review   
    }
    
    return (
        <Formik
            initialValues={{
                review: 0
            }}

            validationSchema={reviewSchema}
            onSubmit={submitHandler}>
            
            {({errors, touched, handleSubmit, values, handleChange}) => (
                <Form noValidate onSubmit={handleSubmit}>
                    {loading && <Loading />}
                    {!loading && (<div>
                        <h1>Submit Review</h1>
                        <Form.Row>
                            <Form.Label>Review Score</Form.Label>
                            <Form.Control type="number" name="review" value={values.review} onChange={handleChange} 
                            isValid={touched.review && !errors.review} />
                            <Form.Control.Feedback>{errors.review}</Form.Control.Feedback>
                        </Form.Row>
                        <div className="text-center mt-2">
                            <Button variant="primary" type="submit">Submit</Button>
                        </div>
                    </div>)}
                </Form>
            )}

        </Formik>
    )
}

We're just creating a form that has one number input for the review and for validation we're using the reviewSchema which just checks that the review is filled and is a number.

Next, we need to add the logic of adding the review to the restaurant by the logged-in user. To do this, first, we need to pass the mongoContext prop to the component which has the MongoDB client and the Realm app instances:

function AddReview({mongoContext: {client, app}}) {
	//...
}

Next, we'll get the id of the restaurant from the URL param using useParam:

const { id } = useParams()

And we'll get the history instance to use later to redirect back to home page:

const history = useHistory()

We can now add the logic to update the restaurant document of the passed id, adding the user's grade. To do that, we'll first get the restaurants collection from our sample_restaurants database:

function submitHandler(values){
    const rests = client.db('sample_restaurants').collection('restaurants')
}

Next, we'll use the method updateOne which takes a query to choose which document to update, then takes the changes. For us, the query will be the restaurant having the id that is passed as a URL param, and the change will be pushing a new entry into the grades array inside the restaurant document:

rests.updateOne({"_id": BSON.ObjectID(id)}, {"$push": {"grades": {
            date: new Date(),
            score: values.review,
            user_id: BSON.ObjectID(app.currentUser.id)
        }}}).then (() => history.push('/'))
            .catch ((err) => {
    			alert(err)
    			setLoading(false)
			})

Notice that:

  1. To query the _id field, we need to use BSON.ObjectID to correctly pass the object id. Make sure to add at the beginning of the file import { BSON } from 'realm-web'.
  2. the grades array holds objects that have date, score, and user_id. This way, we're linking the grade to the appropriate user.
  3. updateOne returns a promise, so once it resolves we're redirecting to the home page using history.push('/').

And with that, our AddReview component is ready. Next, we need to add the new page in our routes in src/App.js:

return (
    <Router>
      <Navigation user={user} />
      <MongoContext.Provider value={{app, client, user, setClient, setUser, setApp}}>
        <Container>
          <Switch>
            <Route path="/signup" render={() => renderComponent(Authentication, {type: 'create'})} />
            <Route path="/signin" render={() => renderComponent(Authentication)} />
            <Route path="/logout" render={() => renderComponent(LogOut)} />
            <Route path="/review/:id" render={() => renderComponent(AddReview)} />
            <Route path="/" render={() => renderComponent(Home)} />
          </Switch>
        </Container>
      </MongoContext.Provider>
    </Router>
  );

Then, we'll need to add the link to the page inside each restaurant card. To do that, edit the src/components/RestaurantCard.js component's return statement:

return (
        <Card className="m-3">
            <Card.Body>
                <Card.Title>{restaurant.name} <Badge variant="warning">{avg}</Badge></Card.Title>
                <Link to={`/review/${restaurant._id}`} className="card-link">Add Review</Link>
            </Card.Body>
        </Card>
    )

Notice that we're passing the restaurant id to the link as a parameter.

Let's run the server now:

npm start

Make sure to log in if you aren't already. We'll test how this will work as a guest in a bit.

You should be able to see new links for each restaurant on the home page now.

Click on "Add Review" for any of the restaurants. You'll see a number input field, enter any number and click "Submit". If you're logged in, you should see a loader, then you'll be redirected to the home page. You can see that the restaurant's review has changed.

Test MongoDB Realm Authorization Roles

If you recall from part 1, we added a new User role. This user role allows users that have an email to insert or update only the grades field of a restaurant. For a user to "belong" to the User role they need to have an email, which we declared in the "Apply When" field:

{
  "%%user.data.email": {
    "%exists": true
  }
}

So, an anonymous user does not have permission to make any changes to the grades field or any field of the restaurants collection.

Let's test that. Log out from the current user. You should still be able to see the Add Review link and be able to access the page, as we still haven't added conditions for a user's authentication.

Try to add a review to any restaurant. Since you're not logged in, you'll get an alert with an error and nothing will be added.

As you can see the error says "update not permitted". The user does not belong to the "User" role we created, so they're not allowed to add a review.

Let's now hide the link to "Add Review" for anonymous users in src/components/RestaurantCard.js:

{!isAnon(user) && <Link to={`/review/${restaurant._id}`} className="card-link">Add Review</Link>}

Add user to the list of props for RestaurantCard:

function RestaurantCard ({restaurant, user}) {
	//...
}

Pass the user prop to RestaurantCard in src/pages/Home.js:

<RestaurantCard key={restaurant._id} restaurant={restaurant} user={user} />

And let's add a condition in src/pages/AddReview.js to redirect to home page if the user is not logged in:

function AddReview({mongoContext: {client, app, user}}) {
    const [loading, setLoading] = useState(false)
    const { id } = useParams()
    const history = useHistory()

    if (isAnon(user)) {
        history.push('/')
    }
    //...
}

Now, if you're not logged in you will not be able to see the review and if you try to access the reviews page directly you'll be redirected to the home page.

Let's test another aspect of the role we created. As we said, the role we created allows logged-in users to update the grades field. However, they should not be able to edit any other field.

Let's change the parameter for updateOne in AddReview to change the name instead:

rests.updateOne({"_id": BSON.ObjectID(id)}, {"name": "test"})

This is just to easily test this restriction. Now, login and go to the "Add Review" and click Submit. You'll see the same "update not permitted" message as before.

This shows how we can easily manage our users, their roles, and data access through MongoDB Realm.


Conclusion

Using MongoDB Realm allows us to easily create serverless apps while also managing data access, roles, and authentication. It's also available to be used on the web (like in this tutorial), on mobile apps, and more. In this tutorial, we covered the basics that you'll probably need in most use cases. If you dive deeper into it, you'll surely find even more features that will be helpful for your serverless apps.