mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-12-13 05:45:22 -05:00
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:
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user