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 register() method in the AuthDAO to do the following:
This process may start to get a little repetitive, but it worth repeating to get used to working with the Neo4j Python Driver.
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. 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 register() method of the AuthDAO at api/dao/auth.py. As we can see from the snippet below, at the moment it will only accept an email address of graphacademy@neo4j.com.
def register(self, email, plain_password, name):
encrypted = bcrypt.hashpw(plain_password.encode("utf8"), bcrypt.gensalt()).decode('utf8')
# TODO: Handle unique constraint error
if email != "graphacademy@neo4j.com":
raise ValidationException(
f"An account already exists with the email address {email}",
{"email": "An account already exists with this email"}
)
# Build a set of claims
payload = {
"userId": "00000000-0000-0000-0000-000000000000",
"email": email,
"name": name,
}
# Generate Token
payload["token"] = self._generate_token(payload)
return payloadFrom 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 further down within this class.
You will replace these TODO comments 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 execute_write() method on the session object with a function to represent unit of work.
Here are the steps to complete the challenge.
Open api/dao/auth.py
Create the Unit of Work function
Next, define a new function which will call the tx.run() method.
The call to tx.run() should include:
-
The Cypher statement as a parameterized string passed as the first argument.
-
The parameters used in the query passed as named parameters:
email,encryptedpassword andname. These will be referenced in the query prefixed with a dollar sign (eg.$email). -
The query will return a single row, so you can call the
.single()method directly on the object to immediately consume the first row.
def create_user(tx, email, encrypted, name):
return tx.run(""" // (1)
CREATE (u:User {
userId: randomUuid(),
email: $email,
password: $encrypted,
name: $name
})
RETURN u
""",
email=email, encrypted=encrypted, name=name # (2)
).single() # (3)Execute the function within a new Write Transaction
Next, open a new session and use the session.execute_write() method to execute this unit of work.
with self.driver.session() as session:
result = session.execute_write(create_user, email, encrypted, name)Return the Result
The call above 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 may be taken from the first row.
Use square brackets to extract the u node returned by the Cypher statement above.
The method expects a JWT token to be returned along with the user’s information.
The token can be generated by passing a selection of the user’s properties, including the id property, to the _generate_token method.
user = result['u']
payload = {
"userId": user["userId"],
"email": user["email"],
"name": user["name"],
}
payload["token"] = self._generate_token(payload)
return payloadWorking Solution
Click here to reveal the fully-implemented register() method.
def register(self, email, plain_password, name):
encrypted = bcrypt.hashpw(plain_password.encode("utf8"), bcrypt.gensalt()).decode('utf8')
def create_user(tx, email, encrypted, name):
return tx.run(""" // (1)
CREATE (u:User {
userId: randomUuid(),
email: $email,
password: $encrypted,
name: $name
})
RETURN u
""",
email=email, encrypted=encrypted, name=name # (2)
).single() # (3)
try:
with self.driver.session() as session:
result = session.execute_write(create_user, email, encrypted, name)
user = result['u']
payload = {
"userId": user["userId"],
"email": user["email"],
"name": user["name"],
}
payload["token"] = self._generate_token(payload)
return payload
except ConstraintError as err:
# Pass error details through to a ValidationException
raise ValidationException(err.message, {
"email": err.message
})Testing
To test that this functionality has been correctly implemented, run the following code in a new terminal session:
pytest tests/03_registering_a_user__test.pyThe test file is located at tests/03_registering_a_user__test.py.
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:
git checkout 03-registering-a-userYou 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@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.
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 shouldVerifySolution
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.
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 a TODO comment in this method for handling unique constraint violations in the database, so let’s learn about that in the next lesson.