mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-08 00:43:12 -05:00
API security hardening (#571)
* Enhance security and safety around user update API - Prevent a regular user from promoting themself to admin - Prevent an admin from demoting themself - Refactor token fixture to admin + regular user tokens * Restrict user CRUD API to admins * Secure admin API routes * Refactor APIrouter into Admin/UserAPIRouter * Secure theme routes * Make 'all recipes' routes public * Secure favorite routes * Remove redundant checks * Fix public routes mistakenly flagged user routes * Make webhooks changeable only by admin * Allow users to create categories and tags * Address lint issues
This commit is contained in:
@@ -42,12 +42,16 @@ def api_routers():
|
||||
app.include_router(meal_plan_router)
|
||||
# Settings Routes
|
||||
app.include_router(settings_router)
|
||||
app.include_router(theme_routes.router)
|
||||
app.include_router(theme_routes.public_router)
|
||||
app.include_router(theme_routes.user_router)
|
||||
# Backups/Imports Routes
|
||||
app.include_router(backup_routes.router)
|
||||
# Migration Routes
|
||||
app.include_router(migration_routes.router)
|
||||
app.include_router(debug_routes.router)
|
||||
# Debug routes
|
||||
app.include_router(debug_routes.public_router)
|
||||
app.include_router(debug_routes.admin_router)
|
||||
# Utility routes
|
||||
app.include_router(utility_routes.router)
|
||||
|
||||
|
||||
|
||||
@@ -1,41 +1,36 @@
|
||||
from http.client import HTTPException
|
||||
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from fastapi import Depends, status
|
||||
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.routers import AdminAPIRouter
|
||||
from mealie.schema.event_notifications import EventNotificationIn, EventNotificationOut
|
||||
from mealie.schema.events import EventsOut, TestEvent
|
||||
from mealie.schema.user import UserInDB
|
||||
from mealie.services.events import test_notification
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/events", tags=["App Events"])
|
||||
router = AdminAPIRouter(prefix="/events", tags=["App Events"])
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
@router.get("", response_model=EventsOut)
|
||||
async def get_events(session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)):
|
||||
async def get_events(session: Session = Depends(generate_session)):
|
||||
""" Get event from the Database """
|
||||
# Get Item
|
||||
return EventsOut(total=db.events.count_all(session), events=db.events.get_all(session, order_by="time_stamp"))
|
||||
|
||||
|
||||
@router.delete("")
|
||||
async def delete_events(
|
||||
session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
|
||||
):
|
||||
async def delete_events(session: Session = Depends(generate_session)):
|
||||
""" Get event from the Database """
|
||||
# Get Item
|
||||
return db.events.delete_all(session)
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
async def delete_event(
|
||||
id: int, session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
|
||||
):
|
||||
async def delete_event(id: int, session: Session = Depends(generate_session)):
|
||||
""" Delete event from the Database """
|
||||
return db.events.delete(session, id)
|
||||
|
||||
@@ -44,7 +39,6 @@ async def delete_event(
|
||||
async def create_event_notification(
|
||||
event_data: EventNotificationIn,
|
||||
session: Session = Depends(generate_session),
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
):
|
||||
""" Create event_notification in the Database """
|
||||
|
||||
@@ -55,7 +49,6 @@ async def create_event_notification(
|
||||
async def test_notification_route(
|
||||
test_data: TestEvent,
|
||||
session: Session = Depends(generate_session),
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
):
|
||||
""" Create event_notification in the Database """
|
||||
|
||||
@@ -71,27 +64,21 @@ async def test_notification_route(
|
||||
|
||||
|
||||
@router.get("/notifications", response_model=list[EventNotificationOut])
|
||||
async def get_all_event_notification(
|
||||
session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
|
||||
):
|
||||
async def get_all_event_notification(session: Session = Depends(generate_session)):
|
||||
""" Get all event_notification from the Database """
|
||||
# Get Item
|
||||
return db.event_notifications.get_all(session, override_schema=EventNotificationOut)
|
||||
|
||||
|
||||
@router.put("/notifications/{id}")
|
||||
async def update_event_notification(
|
||||
id: int, session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
|
||||
):
|
||||
async def update_event_notification(id: int, session: Session = Depends(generate_session)):
|
||||
""" Update event_notification in the Database """
|
||||
# Update Item
|
||||
return {"details": "not yet implemented"}
|
||||
# not yet implemented
|
||||
raise HTTPException(status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||
|
||||
|
||||
@router.delete("/notifications/{id}")
|
||||
async def delete_event_notification(
|
||||
id: int, session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
|
||||
):
|
||||
async def delete_event_notification(id: int, session: Session = Depends(generate_session)):
|
||||
""" Delete event_notification from the Database """
|
||||
# Delete Item
|
||||
return db.event_notifications.delete(session, id)
|
||||
|
||||
@@ -2,19 +2,19 @@ import operator
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, File, HTTPException, UploadFile, status
|
||||
from fastapi import BackgroundTasks, Depends, File, HTTPException, UploadFile, status
|
||||
from mealie.core.config import app_dirs
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.core.security import create_file_token
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.routes.deps import get_current_user
|
||||
from mealie.routes.routers import AdminAPIRouter
|
||||
from mealie.schema.backup import BackupJob, ImportJob, Imports, LocalBackup
|
||||
from mealie.services.backups import imports
|
||||
from mealie.services.backups.exports import backup_all
|
||||
from mealie.services.events import create_backup_event
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/backups", tags=["Backups"], dependencies=[Depends(get_current_user)])
|
||||
router = AdminAPIRouter(prefix="/api/backups", tags=["Backups"])
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import Depends
|
||||
from fastapi.routing import APIRouter
|
||||
from mealie.core.config import APP_VERSION, app_dirs, settings
|
||||
from mealie.core.root_logger import LOGGER_FILE
|
||||
from mealie.core.security import create_file_token
|
||||
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.routers import AdminAPIRouter
|
||||
from mealie.schema.about import AppInfo, AppStatistics, DebugInfo
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/debug", tags=["Debug"])
|
||||
|
||||
admin_router = AdminAPIRouter(prefix="/api/debug", tags=["Debug"])
|
||||
public_router = APIRouter(prefix="/api/debug", tags=["Debug"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def get_debug_info(current_user=Depends(get_current_user)):
|
||||
@admin_router.get("")
|
||||
async def get_debug_info():
|
||||
""" Returns general information about the application for debugging """
|
||||
|
||||
return DebugInfo(
|
||||
@@ -27,7 +30,7 @@ async def get_debug_info(current_user=Depends(get_current_user)):
|
||||
)
|
||||
|
||||
|
||||
@router.get("/statistics")
|
||||
@admin_router.get("/statistics")
|
||||
async def get_app_statistics(session: Session = Depends(generate_session)):
|
||||
return AppStatistics(
|
||||
total_recipes=db.recipes.count_all(session),
|
||||
@@ -38,7 +41,7 @@ async def get_app_statistics(session: Session = Depends(generate_session)):
|
||||
)
|
||||
|
||||
|
||||
@router.get("/version")
|
||||
@public_router.get("/version")
|
||||
async def get_mealie_version():
|
||||
""" Returns the current version of mealie"""
|
||||
return AppInfo(
|
||||
@@ -48,21 +51,21 @@ async def get_mealie_version():
|
||||
)
|
||||
|
||||
|
||||
@router.get("/last-recipe-json")
|
||||
async def get_last_recipe_json(current_user=Depends(get_current_user)):
|
||||
@admin_router.get("/last-recipe-json")
|
||||
async def get_last_recipe_json():
|
||||
""" Returns a token to download a file """
|
||||
return {"fileToken": create_file_token(app_dirs.DEBUG_DIR.joinpath("last_recipe.json"))}
|
||||
|
||||
|
||||
@router.get("/log/{num}")
|
||||
async def get_log(num: int, current_user=Depends(get_current_user)):
|
||||
@admin_router.get("/log/{num}")
|
||||
async def get_log(num: int):
|
||||
""" Doc Str """
|
||||
with open(LOGGER_FILE, "rb") as f:
|
||||
log_text = tail(f, num)
|
||||
return log_text
|
||||
|
||||
|
||||
@router.get("/log")
|
||||
@admin_router.get("/log")
|
||||
async def get_log_file():
|
||||
""" Returns a token to download a file """
|
||||
return {"fileToken": create_file_token(LOGGER_FILE)}
|
||||
|
||||
@@ -75,6 +75,12 @@ async def get_current_user(token: str = Depends(oauth2_scheme), session=Depends(
|
||||
return user
|
||||
|
||||
|
||||
async def get_admin_user(current_user=Depends(get_current_user)) -> UserInDB:
|
||||
if not current_user.admin:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
return current_user
|
||||
|
||||
|
||||
def validate_long_live_token(session: Session, client_token: str, id: int) -> UserInDB:
|
||||
|
||||
tokens: list[LongLiveTokenInDB] = db.api_tokens.get(session, id, "parent_id", limit=9999)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from . import crud, groups
|
||||
from . import groups
|
||||
|
||||
groups_router = APIRouter()
|
||||
|
||||
groups_router.include_router(crud.router)
|
||||
groups_router.include_router(groups.router)
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status
|
||||
from fastapi import BackgroundTasks, 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.routers import AdminAPIRouter, UserAPIRouter
|
||||
from mealie.schema.user import GroupBase, GroupInDB, UpdateGroup, UserInDB
|
||||
from mealie.services.events import create_group_event
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/groups", tags=["Groups"])
|
||||
admin_router = AdminAPIRouter(prefix="/api/groups", tags=["Groups administration"])
|
||||
user_router = UserAPIRouter(prefix="/api/groups", tags=["Groups"])
|
||||
|
||||
|
||||
@router.get("", response_model=list[GroupInDB])
|
||||
@admin_router.get("", response_model=list[GroupInDB])
|
||||
async def get_all_groups(
|
||||
current_user=Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Returns a list of all groups in the database """
|
||||
@@ -19,7 +20,7 @@ async def get_all_groups(
|
||||
return db.groups.get_all(session)
|
||||
|
||||
|
||||
@router.get("/self", response_model=GroupInDB)
|
||||
@user_router.get("/self", response_model=GroupInDB)
|
||||
async def get_current_user_group(
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
@@ -30,11 +31,10 @@ async def get_current_user_group(
|
||||
return db.groups.get(session, current_user.group, "name")
|
||||
|
||||
|
||||
@router.post("", status_code=status.HTTP_201_CREATED)
|
||||
@admin_router.post("", status_code=status.HTTP_201_CREATED)
|
||||
async def create_group(
|
||||
background_tasks: BackgroundTasks,
|
||||
group_data: GroupBase,
|
||||
current_user=Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Creates a Group in the Database """
|
||||
@@ -46,18 +46,17 @@ async def create_group(
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
@router.put("/{id}")
|
||||
@admin_router.put("/{id}")
|
||||
async def update_group_data(
|
||||
id: int,
|
||||
group_data: UpdateGroup,
|
||||
current_user=Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Updates a User Group """
|
||||
db.groups.update(session, id, group_data.dict())
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
@admin_router.delete("/{id}")
|
||||
async def delete_user_group(
|
||||
background_tasks: BackgroundTasks,
|
||||
id: int,
|
||||
|
||||
@@ -3,4 +3,5 @@ from mealie.routes.groups import crud
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(crud.router)
|
||||
router.include_router(crud.admin_router)
|
||||
router.include_router(crud.user_router)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status
|
||||
from fastapi import BackgroundTasks, 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.routers import UserAPIRouter
|
||||
from mealie.schema.meal import MealPlanIn, MealPlanOut
|
||||
from mealie.schema.user import GroupInDB, UserInDB
|
||||
from mealie.services.events import create_group_event
|
||||
@@ -10,7 +11,7 @@ from mealie.services.meal_services import get_todays_meal, set_mealplan_dates
|
||||
from sqlalchemy.orm.session import Session
|
||||
from starlette.responses import FileResponse
|
||||
|
||||
router = APIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
|
||||
router = UserAPIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
|
||||
|
||||
|
||||
@router.get("/all", response_model=list[MealPlanOut])
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import Depends
|
||||
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.routers import UserAPIRouter
|
||||
from mealie.schema.meal import MealPlanOut
|
||||
from mealie.schema.recipe import Recipe
|
||||
from mealie.schema.shopping_list import ListItem, ShoppingListIn, ShoppingListOut
|
||||
@@ -11,7 +12,7 @@ from sqlalchemy.orm.session import Session
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
router = APIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
|
||||
router = UserAPIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
|
||||
|
||||
|
||||
@router.get("/{id}/shopping-list")
|
||||
|
||||
@@ -2,16 +2,15 @@ import operator
|
||||
import shutil
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Depends, File, UploadFile, status
|
||||
from fastapi import Depends, File, HTTPException, UploadFile, status
|
||||
from mealie.core.config import app_dirs
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.routes.deps import get_current_user
|
||||
from mealie.routes.routers import AdminAPIRouter
|
||||
from mealie.schema.migration import MigrationFile, Migrations
|
||||
from mealie.services.migrations import migration
|
||||
from sqlalchemy.orm.session import Session
|
||||
from fastapi import HTTPException
|
||||
|
||||
router = APIRouter(prefix="/api/migrations", tags=["Migration"], dependencies=[Depends(get_current_user)])
|
||||
router = AdminAPIRouter(prefix="/api/migrations", tags=["Migration"])
|
||||
|
||||
|
||||
@router.get("", response_model=List[Migrations])
|
||||
|
||||
@@ -4,7 +4,12 @@ from mealie.routes.recipe import all_recipe_routes, category_routes, comments, r
|
||||
recipe_router = APIRouter()
|
||||
|
||||
recipe_router.include_router(all_recipe_routes.router)
|
||||
recipe_router.include_router(recipe_crud_routes.router)
|
||||
recipe_router.include_router(category_routes.router)
|
||||
recipe_router.include_router(tag_routes.router)
|
||||
recipe_router.include_router(recipe_crud_routes.public_router)
|
||||
recipe_router.include_router(recipe_crud_routes.user_router)
|
||||
recipe_router.include_router(category_routes.public_router)
|
||||
recipe_router.include_router(category_routes.user_router)
|
||||
recipe_router.include_router(category_routes.admin_router)
|
||||
recipe_router.include_router(tag_routes.admin_router)
|
||||
recipe_router.include_router(tag_routes.user_router)
|
||||
recipe_router.include_router(tag_routes.public_router)
|
||||
recipe_router.include_router(comments.router)
|
||||
|
||||
@@ -1,7 +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.routes.deps import is_logged_in
|
||||
from mealie.schema.recipe import RecipeSummary
|
||||
from slugify import slugify
|
||||
from sqlalchemy.orm.session import Session
|
||||
@@ -36,21 +36,17 @@ async def get_recipe_summary(
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/api/recipes/summary/untagged", response_model=list[RecipeSummary], dependencies=[Depends(get_current_user)]
|
||||
)
|
||||
@router.get("/api/recipes/summary/untagged", response_model=list[RecipeSummary])
|
||||
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], dependencies=[Depends(get_current_user)]
|
||||
)
|
||||
@router.get("/api/recipes/summary/uncategorized", response_model=list[RecipeSummary])
|
||||
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", deprecated=True, dependencies=[Depends(get_current_user)])
|
||||
@router.post("/api/recipes/category", deprecated=True)
|
||||
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
|
||||
@@ -60,7 +56,7 @@ def filter_by_category(categories: list, session: Session = Depends(generate_ses
|
||||
return in_category
|
||||
|
||||
|
||||
@router.post("/api/recipes/tag", deprecated=True, dependencies=[Depends(get_current_user)])
|
||||
@router.post("/api/recipes/tag", deprecated=True)
|
||||
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,26 +1,29 @@
|
||||
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, is_logged_in
|
||||
from mealie.routes.deps import is_logged_in
|
||||
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
|
||||
from mealie.schema.category import CategoryIn, RecipeCategoryResponse
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/categories", tags=["Recipe Categories"])
|
||||
public_router = APIRouter(prefix="/api/categories", tags=["Recipe Categories"])
|
||||
user_router = UserAPIRouter(prefix="/api/categories", tags=["Recipe Categories"])
|
||||
admin_router = AdminAPIRouter(prefix="/api/categories", tags=["Recipe Categories"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
@public_router.get("")
|
||||
async def get_all_recipe_categories(session: Session = Depends(generate_session)):
|
||||
""" Returns a list of available categories in the database """
|
||||
return db.categories.get_all_limit_columns(session, ["slug", "name"])
|
||||
|
||||
|
||||
@router.get("/empty")
|
||||
@public_router.get("/empty")
|
||||
def get_empty_categories(session: Session = Depends(generate_session)):
|
||||
""" Returns a list of categories that do not contain any recipes"""
|
||||
return db.categories.get_empty(session)
|
||||
|
||||
|
||||
@router.get("/{category}", response_model=RecipeCategoryResponse)
|
||||
@public_router.get("/{category}", response_model=RecipeCategoryResponse)
|
||||
def get_all_recipes_by_category(
|
||||
category: str, session: Session = Depends(generate_session), is_user: bool = Depends(is_logged_in)
|
||||
):
|
||||
@@ -35,7 +38,7 @@ def get_all_recipes_by_category(
|
||||
return category_obj
|
||||
|
||||
|
||||
@router.post("", dependencies=[Depends(get_current_user)])
|
||||
@user_router.post("")
|
||||
async def create_recipe_category(category: CategoryIn, session: Session = Depends(generate_session)):
|
||||
""" Creates a Category in the database """
|
||||
|
||||
@@ -45,7 +48,7 @@ async def create_recipe_category(category: CategoryIn, session: Session = Depend
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
@router.put("/{category}", response_model=RecipeCategoryResponse, dependencies=[Depends(get_current_user)])
|
||||
@admin_router.put("/{category}", response_model=RecipeCategoryResponse)
|
||||
async def update_recipe_category(category: str, new_category: CategoryIn, session: Session = Depends(generate_session)):
|
||||
""" Updates an existing Tag in the database """
|
||||
|
||||
@@ -55,7 +58,7 @@ async def update_recipe_category(category: str, new_category: CategoryIn, sessio
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
@router.delete("/{category}", dependencies=[Depends(get_current_user)])
|
||||
@admin_router.delete("/{category}")
|
||||
async def delete_recipe_category(category: str, session: Session = Depends(generate_session)):
|
||||
"""
|
||||
Removes a recipe category from the database. Deleting a
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
from http.client import HTTPException
|
||||
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from fastapi import Depends, 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.routers import UserAPIRouter
|
||||
from mealie.schema.comments import CommentIn, CommentOut, CommentSaveToDB
|
||||
from mealie.schema.user import UserInDB
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["Recipe Comments"])
|
||||
router = UserAPIRouter(prefix="/api", tags=["Recipe Comments"])
|
||||
|
||||
|
||||
@router.post("/recipes/{slug}/comments")
|
||||
@@ -35,7 +36,7 @@ async def update_comment(
|
||||
old_comment: CommentOut = db.comments.get(session, id)
|
||||
|
||||
if current_user.id != old_comment.user.id:
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
return db.comments.update(session, id, new_comment)
|
||||
|
||||
@@ -51,4 +52,4 @@ async def delete_comment(
|
||||
db.comments.delete(session, id)
|
||||
return
|
||||
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
import shutil
|
||||
from shutil import copyfileobj
|
||||
from zipfile import ZipFile
|
||||
@@ -21,11 +22,12 @@ from slugify import slugify
|
||||
from sqlalchemy.orm.session import Session
|
||||
from starlette.responses import FileResponse
|
||||
|
||||
router = APIRouter(prefix="/api/recipes", tags=["Recipe CRUD"])
|
||||
user_router = UserAPIRouter(prefix="/api/recipes", tags=["Recipe CRUD"])
|
||||
public_router = APIRouter(prefix="/api/recipes", tags=["Recipe CRUD"])
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
@router.post("/create", status_code=201, response_model=str)
|
||||
@user_router.post("/create", status_code=201, response_model=str)
|
||||
def create_from_json(
|
||||
background_tasks: BackgroundTasks,
|
||||
data: Recipe,
|
||||
@@ -46,12 +48,12 @@ def create_from_json(
|
||||
return recipe.slug
|
||||
|
||||
|
||||
@router.post("/test-scrape-url", dependencies=[Depends(get_current_user)])
|
||||
@user_router.post("/test-scrape-url")
|
||||
def test_parse_recipe_url(url: RecipeURLIn):
|
||||
return scrape_url(url.url)
|
||||
|
||||
|
||||
@router.post("/create-url", status_code=201, response_model=str)
|
||||
@user_router.post("/create-url", status_code=201, response_model=str)
|
||||
def parse_recipe_url(
|
||||
background_tasks: BackgroundTasks,
|
||||
url: RecipeURLIn,
|
||||
@@ -74,7 +76,7 @@ def parse_recipe_url(
|
||||
return recipe.slug
|
||||
|
||||
|
||||
@router.get("/{recipe_slug}", response_model=Recipe)
|
||||
@public_router.get("/{recipe_slug}", response_model=Recipe)
|
||||
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 """
|
||||
|
||||
@@ -88,10 +90,10 @@ def get_recipe(recipe_slug: str, session: Session = Depends(generate_session), i
|
||||
return recipe
|
||||
|
||||
else:
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, {"details": "unauthorized"})
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
|
||||
@router.post("/create-from-zip", dependencies=[Depends(get_current_user)])
|
||||
@user_router.post("/create-from-zip")
|
||||
async def create_recipe_from_zip(
|
||||
session: Session = Depends(generate_session),
|
||||
temp_path=Depends(temporary_zip_path),
|
||||
@@ -121,7 +123,7 @@ async def create_recipe_from_zip(
|
||||
return recipe
|
||||
|
||||
|
||||
@router.get("/{recipe_slug}/zip")
|
||||
@public_router.get("/{recipe_slug}/zip")
|
||||
async def get_recipe_as_zip(
|
||||
recipe_slug: str, session: Session = Depends(generate_session), temp_path=Depends(temporary_zip_path)
|
||||
):
|
||||
@@ -139,7 +141,7 @@ async def get_recipe_as_zip(
|
||||
return FileResponse(temp_path, filename=f"{recipe_slug}.zip")
|
||||
|
||||
|
||||
@router.put("/{recipe_slug}", dependencies=[Depends(get_current_user)])
|
||||
@user_router.put("/{recipe_slug}")
|
||||
def update_recipe(
|
||||
recipe_slug: str,
|
||||
data: Recipe,
|
||||
@@ -154,7 +156,7 @@ def update_recipe(
|
||||
return recipe
|
||||
|
||||
|
||||
@router.patch("/{recipe_slug}", dependencies=[Depends(get_current_user)])
|
||||
@user_router.patch("/{recipe_slug}")
|
||||
def patch_recipe(
|
||||
recipe_slug: str,
|
||||
data: Recipe,
|
||||
@@ -171,7 +173,7 @@ def patch_recipe(
|
||||
return recipe
|
||||
|
||||
|
||||
@router.delete("/{recipe_slug}")
|
||||
@user_router.delete("/{recipe_slug}")
|
||||
def delete_recipe(
|
||||
background_tasks: BackgroundTasks,
|
||||
recipe_slug: str,
|
||||
@@ -194,7 +196,7 @@ def delete_recipe(
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
@router.put("/{recipe_slug}/image", dependencies=[Depends(get_current_user)])
|
||||
@user_router.put("/{recipe_slug}/image")
|
||||
def update_recipe_image(
|
||||
recipe_slug: str,
|
||||
image: bytes = File(...),
|
||||
@@ -208,7 +210,7 @@ def update_recipe_image(
|
||||
return {"image": new_version}
|
||||
|
||||
|
||||
@router.post("/{recipe_slug}/image", dependencies=[Depends(get_current_user)])
|
||||
@user_router.post("/{recipe_slug}/image")
|
||||
def scrape_image_url(
|
||||
recipe_slug: str,
|
||||
url: RecipeURLIn,
|
||||
@@ -218,7 +220,7 @@ def scrape_image_url(
|
||||
scrape_image(url.url, recipe_slug)
|
||||
|
||||
|
||||
@router.post("/{recipe_slug}/assets", response_model=RecipeAsset, dependencies=[Depends(get_current_user)])
|
||||
@user_router.post("/{recipe_slug}/assets", response_model=RecipeAsset)
|
||||
def upload_recipe_asset(
|
||||
recipe_slug: str,
|
||||
name: str = Form(...),
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
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, is_logged_in
|
||||
from mealie.routes.deps import is_logged_in
|
||||
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
|
||||
from mealie.schema.category import RecipeTagResponse, TagIn
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(tags=["Recipes"])
|
||||
|
||||
router = APIRouter(prefix="/api/tags", tags=["Recipe Tags"])
|
||||
public_router = APIRouter(prefix="/api/tags", tags=["Recipe Tags"])
|
||||
user_router = UserAPIRouter(prefix="/api/tags", tags=["Recipe Tags"])
|
||||
admin_router = AdminAPIRouter(prefix="/api/tags", tags=["Recipe Tags"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
@public_router.get("")
|
||||
async def get_all_recipe_tags(session: Session = Depends(generate_session)):
|
||||
""" Returns a list of available tags in the database """
|
||||
return db.tags.get_all_limit_columns(session, ["slug", "name"])
|
||||
|
||||
|
||||
@router.get("/empty")
|
||||
@public_router.get("/empty")
|
||||
def get_empty_tags(session: Session = Depends(generate_session)):
|
||||
""" Returns a list of tags that do not contain any recipes"""
|
||||
return db.tags.get_empty(session)
|
||||
|
||||
|
||||
@router.get("/{tag}", response_model=RecipeTagResponse)
|
||||
@public_router.get("/{tag}", response_model=RecipeTagResponse)
|
||||
def get_all_recipes_by_tag(
|
||||
tag: str, session: Session = Depends(generate_session), is_user: bool = Depends(is_logged_in)
|
||||
):
|
||||
@@ -36,21 +37,21 @@ def get_all_recipes_by_tag(
|
||||
return tag_obj
|
||||
|
||||
|
||||
@router.post("", dependencies=[Depends(get_current_user)])
|
||||
@user_router.post("")
|
||||
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, dependencies=[Depends(get_current_user)])
|
||||
@admin_router.put("/{tag}", response_model=RecipeTagResponse)
|
||||
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}", dependencies=[Depends(get_current_user)])
|
||||
@admin_router.delete("/{tag}")
|
||||
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
|
||||
|
||||
26
mealie/routes/routers.py
Normal file
26
mealie/routes/routers.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from mealie.routes.deps import get_admin_user, get_current_user
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class AdminAPIRouter(APIRouter):
|
||||
""" Router for functions to be protected behind admin authentication """
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tags: Optional[List[str]] = None,
|
||||
prefix: str = "",
|
||||
):
|
||||
super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_admin_user)])
|
||||
|
||||
|
||||
class UserAPIRouter(APIRouter):
|
||||
""" Router for functions to be protected behind user authentication """
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tags: Optional[List[str]] = None,
|
||||
prefix: str = "",
|
||||
):
|
||||
super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_current_user)])
|
||||
@@ -1,12 +1,13 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import Depends
|
||||
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.routers import UserAPIRouter
|
||||
from mealie.schema.shopping_list import ShoppingListIn, ShoppingListOut
|
||||
from mealie.schema.user import UserInDB
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
shopping_list_router = APIRouter(prefix="/api/shopping-lists", tags=["Shopping Lists"])
|
||||
shopping_list_router = UserAPIRouter(prefix="/api/shopping-lists", tags=["Shopping Lists"])
|
||||
|
||||
|
||||
@shopping_list_router.post("", response_model=ShoppingListOut)
|
||||
@@ -28,13 +29,13 @@ async def get_shopping_list(id: int, session: Session = Depends(generate_session
|
||||
return db.shopping_lists.get(session, id)
|
||||
|
||||
|
||||
@shopping_list_router.put("/{id}", dependencies=[Depends(get_current_user)], response_model=ShoppingListOut)
|
||||
@shopping_list_router.put("/{id}", response_model=ShoppingListOut)
|
||||
async def update_shopping_list(id: int, new_data: ShoppingListIn, session: Session = Depends(generate_session)):
|
||||
""" Update Shopping List in the Database """
|
||||
return db.shopping_lists.update(session, id, new_data)
|
||||
|
||||
|
||||
@shopping_list_router.delete("/{id}", dependencies=[Depends(get_current_user)])
|
||||
@shopping_list_router.delete("/{id}")
|
||||
async def delete_shopping_list(id: int, session: Session = Depends(generate_session)):
|
||||
""" Delete Shopping List from the Database """
|
||||
return db.shopping_lists.delete(session, id)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from . import all_settings, custom_pages, site_settings
|
||||
from . import custom_pages, site_settings
|
||||
|
||||
settings_router = APIRouter()
|
||||
|
||||
settings_router.include_router(all_settings.router)
|
||||
settings_router.include_router(custom_pages.router)
|
||||
settings_router.include_router(site_settings.router)
|
||||
settings_router.include_router(custom_pages.public_router)
|
||||
settings_router.include_router(custom_pages.admin_router)
|
||||
settings_router.include_router(site_settings.public_router)
|
||||
settings_router.include_router(site_settings.admin_router)
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
from fastapi import APIRouter
|
||||
from mealie.routes.site_settings import custom_pages, site_settings
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(custom_pages.router)
|
||||
router.include_router(site_settings.router)
|
||||
@@ -1,46 +1,41 @@
|
||||
from mealie.routes.routers import AdminAPIRouter
|
||||
from typing import Union
|
||||
|
||||
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
|
||||
from mealie.schema.settings import CustomPageBase, CustomPageOut
|
||||
from mealie.schema.user import UserInDB
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/site-settings/custom-pages", tags=["Settings"])
|
||||
public_router = APIRouter(prefix="/api/site-settings/custom-pages", tags=["Settings"])
|
||||
admin_router = AdminAPIRouter(prefix="/api/site-settings/custom-pages", tags=["Settings"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
@public_router.get("")
|
||||
def get_custom_pages(session: Session = Depends(generate_session)):
|
||||
""" Returns the sites custom pages """
|
||||
|
||||
return db.custom_pages.get_all(session)
|
||||
|
||||
|
||||
@router.post("")
|
||||
@admin_router.post("")
|
||||
async def create_new_page(
|
||||
new_page: CustomPageBase,
|
||||
session: Session = Depends(generate_session),
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
):
|
||||
""" Creates a new Custom Page """
|
||||
|
||||
db.custom_pages.create(session, new_page.dict())
|
||||
|
||||
|
||||
@router.put("")
|
||||
async def update_multiple_pages(
|
||||
pages: list[CustomPageOut],
|
||||
session: Session = Depends(generate_session),
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
):
|
||||
@admin_router.put("")
|
||||
async def update_multiple_pages(pages: list[CustomPageOut], session: Session = Depends(generate_session)):
|
||||
""" Update multiple custom pages """
|
||||
for page in pages:
|
||||
db.custom_pages.update(session, page.id, page.dict())
|
||||
|
||||
|
||||
@router.get("/{id}")
|
||||
@public_router.get("/{id}")
|
||||
async def get_single_page(
|
||||
id: Union[int, str],
|
||||
session: Session = Depends(generate_session),
|
||||
@@ -52,23 +47,21 @@ async def get_single_page(
|
||||
return db.custom_pages.get(session, id, "slug")
|
||||
|
||||
|
||||
@router.put("/{id}")
|
||||
@admin_router.put("/{id}")
|
||||
async def update_single_page(
|
||||
data: CustomPageOut,
|
||||
id: int,
|
||||
session: Session = Depends(generate_session),
|
||||
current_user=Depends(get_current_user),
|
||||
):
|
||||
""" Removes a custom page from the database """
|
||||
|
||||
return db.custom_pages.update(session, id, data.dict())
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
@admin_router.delete("/{id}")
|
||||
async def delete_custom_page(
|
||||
id: int,
|
||||
session: Session = Depends(generate_session),
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
):
|
||||
""" Removes a custom page from the database """
|
||||
|
||||
|
||||
@@ -2,22 +2,24 @@ 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.routers import AdminAPIRouter
|
||||
from mealie.schema.settings import SiteSettings
|
||||
from mealie.schema.user import GroupInDB, UserInDB
|
||||
from mealie.utils.post_webhooks import post_webhooks
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/site-settings", tags=["Settings"])
|
||||
public_router = APIRouter(prefix="/api/site-settings", tags=["Settings"])
|
||||
admin_router = AdminAPIRouter(prefix="/api/site-settings", tags=["Settings"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
@public_router.get("")
|
||||
def get_main_settings(session: Session = Depends(generate_session)):
|
||||
""" Returns basic site settings """
|
||||
|
||||
return db.settings.get(session, 1)
|
||||
|
||||
|
||||
@router.put("", dependencies=[Depends(get_current_user)])
|
||||
@admin_router.put("")
|
||||
def update_settings(
|
||||
data: SiteSettings,
|
||||
session: Session = Depends(generate_session),
|
||||
@@ -26,7 +28,7 @@ def update_settings(
|
||||
db.settings.update(session, 1, data.dict())
|
||||
|
||||
|
||||
@router.post("/webhooks/test")
|
||||
@admin_router.post("/webhooks/test")
|
||||
def test_webhooks(
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
|
||||
@@ -1,45 +1,46 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.routing import APIRouter
|
||||
from fastapi import 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.routers import UserAPIRouter
|
||||
from mealie.schema.theme import SiteTheme
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["Themes"])
|
||||
user_router = UserAPIRouter(prefix="/api", tags=["Themes"])
|
||||
public_router = APIRouter(prefix="/api", tags=["Themes"])
|
||||
|
||||
|
||||
@router.get("/themes")
|
||||
@public_router.get("/themes")
|
||||
def get_all_themes(session: Session = Depends(generate_session)):
|
||||
""" Returns all site themes """
|
||||
|
||||
return db.themes.get_all(session)
|
||||
|
||||
|
||||
@router.post("/themes/create", status_code=status.HTTP_201_CREATED)
|
||||
def create_theme(data: SiteTheme, session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
|
||||
@user_router.post("/themes/create", status_code=status.HTTP_201_CREATED)
|
||||
def create_theme(data: SiteTheme, session: Session = Depends(generate_session)):
|
||||
""" Creates a site color theme database entry """
|
||||
db.themes.create(session, data.dict())
|
||||
|
||||
|
||||
@router.get("/themes/{id}")
|
||||
@public_router.get("/themes/{id}")
|
||||
def get_single_theme(id: int, session: Session = Depends(generate_session)):
|
||||
""" Returns a named theme """
|
||||
return db.themes.get(session, id)
|
||||
|
||||
|
||||
@router.put("/themes/{id}", status_code=status.HTTP_200_OK)
|
||||
@user_router.put("/themes/{id}", status_code=status.HTTP_200_OK)
|
||||
def update_theme(
|
||||
id: int,
|
||||
data: SiteTheme,
|
||||
session: Session = Depends(generate_session),
|
||||
current_user=Depends(get_current_user),
|
||||
):
|
||||
""" Update a theme database entry """
|
||||
db.themes.update(session, id, data.dict())
|
||||
|
||||
|
||||
@router.delete("/themes/{id}", status_code=status.HTTP_200_OK)
|
||||
def delete_theme(id: int, session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
|
||||
@user_router.delete("/themes/{id}", status_code=status.HTTP_200_OK)
|
||||
def delete_theme(id: int, session: Session = Depends(generate_session)):
|
||||
""" Deletes theme from the database """
|
||||
try:
|
||||
db.themes.delete(session, id)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.routes.deps import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/api/foods", dependencies=[Depends(get_current_user)])
|
||||
router = UserAPIRouter(prefix="/api/foods")
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.routes.deps import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/api/units", dependencies=[Depends(get_current_user)])
|
||||
router = UserAPIRouter(prefix="/api/units")
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,11 @@ from . import api_tokens, auth, crud, sign_up
|
||||
|
||||
user_router = APIRouter()
|
||||
|
||||
user_router.include_router(auth.router)
|
||||
user_router.include_router(sign_up.router)
|
||||
user_router.include_router(crud.router)
|
||||
user_router.include_router(auth.public_router)
|
||||
user_router.include_router(auth.user_router)
|
||||
user_router.include_router(sign_up.public_router)
|
||||
user_router.include_router(sign_up.admin_router)
|
||||
user_router.include_router(crud.public_router)
|
||||
user_router.include_router(crud.user_router)
|
||||
user_router.include_router(crud.admin_router)
|
||||
user_router.include_router(api_tokens.router)
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from fastapi import HTTPException, status
|
||||
from fastapi.param_functions import Depends
|
||||
from mealie.core.security import create_access_token
|
||||
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.routers import UserAPIRouter
|
||||
from mealie.schema.user import CreateToken, LoingLiveTokenIn, LongLiveTokenInDB, UserInDB
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/users", tags=["User API Tokens"])
|
||||
router = UserAPIRouter(prefix="/api/users", tags=["User API Tokens"])
|
||||
|
||||
|
||||
@router.post("/api-tokens", status_code=status.HTTP_201_CREATED)
|
||||
@@ -53,4 +54,4 @@ async def delete_api_token(
|
||||
deleted_token = db.api_tokens.delete(session, token_id)
|
||||
return {"token_delete": deleted_token.name}
|
||||
else:
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
@@ -5,15 +5,17 @@ from mealie.core import security
|
||||
from mealie.core.security import authenticate_user
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.routes.deps import get_current_user
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.schema.user import UserInDB
|
||||
from mealie.services.events import create_user_event
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["Authentication"])
|
||||
public_router = APIRouter(prefix="/api/auth", tags=["Authentication"])
|
||||
user_router = UserAPIRouter(prefix="/api/auth", tags=["Authentication"])
|
||||
|
||||
|
||||
@router.post("/token/long")
|
||||
@router.post("/token")
|
||||
@public_router.post("/token/long")
|
||||
@public_router.post("/token")
|
||||
def get_token(
|
||||
background_tasks: BackgroundTasks,
|
||||
request: Request,
|
||||
@@ -38,7 +40,7 @@ def get_token(
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
|
||||
@router.get("/refresh")
|
||||
@user_router.get("/refresh")
|
||||
async def refresh_token(current_user: UserInDB = Depends(get_current_user)):
|
||||
""" Use a valid token to get another token"""
|
||||
access_token = security.create_access_token(data=dict(sub=current_user.email))
|
||||
|
||||
@@ -1,21 +1,34 @@
|
||||
import shutil
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, File, HTTPException, UploadFile, status
|
||||
from fastapi import BackgroundTasks, Depends, File, HTTPException, UploadFile, status
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.routing import APIRouter
|
||||
from mealie.core import security
|
||||
from mealie.core.config import app_dirs, settings
|
||||
from mealie.core.security import get_password_hash, verify_password
|
||||
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.routers import AdminAPIRouter, UserAPIRouter
|
||||
from mealie.schema.user import ChangePassword, UserBase, UserFavorites, UserIn, UserInDB, UserOut
|
||||
from mealie.services.events import create_user_event
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/users", tags=["Users"])
|
||||
public_router = APIRouter(prefix="/api/users", tags=["Users"])
|
||||
user_router = UserAPIRouter(prefix="/api/users", tags=["Users"])
|
||||
admin_router = AdminAPIRouter(prefix="/api/users", tags=["Users"])
|
||||
|
||||
|
||||
@router.post("", response_model=UserOut, status_code=201)
|
||||
async def assert_user_change_allowed(
|
||||
id: int,
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
):
|
||||
if current_user.id != id and not current_user.admin:
|
||||
# only admins can edit other users
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN, detail="NOT_AN_ADMIN")
|
||||
|
||||
|
||||
@admin_router.post("", response_model=UserOut, status_code=201)
|
||||
async def create_user(
|
||||
background_tasks: BackgroundTasks,
|
||||
new_user: UserIn,
|
||||
@@ -30,26 +43,19 @@ async def create_user(
|
||||
return db.users.create(session, new_user.dict())
|
||||
|
||||
|
||||
@router.get("", response_model=list[UserOut])
|
||||
async def get_all_users(
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
|
||||
if not current_user.admin:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
@admin_router.get("", response_model=list[UserOut])
|
||||
async def get_all_users(session: Session = Depends(generate_session)):
|
||||
return db.users.get_all(session)
|
||||
|
||||
|
||||
@router.get("/self", response_model=UserOut)
|
||||
@user_router.get("/self", response_model=UserOut)
|
||||
async def get_logged_in_user(
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
):
|
||||
return current_user.dict()
|
||||
|
||||
|
||||
@router.get("/{id}", response_model=UserOut, dependencies=[Depends(get_current_user)])
|
||||
@admin_router.get("/{id}", response_model=UserOut)
|
||||
async def get_user_by_id(
|
||||
id: int,
|
||||
session: Session = Depends(generate_session),
|
||||
@@ -57,7 +63,7 @@ async def get_user_by_id(
|
||||
return db.users.get(session, id)
|
||||
|
||||
|
||||
@router.put("/{id}/reset-password", dependencies=[Depends(get_current_user)])
|
||||
@user_router.put("/{id}/reset-password")
|
||||
async def reset_user_password(
|
||||
id: int,
|
||||
session: Session = Depends(generate_session),
|
||||
@@ -67,7 +73,7 @@ async def reset_user_password(
|
||||
db.users.update_password(session, id, new_password)
|
||||
|
||||
|
||||
@router.put("/{id}")
|
||||
@user_router.put("/{id}")
|
||||
async def update_user(
|
||||
id: int,
|
||||
new_data: UserBase,
|
||||
@@ -75,16 +81,24 @@ async def update_user(
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
|
||||
token = None
|
||||
if current_user.id == id or current_user.admin:
|
||||
db.users.update(session, id, new_data.dict())
|
||||
assert_user_change_allowed(id)
|
||||
|
||||
if not current_user.admin and (new_data.admin or current_user.group != new_data.group):
|
||||
# prevent a regular user from doing admin tasks on themself
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
if current_user.id == id and current_user.admin and not new_data.admin:
|
||||
# prevent an admin from demoting themself
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
db.users.update(session, id, new_data.dict())
|
||||
if current_user.id == id:
|
||||
access_token = security.create_access_token(data=dict(sub=new_data.email))
|
||||
token = {"access_token": access_token, "token_type": "bearer"}
|
||||
return token
|
||||
|
||||
|
||||
@router.get("/{id}/image")
|
||||
@public_router.get("/{id}/image")
|
||||
async def get_user_image(id: str):
|
||||
""" Returns a users profile picture """
|
||||
user_dir = app_dirs.USER_DIR.joinpath(id)
|
||||
@@ -94,13 +108,15 @@ async def get_user_image(id: str):
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
@router.post("/{id}/image", dependencies=[Depends(get_current_user)])
|
||||
@user_router.post("/{id}/image")
|
||||
async def update_user_image(
|
||||
id: str,
|
||||
profile_image: UploadFile = File(...),
|
||||
):
|
||||
""" Updates a User Image """
|
||||
|
||||
assert_user_change_allowed(id)
|
||||
|
||||
extension = profile_image.filename.split(".")[-1]
|
||||
|
||||
app_dirs.USER_DIR.joinpath(id).mkdir(parents=True, exist_ok=True)
|
||||
@@ -116,7 +132,7 @@ async def update_user_image(
|
||||
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
@router.put("/{id}/password")
|
||||
@user_router.put("/{id}/password")
|
||||
async def update_password(
|
||||
id: int,
|
||||
password_change: ChangePassword,
|
||||
@@ -125,24 +141,24 @@ async def update_password(
|
||||
):
|
||||
""" Resets the User Password"""
|
||||
|
||||
assert_user_change_allowed(id)
|
||||
match_passwords = verify_password(password_change.current_password, current_user.password)
|
||||
match_id = current_user.id == id
|
||||
|
||||
if not (match_passwords and match_id):
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
|
||||
if not (match_passwords):
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
new_password = get_password_hash(password_change.new_password)
|
||||
db.users.update_password(session, id, new_password)
|
||||
|
||||
|
||||
@router.get("/{id}/favorites", response_model=UserFavorites)
|
||||
@user_router.get("/{id}/favorites", response_model=UserFavorites)
|
||||
async def get_favorites(id: str, session: Session = Depends(generate_session)):
|
||||
""" Adds a Recipe to the users favorites """
|
||||
""" Get user's favorite recipes """
|
||||
|
||||
return db.users.get(session, id, override_schema=UserFavorites)
|
||||
|
||||
|
||||
@router.post("/{id}/favorites/{slug}")
|
||||
@user_router.post("/{id}/favorites/{slug}")
|
||||
async def add_favorite(
|
||||
slug: str,
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
@@ -150,12 +166,13 @@ async def add_favorite(
|
||||
):
|
||||
""" Adds a Recipe to the users favorites """
|
||||
|
||||
assert_user_change_allowed(id)
|
||||
current_user.favorite_recipes.append(slug)
|
||||
|
||||
db.users.update(session, current_user.id, current_user)
|
||||
|
||||
|
||||
@router.delete("/{id}/favorites/{slug}")
|
||||
@user_router.delete("/{id}/favorites/{slug}")
|
||||
async def remove_favorite(
|
||||
slug: str,
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
@@ -163,6 +180,7 @@ async def remove_favorite(
|
||||
):
|
||||
""" Adds a Recipe to the users favorites """
|
||||
|
||||
assert_user_change_allowed(id)
|
||||
current_user.favorite_recipes = [x for x in current_user.favorite_recipes if x != slug]
|
||||
|
||||
db.users.update(session, current_user.id, current_user)
|
||||
@@ -170,21 +188,21 @@ async def remove_favorite(
|
||||
return
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
@admin_router.delete("/{id}")
|
||||
async def delete_user(
|
||||
background_tasks: BackgroundTasks,
|
||||
id: int,
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Removes a user from the database. Must be the current user or a super user"""
|
||||
|
||||
assert_user_change_allowed(id)
|
||||
|
||||
if id == 1:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="SUPER_USER")
|
||||
|
||||
if current_user.id == id or current_user.admin:
|
||||
try:
|
||||
db.users.delete(session, id)
|
||||
background_tasks.add_task(create_user_event, "User Deleted", f"User ID: {id}", session=session)
|
||||
except Exception:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
try:
|
||||
db.users.delete(session, id)
|
||||
background_tasks.add_task(create_user_event, "User Deleted", f"User ID: {id}", session=session)
|
||||
except Exception:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@@ -4,18 +4,19 @@ from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status
|
||||
from mealie.core.security import get_password_hash
|
||||
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_admin_user
|
||||
from mealie.routes.routers import AdminAPIRouter
|
||||
from mealie.schema.sign_up import SignUpIn, SignUpOut, SignUpToken
|
||||
from mealie.schema.user import UserIn, UserInDB
|
||||
from mealie.services.events import create_user_event
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/users/sign-ups", tags=["User Signup"])
|
||||
public_router = APIRouter(prefix="/api/users/sign-ups", tags=["User Signup"])
|
||||
admin_router = AdminAPIRouter(prefix="/api/users/sign-ups", tags=["User Signup"])
|
||||
|
||||
|
||||
@router.get("", response_model=list[SignUpOut])
|
||||
@admin_router.get("", response_model=list[SignUpOut])
|
||||
async def get_all_open_sign_ups(
|
||||
current_user=Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Returns a list of open sign up links """
|
||||
@@ -23,18 +24,15 @@ async def get_all_open_sign_ups(
|
||||
return db.sign_ups.get_all(session)
|
||||
|
||||
|
||||
@router.post("", response_model=SignUpToken)
|
||||
@admin_router.post("", response_model=SignUpToken)
|
||||
async def create_user_sign_up_key(
|
||||
background_tasks: BackgroundTasks,
|
||||
key_data: SignUpIn,
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
current_user: UserInDB = Depends(get_admin_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Generates a Random Token that a new user can sign up with """
|
||||
|
||||
if not current_user.admin:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
sign_up = {
|
||||
"token": str(uuid.uuid1().hex),
|
||||
"name": key_data.name,
|
||||
@@ -47,7 +45,7 @@ async def create_user_sign_up_key(
|
||||
return db.sign_ups.create(session, sign_up)
|
||||
|
||||
|
||||
@router.post("/{token}")
|
||||
@public_router.post("/{token}")
|
||||
async def create_user_with_token(
|
||||
background_tasks: BackgroundTasks,
|
||||
token: str,
|
||||
@@ -59,7 +57,7 @@ async def create_user_with_token(
|
||||
# Validate Token
|
||||
db_entry: SignUpOut = db.sign_ups.get(session, token, limit=1)
|
||||
if not db_entry:
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Create User
|
||||
new_user.admin = db_entry.admin
|
||||
@@ -73,14 +71,10 @@ async def create_user_with_token(
|
||||
db.sign_ups.delete(session, token)
|
||||
|
||||
|
||||
@router.delete("/{token}")
|
||||
@admin_router.delete("/{token}")
|
||||
async def delete_token(
|
||||
token: str,
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Removed a token from the database """
|
||||
if not current_user.admin:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
db.sign_ups.delete(session, token)
|
||||
|
||||
Reference in New Issue
Block a user