In the Registering a User challenge, you updated the register()
method in the AuthDAO
to create a new User node in the database.
There is still one # TODO
comment remaining in this file.
Currently, it is still possible to use the same email address twice, we should take advantage of Unique Constraints in the database to guard against that.
This functionality could be handled by checking the database before running the CREATE
Cypher statement, but this could still cause problems if the database is manually updated elsewhere.
Instead, you can pass the responsibility of handling the duplicate user error to the database by creating a Unique Constraint on the :User
label, asserting that the email
property must be unique.
This will create a potential error case that will need to be handled in the code.
To pass this challenge, you will need to:
Handling Constraint Errors
If we take a look at register()
method, it has been hardcoded to throw a new ValidationError
if the email address is anything other than graphacademy@neo4j.com
.
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
})
The code also has no explicit error handling.
Any errors will be sent back up the stack and will result in a 500 Bad Request
error.
Instead, this error should be caught and reformatted in such a way that the server would return a 422 Unprocessable Entity
error.
This way, the error can be better handled by the UI.
To do this, you will need to rearange the code into try
/catch
blocks.
try:
# Attempt the code inside the block
except ConstraintError:
# If a ConstraintError is thrown in the try block,
# then handle the error here
When a user tries to register with an email address that has already been taken, the database will throw an Neo.ClientError.Schema.ConstraintViolation
error, at which point the driver will raise a neo4j.exceptions.ConstraintError
.
Instead of this being treated as an internal server error, it should instead be treated as a 422 Unprocessable Entity
.
This will allow the front end to handle the error appropriately.
A ValidationError
class already exists in the codebase which is handled by a Flask middleware.
Completing the Challenge
To complete this challenge, you will first create a new constraint in your Sandbox database and modify the code to add a try/catch block.
Openapi/dao/auth.py
→
Create a Unique Constraint
In order to ensure that a property and label combination is unique, you run a CREATE CONSTRAINT
query.
In this case, we need to ensure that the email
property is unique across all nodes with a :User
label.
Click the Run in Sandbox button to create the constraint on your Sandbox.
CREATE CONSTRAINT UserEmailUnique
IF NOT EXISTS
FOR (user:User)
REQUIRE user.email IS UNIQUE;
Add a Try/Catch Block
In the method, we should:
-
Try to create a User with the supplied email, password and name.
-
Catch a
neo4j.exception.ConstraintError
error if it is thrown and instead throw anapi.exceptions.ValidationException
Your code should look like this:
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
})
Update the register()
method to reflect the changes above, then scroll to Testing to verify that the code works as expected.
Working 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/04_handle_constraint_errors__test.py
The test file is located at tests/04_handle_constraint_errors__test.py
.
Are you stuck? Click here for help
If you get stuck, you can see a working solution by checking out the 04-handle-constraint-errors
branch by running:
git checkout 04-handle-constraint-errors
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
If you have completed the steps in this challenge, a Unique Constraint will have been added to the database.
Click the Check Database button below to verify the constraint has been correctly created.
Hint
Try running the Cypher statement at Create a Unique Constraint and then click Check Database again.
Solution
If you haven’t already done so, run the following statement to create the constraint:
CREATE CONSTRAINT UserEmailUnique
IF NOT EXISTS
FOR (user:User)
REQUIRE user.email IS UNIQUE;
The unit test then attempts to create a user twice with a random email address, with the test passing if the ValidationException
error is thrown by the AuthDAO
.
Once you have run this statement, click Try again… to complete the challenge.
Lesson Summary
In this Challenge, you have modified the register()
function to catch specific errors thrown by the database.
If you wanted to go further, you could use a Regular Expression to extract more specific information about the ConstraintValidationFailed
error.
Now that a user is able to successfully register, in the next Challenge, you will update the authenticate()
method to find our user in the database.