API for managing your cooking recipes.
Live Heroku deployment: https://fsnd-recipehub.herokuapp.com/recipes
This is my capstone project submission for the Udacity Full-Stack Developer Nanodegree program.
The goal is to demonstrate the ability to:
- Design data models and their relations using SQLAlchemy.
- Write database queries using SQLAlchemy.
- Design an HTTP API with Flask.
- Document the API and development guide in detail.
- Implement authentication and Role Based Access Control using Auth0.
- Test the API and access control capabilities.
- Provide PEP8 compliant, and readable code.
- Deploy the app to Heroku.
The Heroku deployment requires authentication to start using. There are two access levels: ReadOnly and Admin.
For testing the live deployment, a Postman collection with access tokens is provided for convenience (fsnd-recipehub.postman_collection.json
).
For automated tests, please see the Testing section.
For authentication details, please see the Authentication and Permissions section later in the document.
Install dependencies:
pip install -r requirements.txt
Start the app locally:
source setup.sh
flask run --reload
To update your Heroku deployment with latest changes:
git push heroku master
To deploy the app to Heroku from scratch, keep reading.
First, you need a free Heroku account and have the heroku client installed. Please see the official docs on how to do these.
heroku create <app_name>
When successful you should have a heroku
remote in your repo with address:
https://git.heroku.com/<app_name>.git
heroku addons:create heroku-postgresql:hobby-dev --app <app_name>
This will setup a database and make its connection string available as DATABASE_URL
environment variable.
You can set your app's config variables from the web dashboard's app settings.
You can also do it from the command line. For example:
heroku config:set AUTH0_DOMAIN="mydomain.auth0.com"
Check your app's current config with:
heroku config
There is a DISABLE_AUTH0
environment variable that's used in testing.
The main reasons to disable authentication are to avoid spamming Auth0 with our test requests, and keep the main functionality tests shorter.
The app functionality tests are in test_app.py
. This test module uses DISABLE_AUTH0=1
setting.
source setup.sh
pytest test_app.py
The access control tests make requests to Auth0, and should be run every so often. This test module unsets DISABLE_AUTH0
variable.
source setup.sh
pytest test_rbac.py
The source follows PEP8. Please use pycodestyle
for guidance:
pycodestyle --exclude=env .
The only public endpoint, for debugging. Returns: "Healthy"
- Returns the list of recipes.
- Required headers:
Authorization
header with bearer token that hasread:recipes
permission.
- Request arguments: None
- Returns:
200 OK
response, body with aresult
key, its value being the list of recipes.
Example response:
{
"result": [
{
"id": 1,
"name": "Pizza",
"procedure": "Est qui alias molestias facilis et et eum. Ducimus est corrupti et qui. Et quidem nostrum qui ipsum perspiciatis et enim. Odio impedit et unde voluptatem.",
"time": 30,
"ingredients": [
{
"id": 1,
"recipe_id": 1,
"name": "Flour",
"optional": false,
"measurement": 250,
"measurement_unit": "grams"
}
],
}
]
}
- Returns a single recipe.
- Required headers:
Authorization
header with bearer token that hasread:recipes
permission.
- Request path arguments:
recipe_id
- Returns:
200 OK
response, body with aresult
key, value being the recipe object.404 Not Found
response when an unknown recipe ID was provided.
Example response:
{
"result": {
"id": 1,
"name": "Pizza",
"procedure": "Est qui alias molestias facilis et et eum. Ducimus est corrupti et qui. Et quidem nostrum qui ipsum perspiciatis et enim. Odio impedit et unde voluptatem.",
"time": 30,
"ingredients": []
}
}
- Adds a new recipe.
- Required headers:
Authorization
header with bearer token that hascreate:recipes
permission.
- Request body:
name
: Recipe name stringprocedure
: Recipe instruction stringingredients
: List ofIngredient
objectstime
: Time to cook minutes, integer
- Returns:
200 OK
response when a new record was successfully created.400 Bad Request
response when any of the fields are missing.
Example response:
{
"success": true,
"result": {
"id": 1,
"name": "Pizza",
"procedure": "Est qui alias molestias facilis et et eum. Ducimus est corrupti et qui. Et quidem nostrum qui ipsum perspiciatis et enim. Odio impedit et unde voluptatem.",
"time": 30,
"ingredients": []
}
}
- Updates a recipe.
- Required headers:
Authorization
header with bearer token that hasupdate:recipes
permission.
- Request path argument:
recipe_id
- Request body (can be a subset of):
name
: Recipe name stringprocedure
: Recipe instruction stringingredients
: List ofIngredient
objectstime
: Time to cook minutes, integer
- Returns:
200 OK
response when a new record was successfully created.400 Bad Request
response when provided fields are invalid.404 Not Found
response when an unknown recipe ID was provided.
Example response:
{
"success": true,
"result": {
"id": 1,
"name": "Pizza",
"procedure": "Est qui alias molestias facilis et et eum. Ducimus est corrupti et qui. Et quidem nostrum qui ipsum perspiciatis et enim. Odio impedit et unde voluptatem.",
"time": 30,
"ingredients": []
}
}
- Deletes a recipe.
- Required headers:
Authorization
header with bearer token that hasdelete:recipes
permission.
- Request path argument:
recipe_id
- Returns:
200 OK
response when a new record was successfully created.404 Not Found
response when an unknown recipe ID was provided.
Example response:
{
"success": true,
"recipe_id": 1
}
- Returns the list of ingredients.
- Required headers:
Authorization
header with bearer token that hasread:recipes
permission.
- Request arguments: None
- Returns:
200 OK
response, body with aresult
key, value being the list of ingredients.
Example response:
{
"result": [
{
"id": 1,
"recipe_id": 1,
"name": "Flour",
"optional": false,
"measurement": 250,
"measurement_unit": "grams"
}
]
}
- Returns a single ingredient.
- Required headers:
Authorization
header with bearer token that hasread:recipes
permission.
- Request path arguments:
item_id
- Returns:
200 OK
response, body with aresult
key, value being the ingredient object.404 Not Found
response when an unknown ingredient ID was provided.
Example response:
{
"result": {
"id": 1,
"recipe_id": 1,
"name": "Flour",
"optional": false,
"measurement": 250,
"measurement_unit": "grams"
}
}
- Adds a new ingredient.
- Required headers:
Authorization
header with bearer token that hascreate:recipes
permission.
- Request body:
recipe_id
: Related recipe IDname
: Ingredient nameoptional
: Whether the ingredient is optional, booleanmeasurement
: Measurement value, integermeasurement_unit
: Measurement unit name
- Returns:
200 OK
response when a new record was successfully created.400 Bad Request
response when provided fields were invalid.
Example response:
{
"success": true,
"result": {
"id": 1,
"ingredients": [],
"name": "Pizza",
"procedure": "Est qui alias molestias facilis et et eum. Ducimus est corrupti et qui. Et quidem nostrum qui ipsum perspiciatis et enim. Odio impedit et unde voluptatem.",
"time": 30
}
}
- Updates an ingredient.
- Required headers:
Authorization
header with bearer token that hasupdate:recipes
permission.
- Request body (can be a subset of):
recipe_id
: Related recipe IDname
: Ingredient nameoptional
: Whether the ingredient is optional, booleanmeasurement
: Measurement value, integermeasurement_unit
: Measurement unit name
- Returns:
200 OK
response when the record was successfully updated.400 Bad Request
response when provided fields were invalid.404 Not Found
response when an unknown ingredient ID was provided.
Example response:
{
"success": true,
"result": {
"id": 1,
"recipe_id": 1,
"name": "Flour",
"optional": false,
"measurement": 250,
"measurement_unit": "grams"
}
}
- Deletes an ingredient.
- Required headers:
Authorization
header with bearer token that hasdelete:recipes
permission.
- Request path arguments:
item_id
- Returns:
200 OK
response when a new record was successfully created.404 Not Found
response when an unknown recipe ID was provided.
Example response:
{
"success": true,
"ingredient_id": 1
}
Authentication is handled via Auth0.
All except one endpoints require authentication, and proper permission. The root is a public endpoint left there for debugging.
Caveat: Currently the app does not offer a frontend with a login flow. For testing purposes, the access tokens are generated from two Machine-to-Machine apps' authentication using client_credentials
grant type. The two apps are assigned permissions and act as users with ReadOnly and Admin roles.
API endpoints use these permissions:
- 'create:recipe' (can add recipes and ingredients)
- 'read:recipe' (can read recipes and ingredients)
- 'update:recipe' (can update recipes and ingredients)
- 'delete:recipe' (can delete recipes and ingredients)
Replace client_id
and client_secret
values.
curl --request POST \
--url https://recipehub.auth0.com/oauth/token \
--header 'content-type: application/json' \
--data '{"client_id":"your_client_id","client_secret":"your_client_secret","audience":"recipehub-api","grant_type":"client_credentials"}' | jq .access_token
For reference, see the Test tab of your Auth0 API.