Registering a User

Now that you have read data from the database, you are now ready to write data to the database.

In this challenge, you will rewrite the RegisterAsync() method in the AuthService to do the following:

Registering Users

A key piece of functionality that the application should provide is for new users to be able to register themselves with the site. This functionality is already built into the front end, but at the moment the credentials are hard coded in the API using a fixture. This might be fine for demo purposes, but limiting the number of users to one is bad for Neoflix’s bottom line.

The dummy register logic is already written into the RegisterAsync() method of the AuthService in Neoflix/Services/AuthService.cs. As we can see from the snippet below, at the moment, it loads the fixture from user.json and will only accept an email address of graphacademy@neo4j.com.

You also see that the password is hashed/encrypted using BCryptNet.HashPassword(), we will use that later.

c#
Neoflix/Services/AuthService.cs
public async Task<Dictionary<string, object>> RegisterAsync(string email, string plainPassword, string name)
{
    var rounds = Config.UnpackPasswordConfig();
    var encrypted = BCryptNet.HashPassword(plainPassword, rounds);
    // TODO: Handle Unique constraints in the database
    if (email != "graphacademy@neo4j.com")
        throw new ValidationException($"An account already exists with the email address", email);
    
    // TODO: Save user
    var exampleUser = new Dictionary<string, object>
    {
        ["identity"] = 1,
        ["properties"] = new Dictionary<string, object>
        {
            ["userId"] = 1,
            ["email"] = "graphacademy@neo4j.com",
            ["name"] = "Graph Academy"
        }
    };

    var safeProperties = SafeProperties(exampleUser["properties"] as Dictionary<string, object>);
    safeProperties.Add("token", JwtHelper.CreateToken(GetUserClaims(safeProperties)));

    return safeProperties;
}

From the last line, you can see that an additional token property is added to the return. This represents the JWT token required to authenticate the user on any future requests. This token is generated in JwtHelper.CreateToken() which is provided for us.

You will replace these TODO comment with working code to complete the challenge.

Implementing Write Transactions

You will follow similar steps to the previous challenge, with the one change that the Cypher statement will be executed within a Write Transaction.

To do so, you will need to call the ExecuteWriteAsync() method on the session object with a function to represent unit of work.

Here are the steps to complete the challenge.

Open a new Session

First, open a new session (preferably in a try block):

c#
// Open a new session
using var session = _driver.AsyncSession();
// Do something with the session...

Execute a Cypher statement within a new Write Transaction

Next, within that session, run the ExecuteWriteAsync() method with two arguments:

  1. The Cypher statement as a parameterized string

  2. An object containing the names and values for the parameters

You will need to pass three parameters to the query:

  • email the user’s email

  • encrypted an encrypted version of the password provided

  • name the users’s name

The user’s userId is generated via the randomUuid() in Cypher.

c#
var query = @"
    CREATE (u:User {
        userId: randomUuid(),
        email: $email,
        password: $encrypted,
        name: $name
    })
    RETURN u { .userId, .name, .email } as u";
var cursor = await tx.RunAsync(query, new {email, encrypted, name});

Extract the User from the Result

The Cypher statement above returns the newly-created :User node as u.

As this query creates a single node, it will only ever return one result, so the u value is can be extracted by calling the SingleAsync() method. That method will fail with an error, if zero, or more than one row returned.

c#
var record = await cursor.SingleAsync();
// Extract safe properties from the user node (`u`) in the first row
return record["u"].As<Dictionary<string, object>>();

Return the Results

The return statement has already been written, so this can be left as it is.

c#
return safeProperties;

Working Solution

Click here to reveal the fully-implemented RegisterAsync() method.
c#
public async Task<Dictionary<string, object>> RegisterAsync(string email, string plainPassword, string name)
{
    var rounds = Config.UnpackPasswordConfig();
    var encrypted = BCryptNet.HashPassword(plainPassword, rounds);

    await using var session = _driver.AsyncSession();

    var user = await session.ExecuteWriteAsync(async tx =>
    {
        var query = @"
            CREATE (u:User {
                userId: randomUuid(),
                email: $email,
                password: $encrypted,
                name: $name
            })
            RETURN u { .userId, .name, .email } as u";
        var cursor = await tx.RunAsync(query, new {email, encrypted, name});

        var record = await cursor.SingleAsync();
        // Extract safe properties from the user node (`u`) in the first row
        return record["u"].As<Dictionary<string, object>>();
    });

    var safeProperties = SafeProperties(user);
    safeProperties.Add("token", JwtHelper.CreateToken(GetUserClaims(safeProperties)));

    return safeProperties;
}

Testing

To test that this functionality has been correctly implemented, run the following code in a new terminal session:

sh
Running the test
dotnet test --logger "console;verbosity=detailed" --filter "Neoflix.Challenges._03_RegisterUsers"

The test file is located at Neoflix.Challenges/_03_RegisterUsers.cs.

Are you stuck? Click here for help

If you get stuck, you can see a working solution by checking out the 03-registering-a-user branch by running:

sh
Check out the 03-registering-a-user branch
git checkout 03-registering-a-user

You may have to commit or stash your changes before checking out this branch. You can also click here to expand the Support pane.

Verifying the Test

Here is where things get interesting.

If you have completed the course to this point, you should have a project that connects to the Neo4j Sandbox instance.

If the test above has succeeded, there should be a :User node in the sandbox with the email address graphacademy.register@neo4j.com, name Graph Academy, and an encrypted password.

Hint

You can run the following query to check for the user within the database. If the shouldVerify value returns true, the verification should be successful.

cypher
MATCH (u:User {email: 'graphacademy@neo4j.com'})
RETURN u.email, u.name, u.password,
    (u.email = 'graphacademy@neo4j.com' AND u.name = 'Graph Academy' AND u.password <> 'letmein') AS shouldVerify

Solution

The following statement will mimic the behaviour of the test, merging a new :User node with the email address graphacademy@neo4j.com and assigning a random UUID value to the .userId property.

cypher
MERGE (u:User {email: "graphacademy@neo4j.com"})
SET u.userId = randomUuid(),
    u.createdAt = datetime(),
    u.authenticatedAt = datetime()

Once you have run this statement, click Try again…​* to complete the challenge.

Lesson Summary

In this Challenge, you wrote the code to create a new User node to Neo4j.

We still have TODO comments in the query for handling unique constraint violations in the database, so let’s learn about that in the next lesson.