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.
User
Properties
password
: The hashed version of the user’s chosen passwordapi_key
: The user’s API key, which the user uses to authenticate requestsid
: The user’s unique IDusername
: 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.
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.
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.
Figure 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:
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.
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
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