Before we get to today’s schemas and endpoints, we need to talk about validation. I spent some time researching API validation and ultimately took most of my inspiration from this article by Miguel Grinberg. My passwords are hashed with a custom_app_context object, which is done in a class function.
Additionally, I created methods for creating and authenticating a token, as recommended by the article. This way the user’s credentials are not sent back and forth with every API request.
I imported the flask_httpauth package and initialized an HTTPBasicAuth object in my __init__.py folder. Then I created the verify_password function.
This function first tries to authenticate by token, and then by username and password if token authentication fails. It’s a pretty straight rewrite of Miguel Grinberg’s example, with one difference: I added a needs_valid_email check to determine if the user had validated their email. This was enabled on default, but I needed an option to disable it for specific instances, which I’ll detail later in this post. The user.email_validated variable is a boolean that I added to the User model.
With the authentication written, it was time to define my schema.
Again, this schema is extremely simple and relies on the SQLAlchemyAutoSchema class to generate. The only piece I added on was a post_load call that checked if the user was in the system, and returned that specific user if so. If not, it reaturned a new user. I queried the users using the email attribute, as it is guaranteed to be present (unlike the id and also unique).
Additionally, I added a second schema, specifically for the creation of new Users. I chose to do it this way because I wanted to store the logic of hashing the password in the schema, rather than doing so in the actual route. This also allowed me to validate a “password” field to ensure that the user’s password was up to security standards before hashing. I used a post_load decorator to hash the password and a validates decorator to validate the password.
Also note that access_level defaults to 0 right now. I’m still working out the best way to structure user levels of access, but tentatively I’m thinking that 2 is an admin (and would only use my specific account), whereas 0 and 1 are either guest accounts or unverified emails. Frankly, I’m not entirely wedded to the three-tiered system, as you’ll see later in this post.
The Endpoints
The endpoints follow REST standards: there is a “GET” for both collections and single users, a “POST” for new users, a “DELETE” for single users, and a “PUT” for changing user information, such as password and email.
The two “GET” routes and the “DELETE” route are the simplest, and are extremely similar to my other resource routes of the same type:
My “POST” request for new users makes use of the CreateUserSchema, and I catch any ValidationErrors to send back to the client as a payload.
My “PUT” request went through several iterations before I realized that I wasn’t complying with proper RESTful standards by allowing the user to change only part of the data. I rewrote it so that all of the information about the user must be provided for every “PUT” request. In order to make sure that the new information was valid, I reused my CreateUserSchema, which validated the password for me and checked that all information was there. It then returns a new user, and I transfer the data from that user to my old user (to preserve the ID).
In addition to the basic endpoints, I also implemented three additional ones for verifying email and resources. The first simply generates a token using the method that I created in my Users model.
The next one sends an email to the user’s provided email account in order to validate it. This one was a bit tricker for several reasons. One, my auth.login_required function checked if a user’s email account was verified and denied permission if it was not. Therefore, I couldn’t use it to validate the user’s credentials. I solved this by manually verifying in the function, using my verify_password funciton with the needs_valid_email setting set to false.
The second issue was the route. In my previous example, the email verification sent a link to the email that, when clicked on, redirected to the app and valided the email of the user in the provided token. However, I couldn’t do that this time, because I did not want any user interaction with the backend.
My solution was partial, and won’t be complete until I can work on the frontend. Essentially, the /users/verification route requires a url argument, in addition to login credentials. This will be the route to the frontend app. The backend sends the email to the user, which directs the user to the frontend page. The frontend then informs the backend that the user is successfully verified.
It’s a bit confusing, but hopefully when I get the frontend set up, I’ll be able to revisit this and explain the process in a bit more depth.
And those are my endpoints. Before I was finished with the user implementation, however, there were a few things I needed to change in the other resources. The addition of Users meant that I needed permissions on who got to edit GroceryLists and Recipes.
New Permissions
The first thing I did was make a slight modification to how my Recipes and GroceryLists were stored. I implemented a creator_id field for both of them, which would store the id of the User who created them. Recipes would only be allowed to be modified by their creator, although anyone can use one in a GroceryList. GroceryLists will have the ability to have multiple collaborators, but I still wanted to establish the creator as a separate field. Thus, the association table became one of “editors” rather than “users.”
This required some updating of my endpoints. For my Recipes, the changes were minor: I simply added a login_required decorator in front of the “DELETE” and “POST” methods. For the “POST” method, the user’s id was added to the json data before it was passed into the RecipeSchema. For the “DELETE” method, the user’s login information is compared with the creator’s ID (to ensure that only the creator can delete a recipe). If they don’t match, then a 401 response is given.
For my GroceryList endpoints, things were a bit more complicated. The easy change was to the “POST” and “DELETE” methods, and were essentially the same as the new “POST” method for the recipe. For my “PUT” method, however, I needed to check both the creator and the list of editors. I also rewrote the method so that it replaced the entire contents of the list, rather than modifying it as it had before.
Finally, I also needed a new route that would allow the creator of the list to modify who could edit it. This route takes a list of users and verifies them before replacing the old list of editors.
Conclusions
And that’s it! There are still a few areas that I need to cover, such as adding in filters on my GroceryList, as well as integrating spaCy and my web scrapers. I would also like to do some refactoring, since code is repeated in several places and that’s a no-go. But the bulk of my functionality is in place now for the backend, and I’m really excited to begin work on the front. I’ve been teaching myself React in the mornings since I started this new version, and I’m really excited to bring a ton of new functionality to this thing.