Adding Users to the Node.js / React.js Neo4j Movie App


Introduction


The Neo4j Movie App Template provides an easy-to-use foundation for your next Neo4j project or experiment using either Node.js or React.js. This article will walk through the creation of users that can log in and interact with the web app’s data.

In the Neo4j Movie App Template example, these users will be able to log in and out, rate movies, and receive movie recommendations.

The User Model


Aside from creating themselves and authenticating with the app, Users (blue) can rate Movies (yellow) with the :RATED relationship, illustrated in the graph data model below.

Learn how to add users to the Node.js / React.js example Neo4j Movie App


User Properties

    • password: The hashed version of the user’s chosen password
    • api_key: The user’s API key, which the user uses to authenticate requests
    • id: The user’s unique ID
    • username: The user’s chosen username
:RATED Properties

rating: an integer rating between 1 and 5, with 5 being love it and 1 being hate it.

My Rated Movie in the Neo4j Movie App


Users Can Create Accounts


Before a User can rate a Movie, the the user has to exist – someone has to sign up for an account. Signup will create a node in the database with a User label along with properties necessary for logging in and maintaining a session.

Create a new user account in the Neo4j Movie App

Figure 1. web/src/pages/Signup.jsx

The registration endpoint is located at /api/v0/register. The app submits a request to the register endpoint when a user fills out the “Create an Account” form and taps “Create Account”.

Assuming you have the API running, you can test requests either by using the interactive docs at 3000/docs/, or by using cURL.

Use Case: Create New User


Request

curl -X POST 
--header 'Content-Type: application/json' 
--header 'Accept: application/json' 
-d '{ "username": "Mary Jane", "password": "SuperPassword"}' 'https://localhost:3000/api/v0/register'

Response

{
   "id":"e1e157a2-1fb5-416a-b819-eb75c480dfc6",
   "username":"Mary333 Jane",
   "avatar":{
      "full_size":"https://www.gravatar.com/avatar/b2a02b21db2222c472fc23ff78804687?d=retro"
   }
}

Use Case: Try to Create New User but Username Is Already Taken


Request

curl -X POST 
--header 'Content-Type: application/json' 
--header 'Accept: application/json' 
-d '{ "username": "Mary Jane", "password": "SuperPassword"}' 'https://localhost:3000/api/v0/register'

Response

{
   "username":"username already in use"
}

User registration logic is implemented in /api/models/users.js. Here’s the JavaScript:

var register = function(session, username, password) {
    return session.run('MATCH (user:User {username: {username}}) RETURN user', {
            username: username
        })
        .then(results => {
            if (!_.isEmpty(results.records)) {
                throw {
                    username: 'username already in use',
                    status: 400
                }
            }
            else {
                return session.run('CREATE (user:User {id: {id}, username: {username},
                       password: {password}, api_key: {api_key}}) RETURN user', {
                    id: uuid.v4(),
                    username: username,
                    password: hashPassword(username, password),
                    api_key: randomstring.generate({
                        length: 20,
                        charset: 'hex'
                    })
                }).then(results => {
                    return new User(results.records[0].get('user'));
                })
            }
        });
};

Users Can Log in


Now that users are able to register for an account, we can define the view that allows them to login to the site and start a session.



User login on the Neo4j Movies AppFigure 2. /web/src/pages/Login.jsx

The registration endpoint is located at /api/v0/login. The app submits a request to the login endpoint when a user fills a username and password and taps “Create Account”.

Assuming you have the API running, you can test requests either by using the interactive docs at 3000/docs/, or by using cURL.

Use Case: Login


Request

curl -X POST 
--header 'Content-Type: application/json' 
--header 'Accept: application/json' 
-d '{"username": "Mary Jane", "password": "SuperPassword"}' 'https://localhost:3000/api/v0/login'

Response

{
	"token":"5a85862fb28a316ea6a1"
}

Use Case: Wrong Password


Request

curl -X POST 
--header 'Content-Type: application/json' 
--header 'Accept: application/json' 
-d '{ "username": "Mary Jane", "password": "SuperPassword"}' 'https://localhost:3000/api/v0/register'

Response

{
   "username":"username already in use"
}

Use Case: See Myself


Request

curl -X GET 
--header 'Accept: application/json' 
--header 'Authorization: Token 5a85862fb28a316ea6a1' 'https://localhost:3000/api/v0/users/me'

Response

{
  "id": "94a604f7-3eab-4f28-88ab-12704c228936",
  "username": "Mary Jane",
  "avatar": {
    "full_size": "https://www.gravatar.com/avatar/c2eab5611cabda1c87463d7d24d98026?d=retro"
  }
}

You can take a look at the implementation in /api/models/users.js:

var me = function(session, apiKey) {
    return session.run('MATCH (user:User {api_key: {api_key}}) RETURN user', {
            api_key: apiKey
        })
        .then(results => {
            if (_.isEmpty(results.records)) {
                throw {
                    message: 'invalid authorization key',
                    status: 401
                };
            }
            return new User(results.records[0].get('user'));
        });
};
var login = function(session, username, password) {
    return session.run('MATCH (user:User {username: {username}}) RETURN user', {
            username: username
        })
        .then(results => {
            if (_.isEmpty(results.records)) {
                throw {
                    username: 'username does not exist',
                    status: 400
                }
            }
            else {
                var dbUser = _.get(results.records[0].get('user'), 'properties');
                if (dbUser.password != hashPassword(username, password)) {
                    throw {
                        password: 'wrong password',
                        status: 400
                    }
                }
                return {
                    token: _.get(dbUser, 'api_key')
                };
            }
        });
};

The code here should look similar to /register. There is a similar form to fill out, where a user types in their username and password.

With the given username, a User is initialized. The password they filled out in the form is verified against the hashed password that was retrieved from the corresponding :User node in the database.

If the verification is successful it will return a token. The user is then directed to an authentication page, from which they can navigate through the app, view their user profile and rate movies. Below is a rather empty user profile for a freshly created user:

An empty user profile in the Neo4j Movies App

Figure 3. /web/src/pages/Profile.jsx

Users Can Rate Movies


Once a user has logged in and navigated to a page that displays movies, the user can select a star rating for the movie or remove the rating of a movie he or she has already rated.

My Rated Movie in the Neo4j Movie App


The user should be able to access their previous ratings (and the movies that were rated) both on their user profile and the movie detail page in question.

Use Case: Rate a Movie


Request

curl -X POST 
--header 'Content-Type: application/json' 
--header 'Accept: application/json' 
--header 'Authorization: Token 5a85862fb28a316ea6a1' 
-d '{"rating":4}' 'https://localhost:3000/api/v0/movies/683/rate'

Response

{}

Use Case: See All of My Ratings


Request

curl -X GET 
--header 'Accept: application/json' 
--header 'Authorization: Token 5a85862fb28a316ea6a1' 'https://localhost:3000/api/v0/movies/rated'

Response

[
  {
    "summary": "Six months after the events depicted in The Matrix, ...",
    "duration": 138,
    "rated": "R",
    "tagline": "Free your mind.",
    "id": 28,
    "title": "The Matrix Reloaded",
    "poster_image": "https://image.tmdb.org/t/p/w185/ezIurBz2fdUc68d98Fp9dRf5ihv.jpg",
    "my_rating": 4
  },
  {
    "summary": "Thomas A. Anderson is a man living two lives....",
    "duration": 136,
    "rated": "R",
    "tagline": "Welcome to the Real World.",
    "id": 1,
    "title": "The Matrix",
    "poster_image": "https://image.tmdb.org/t/p/w185/gynBNzwyaHKtXqlEKKLioNkjKgN.jpg",
    "my_rating": 4
  }
]

Use Case: See My Rating on a Particular Movie


Request

curl -X GET 
--header 'Accept: application/json' 
--header 'Authorization: Token 5a85862fb28a316ea6a1' 'https://localhost:3000/api/v0/movies/1'

Response

{
   "summary":"Thomas A. Anderson is a man living two lives....",
   "duration":136,
   "rated":"R",
   "tagline":"Welcome to the Real World.",
   "id":1,
   "title":"The Matrix",
"poster_image":"https://image.tmdb.org/t/p/w185/gynBNzwyaHKtXqlEKKLioNkjKgN.jpg",
   "my_rating":4,
   "directors":[...],
   "genres":[...],
   "producers":[...],
   "writers":[...],
   "actors":[...],
   "related":[...],
   "keywords":[...]
}

Users Can Be Recommended Movies Based on Their Recommendations


When a user visits their own profile, the user will see movie recommendations. There are many ways to build a recommendation engine, and you might want to use one or a combination of the methods below to build the appropriate recommendation system for your particular use case.

In the movie template, you can find the recommendation endpoint at movies/recommended.

User-Centric, User-Based Recommendations

Here’s an example Cypher query for a user-centric recommendation:

MATCH (me:User {username:'Sherman'})-[my:RATED]->(m:Movie)
MATCH (other:User)-[their:RATED]->(m)
WHERE me <> other
AND abs(my.rating - their.rating) < 2
WITH other,m
MATCH (other)-[otherRating:RATED]->(movie:Movie)
WHERE movie <> m
WITH avg(otherRating.rating) AS avgRating, movie
RETURN movie
ORDER BY avgRating desc
LIMIT 25

Movie-Centric, Keyword-Based Recommendations

Newer movies will have few or no ratings, so they will never be recommended to users if the application uses users’ rating-based recommendations.

Since movies have keywords, the application can recommend movies with similar keywords for a particular movie. This case is useful when the user has made few or no ratings.

For example, site visitors interested in movies like Elysium will likely be interested in movies with similar keywords as Elysium.

MATCH (m:Movie {title:'Elysium'})
MATCH (m)-[:HAS_KEYWORD]->(k:Keyword)
MATCH (movie:Movie)-[r:HAS_KEYWORD]->(k)
WHERE m <> movie
WITH movie, count(DISTINCT r) AS commonKeywords
RETURN movie
ORDER BY commonKeywords DESC
LIMIT 25

User-Centric, Keyword-Based Recommendations

Users with established tastes may be interested in finding movies with similar characteristics as his or her highly-rated movies, while not necessarily caring about whether another user has or hasn’t already rated the movie. For example, Sherman has seen many movies and is looking for new movies similar to the ones he has already watched.

MATCH (u:User {username:'Sherman'})-[:RATED]->(m:Movie)
MATCH (m)-[:HAS_KEYWORD]->(k:Keyword)
MATCH (movie:Movie)-[r:HAS_KEYWORD]->(k)
WHERE m <> movie
WITH movie, count(DISTINCT r) AS commonKeywords
RETURN movie
ORDER BY commonKeywords DESC
LIMIT 25

Next Steps




Want to learn more about what you can do with graph databases like Neo4j?
Click below to get your free copy the O’Reilly Graph Databases book and discover how to harness the power of graph technology.


Download My Free Copy