add soft fail user dependency (#479)

* add soft fail user dependency

* filter private recipes on get_recipe_summary

* code clean-up

* restrict single recipe

* cleanup dependencies

* add auto_error oauth2 scheme

* update make file

* update make file

* fix early return

* bump python deps

* restrict category/tags

* format deps

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden
2021-06-10 18:31:14 -08:00
committed by GitHub
parent c2ed4a39ac
commit c175c8e9a0
12 changed files with 477 additions and 424 deletions

View File

@@ -7,6 +7,7 @@ from mealie.db.models.group import Group
from mealie.db.models.mealplan import MealPlan
from mealie.db.models.recipe.comment import RecipeComment
from mealie.db.models.recipe.recipe import Category, RecipeModel, Tag
from mealie.db.models.recipe.settings import RecipeSettings
from mealie.db.models.settings import CustomPage, SiteSettings
from mealie.db.models.shopping_list import ShoppingList
from mealie.db.models.sign_up import SignUp
@@ -35,7 +36,36 @@ class _Recipes(BaseDocument):
self.sql_model: RecipeModel = RecipeModel
self.schema: Recipe = Recipe
def update_image(self, session: Session, slug: str, extension: str = None) -> str:
def get_all_not_private(
self, session: Session, limit: int = None, order_by: str = None, start=0, override_schema=None
):
eff_schema = override_schema or self.schema
if order_by:
order_attr = getattr(self.sql_model, str(order_by))
return [
eff_schema.from_orm(x)
for x in session.query(self.sql_model)
.join(RecipeSettings)
.filter(RecipeSettings.public == True) # noqa: 711
.order_by(order_attr.desc())
.offset(start)
.limit(limit)
.all()
]
return [
eff_schema.from_orm(x)
for x in session.query(self.sql_model)
.join(RecipeSettings)
.filter(RecipeSettings.public == True) # noqa: 711
.offset(start)
.limit(limit)
.all()
]
def update_image(self, session: Session, slug: str, _: str = None) -> str:
entry: RecipeModel = self._query_one(session, match_value=slug)
entry.image = randint(0, 255)
session.commit()

View File

@@ -17,7 +17,6 @@ class BaseDocument:
self.sql_model: SqlAlchemyBase
self.schema: BaseModel
# TODO: Improve Get All Query Functionality
def get_all(
self, session: Session, limit: int = None, order_by: str = None, start=0, end=9999, override_schema=None
) -> list[dict]:
@@ -37,7 +36,7 @@ class BaseDocument:
"""Queries the database for the selected model. Restricts return responses to the
keys specified under "fields"
Args: \n
Args:
session (Session): Database Session Object
fields (list[str]): list of column names to query
limit (int): A limit of values to return
@@ -51,7 +50,7 @@ class BaseDocument:
"""Queries the database of the selected model and returns a list
of all primary_key values
Args: \n
Args:
session (Session): Database Session object
Returns:
@@ -65,7 +64,8 @@ class BaseDocument:
"""Query the sql database for one item an return the sql alchemy model
object. If no match key is provided the primary_key attribute will be used.
Args: \n
Args:
session (Session): Database Session object
match_value (str): The value to use in the query
match_key (str, optional): the key/property to match against. Defaults to None.
@@ -84,7 +84,7 @@ class BaseDocument:
key is provided the class objects primary key will be used to match against.
Args: \n
Args:
match_value (str): A value used to match against the key/value in the database \n
match_key (str, optional): They key to match the value against. Defaults to None. \n
limit (int, optional): A limit to returned responses. Defaults to 1. \n
@@ -116,7 +116,7 @@ class BaseDocument:
def create(self, session: Session, document: dict) -> BaseModel:
"""Creates a new database entry for the given SQL Alchemy Model.
Args: \n
Args:
session (Session): A Database Session
document (dict): A python dictionary representing the data structure
@@ -133,7 +133,7 @@ class BaseDocument:
def update(self, session: Session, match_value: str, new_data: dict) -> BaseModel:
"""Update a database entry.
Args: \n
Args:
session (Session): Database Session
match_value (str): Match "key"
new_data (str): Match "value"

View File

@@ -12,9 +12,42 @@ from mealie.schema.user import LongLiveTokenInDB, UserInDB
from sqlalchemy.orm.session import Session
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
oauth2_scheme_soft_fail = OAuth2PasswordBearer(tokenUrl="/api/auth/token", auto_error=False)
ALGORITHM = "HS256"
async def is_logged_in(token: str = Depends(oauth2_scheme_soft_fail), session=Depends(generate_session)) -> bool:
"""
When you need to determine if the user is logged in, but don't need the user, you can use this
function to return a boolean value to represent if the user is logged in. No Auth exceptions are raised
if the user is not logged in. This behavior is not the same as 'get_current_user'
Args:
token (str, optional): [description]. Defaults to Depends(oauth2_scheme_soft_fail).
session ([type], optional): [description]. Defaults to Depends(generate_session).
Returns:
bool: True = Valid User / False = Not User
"""
try:
payload = jwt.decode(token, settings.SECRET, algorithms=[ALGORITHM])
username: str = payload.get("sub")
long_token: str = payload.get("long_token")
if long_token is not None:
try:
user = validate_long_live_token(session, token, payload.get("id"))
if user:
return True
except Exception:
return False
return username is not None
except Exception:
return False
async def get_current_user(token: str = Depends(oauth2_scheme), session=Depends(generate_session)) -> UserInDB:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,

View File

@@ -1,6 +1,7 @@
from fastapi import APIRouter, Depends
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user, is_logged_in
from mealie.schema.recipe import RecipeSummary
from slugify import slugify
from sqlalchemy.orm.session import Session
@@ -10,9 +11,7 @@ router = APIRouter(tags=["Query All Recipes"])
@router.get("/api/recipes/summary", response_model=list[RecipeSummary])
async def get_recipe_summary(
start=0,
limit=9999,
session: Session = Depends(generate_session),
start=0, limit=9999, session: Session = Depends(generate_session), user: bool = Depends(is_logged_in)
):
"""
Returns key the recipe summary data for recipes in the database. You can perform
@@ -26,20 +25,32 @@ async def get_recipe_summary(
"""
return db.recipes.get_all(session, limit=limit, start=start, order_by="date_updated", override_schema=RecipeSummary)
if user:
return db.recipes.get_all(
session, limit=limit, start=start, order_by="date_updated", override_schema=RecipeSummary
)
else:
return db.recipes.get_all_not_private(
session, limit=limit, start=start, order_by="date_updated", override_schema=RecipeSummary
)
@router.get("/api/recipes/summary/untagged", response_model=list[RecipeSummary])
@router.get(
"/api/recipes/summary/untagged", response_model=list[RecipeSummary], dependencies=[Depends(get_current_user)]
)
async def get_untagged_recipes(count: bool = False, session: Session = Depends(generate_session)):
return db.recipes.count_untagged(session, count=count, override_schema=RecipeSummary)
@router.get("/api/recipes/summary/uncategorized", response_model=list[RecipeSummary])
@router.get(
"/api/recipes/summary/uncategorized", response_model=list[RecipeSummary], dependencies=[Depends(get_current_user)]
)
async def get_uncategorized_recipes(count: bool = False, session: Session = Depends(generate_session)):
return db.recipes.count_uncategorized(session, count=count, override_schema=RecipeSummary)
@router.post("/api/recipes/category")
@router.post("/api/recipes/category", deprecated=True, dependencies=[Depends(get_current_user)])
def filter_by_category(categories: list, session: Session = Depends(generate_session)):
""" pass a list of categories and get a list of recipes associated with those categories """
# ! This should be refactored into a single database call, but I couldn't figure it out
@@ -49,7 +60,7 @@ def filter_by_category(categories: list, session: Session = Depends(generate_ses
return in_category
@router.post("/api/recipes/tag")
@router.post("/api/recipes/tag", deprecated=True, dependencies=[Depends(get_current_user)])
async def filter_by_tags(tags: list, session: Session = Depends(generate_session)):
""" pass a list of tags and get a list of recipes associated with those tags"""
# ! This should be refactored into a single database call, but I couldn't figure it out

View File

@@ -1,7 +1,7 @@
from fastapi import APIRouter, Depends, HTTPException, status
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.deps import get_current_user, is_logged_in
from mealie.schema.category import CategoryIn, RecipeCategoryResponse
from sqlalchemy.orm.session import Session
@@ -21,15 +21,22 @@ def get_empty_categories(session: Session = Depends(generate_session)):
@router.get("/{category}", response_model=RecipeCategoryResponse)
def get_all_recipes_by_category(category: str, session: Session = Depends(generate_session)):
""" Returns a list of recipes associated with the provided category. """
return db.categories.get(session, category)
@router.post("")
async def create_recipe_category(
category: CategoryIn, session: Session = Depends(generate_session), current_user=Depends(get_current_user)
def get_all_recipes_by_category(
category: str, session: Session = Depends(generate_session), is_user: bool = Depends(is_logged_in)
):
""" Returns a list of recipes associated with the provided category. """
category_obj = db.categories.get(session, category)
category_obj = RecipeCategoryResponse.from_orm(category_obj)
if not is_user:
category_obj.recipes = [x for x in category_obj.recipes if x.settings.public]
return category_obj
@router.post("", dependencies=[Depends(get_current_user)])
async def create_recipe_category(category: CategoryIn, session: Session = Depends(generate_session)):
""" Creates a Category in the database """
try:
@@ -38,13 +45,8 @@ async def create_recipe_category(
raise HTTPException(status.HTTP_400_BAD_REQUEST)
@router.put("/{category}", response_model=RecipeCategoryResponse)
async def update_recipe_category(
category: str,
new_category: CategoryIn,
session: Session = Depends(generate_session),
current_user=Depends(get_current_user),
):
@router.put("/{category}", response_model=RecipeCategoryResponse, dependencies=[Depends(get_current_user)])
async def update_recipe_category(category: str, new_category: CategoryIn, session: Session = Depends(generate_session)):
""" Updates an existing Tag in the database """
try:
@@ -53,13 +55,13 @@ async def update_recipe_category(
raise HTTPException(status.HTTP_400_BAD_REQUEST)
@router.delete("/{category}")
async def delete_recipe_category(
category: str, session: Session = Depends(generate_session), current_user=Depends(get_current_user)
):
"""Removes a recipe category from the database. Deleting a
@router.delete("/{category}", dependencies=[Depends(get_current_user)])
async def delete_recipe_category(category: str, session: Session = Depends(generate_session)):
"""
Removes a recipe category from the database. Deleting a
category does not impact a recipe. The category will be removed
from any recipes that contain it"""
from any recipes that contain it
"""
try:
db.categories.delete(session, category)

View File

@@ -6,7 +6,7 @@ from mealie.core.config import settings
from mealie.core.root_logger import get_logger
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.deps import get_current_user, is_logged_in
from mealie.schema.recipe import Recipe, RecipeAsset, RecipeURLIn
from mealie.schema.user import UserInDB
from mealie.services.events import create_recipe_event
@@ -71,18 +71,24 @@ def parse_recipe_url(
@router.get("/{recipe_slug}", response_model=Recipe)
def get_recipe(recipe_slug: str, session: Session = Depends(generate_session)):
def get_recipe(recipe_slug: str, session: Session = Depends(generate_session), is_user: bool = Depends(is_logged_in)):
""" Takes in a recipe slug, returns all data for a recipe """
return db.recipes.get(session, recipe_slug)
recipe: Recipe = db.recipes.get(session, recipe_slug)
if recipe.settings.public or is_user:
return recipe
else:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, {"details": "unauthorized"})
@router.put("/{recipe_slug}")
@router.put("/{recipe_slug}", dependencies=[Depends(get_current_user)])
def update_recipe(
recipe_slug: str,
data: Recipe,
session: Session = Depends(generate_session),
current_user=Depends(get_current_user),
):
""" Updates a recipe by existing slug and data. """
@@ -93,12 +99,11 @@ def update_recipe(
return recipe
@router.patch("/{recipe_slug}")
@router.patch("/{recipe_slug}", dependencies=[Depends(get_current_user)])
def patch_recipe(
recipe_slug: str,
data: Recipe,
session: Session = Depends(generate_session),
current_user=Depends(get_current_user),
):
""" Updates a recipe by existing slug and data. """
@@ -148,18 +153,17 @@ def update_recipe_image(
return {"image": new_version}
@router.post("/{recipe_slug}/image")
@router.post("/{recipe_slug}/image", dependencies=[Depends(get_current_user)])
def scrape_image_url(
recipe_slug: str,
url: RecipeURLIn,
current_user=Depends(get_current_user),
):
""" Removes an existing image and replaces it with the incoming file. """
scrape_image(url.url, recipe_slug)
@router.post("/{recipe_slug}/assets", response_model=RecipeAsset)
@router.post("/{recipe_slug}/assets", response_model=RecipeAsset, dependencies=[Depends(get_current_user)])
def upload_recipe_asset(
recipe_slug: str,
name: str = Form(...),
@@ -167,7 +171,6 @@ def upload_recipe_asset(
extension: str = Form(...),
file: UploadFile = File(...),
session: Session = Depends(generate_session),
current_user=Depends(get_current_user),
):
""" Upload a file to store as a recipe asset """
file_name = slugify(name) + "." + extension

View File

@@ -1,7 +1,7 @@
from fastapi import APIRouter, Depends, HTTPException, status
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.deps import get_current_user, is_logged_in
from mealie.schema.category import RecipeTagResponse, TagIn
from sqlalchemy.orm.session import Session
@@ -23,33 +23,35 @@ def get_empty_tags(session: Session = Depends(generate_session)):
@router.get("/{tag}", response_model=RecipeTagResponse)
def get_all_recipes_by_tag(tag: str, session: Session = Depends(generate_session)):
""" Returns a list of recipes associated with the provided tag. """
return db.tags.get(session, tag)
@router.post("")
async def create_recipe_tag(
tag: TagIn, session: Session = Depends(generate_session), current_user=Depends(get_current_user)
def get_all_recipes_by_tag(
tag: str, session: Session = Depends(generate_session), is_user: bool = Depends(is_logged_in)
):
""" Returns a list of recipes associated with the provided tag. """
tag_obj = db.tags.get(session, tag)
tag_obj = RecipeTagResponse.from_orm(tag_obj)
if not is_user:
tag_obj.recipes = [x for x in tag_obj.recipes if x.settings.public]
return tag_obj
@router.post("", dependencies=[Depends(get_current_user)])
async def create_recipe_tag(tag: TagIn, session: Session = Depends(generate_session)):
""" Creates a Tag in the database """
return db.tags.create(session, tag.dict())
@router.put("/{tag}", response_model=RecipeTagResponse)
async def update_recipe_tag(
tag: str, new_tag: TagIn, session: Session = Depends(generate_session), current_user=Depends(get_current_user)
):
@router.put("/{tag}", response_model=RecipeTagResponse, dependencies=[Depends(get_current_user)])
async def update_recipe_tag(tag: str, new_tag: TagIn, session: Session = Depends(generate_session)):
""" Updates an existing Tag in the database """
return db.tags.update(session, tag, new_tag.dict())
@router.delete("/{tag}")
async def delete_recipe_tag(
tag: str, session: Session = Depends(generate_session), current_user=Depends(get_current_user)
):
@router.delete("/{tag}", dependencies=[Depends(get_current_user)])
async def delete_recipe_tag(tag: str, session: Session = Depends(generate_session)):
"""Removes a recipe tag from the database. Deleting a
tag does not impact a recipe. The tag will be removed
from any recipes that contain it"""

View File

@@ -17,11 +17,10 @@ def get_main_settings(session: Session = Depends(generate_session)):
return db.settings.get(session, 1)
@router.put("")
@router.put("", dependencies=[Depends(get_current_user)])
def update_settings(
data: SiteSettings,
session: Session = Depends(generate_session),
current_user=Depends(get_current_user),
):
""" Returns Site Settings """
db.settings.update(session, 1, data.dict())