1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-08-04 21:15:22 +02: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:
sephrat 2021-06-22 20:22:15 +02:00 committed by GitHub
parent f5faff66d3
commit 6320ba7ec5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 456 additions and 347 deletions

View file

@ -42,12 +42,16 @@ def api_routers():
app.include_router(meal_plan_router) app.include_router(meal_plan_router)
# Settings Routes # Settings Routes
app.include_router(settings_router) 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 # Backups/Imports Routes
app.include_router(backup_routes.router) app.include_router(backup_routes.router)
# Migration Routes # Migration Routes
app.include_router(migration_routes.router) 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) app.include_router(utility_routes.router)

View file

@ -1,41 +1,36 @@
from http.client import HTTPException 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.core.root_logger import get_logger
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import generate_session 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.event_notifications import EventNotificationIn, EventNotificationOut
from mealie.schema.events import EventsOut, TestEvent from mealie.schema.events import EventsOut, TestEvent
from mealie.schema.user import UserInDB
from mealie.services.events import test_notification from mealie.services.events import test_notification
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/events", tags=["App Events"]) router = AdminAPIRouter(prefix="/events", tags=["App Events"])
logger = get_logger() logger = get_logger()
@router.get("", response_model=EventsOut) @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 event from the Database """
# Get Item # Get Item
return EventsOut(total=db.events.count_all(session), events=db.events.get_all(session, order_by="time_stamp")) return EventsOut(total=db.events.count_all(session), events=db.events.get_all(session, order_by="time_stamp"))
@router.delete("") @router.delete("")
async def delete_events( async def delete_events(session: Session = Depends(generate_session)):
session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
):
""" Get event from the Database """ """ Get event from the Database """
# Get Item # Get Item
return db.events.delete_all(session) return db.events.delete_all(session)
@router.delete("/{id}") @router.delete("/{id}")
async def delete_event( async def delete_event(id: int, session: Session = Depends(generate_session)):
id: int, session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
):
""" Delete event from the Database """ """ Delete event from the Database """
return db.events.delete(session, id) return db.events.delete(session, id)
@ -44,7 +39,6 @@ async def delete_event(
async def create_event_notification( async def create_event_notification(
event_data: EventNotificationIn, event_data: EventNotificationIn,
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user),
): ):
""" Create event_notification in the Database """ """ Create event_notification in the Database """
@ -55,7 +49,6 @@ async def create_event_notification(
async def test_notification_route( async def test_notification_route(
test_data: TestEvent, test_data: TestEvent,
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user),
): ):
""" Create event_notification in the Database """ """ Create event_notification in the Database """
@ -71,27 +64,21 @@ async def test_notification_route(
@router.get("/notifications", response_model=list[EventNotificationOut]) @router.get("/notifications", response_model=list[EventNotificationOut])
async def get_all_event_notification( async def get_all_event_notification(session: Session = Depends(generate_session)):
session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
):
""" Get all event_notification from the Database """ """ Get all event_notification from the Database """
# Get Item # Get Item
return db.event_notifications.get_all(session, override_schema=EventNotificationOut) return db.event_notifications.get_all(session, override_schema=EventNotificationOut)
@router.put("/notifications/{id}") @router.put("/notifications/{id}")
async def update_event_notification( async def update_event_notification(id: int, session: Session = Depends(generate_session)):
id: int, session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
):
""" Update event_notification in the Database """ """ Update event_notification in the Database """
# Update Item # not yet implemented
return {"details": "not yet implemented"} raise HTTPException(status.HTTP_405_METHOD_NOT_ALLOWED)
@router.delete("/notifications/{id}") @router.delete("/notifications/{id}")
async def delete_event_notification( async def delete_event_notification(id: int, session: Session = Depends(generate_session)):
id: int, session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
):
""" Delete event_notification from the Database """ """ Delete event_notification from the Database """
# Delete Item # Delete Item
return db.event_notifications.delete(session, id) return db.event_notifications.delete(session, id)

View file

@ -2,19 +2,19 @@ import operator
import shutil import shutil
from pathlib import Path 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.config import app_dirs
from mealie.core.root_logger import get_logger from mealie.core.root_logger import get_logger
from mealie.core.security import create_file_token from mealie.core.security import create_file_token
from mealie.db.db_setup import generate_session 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.schema.backup import BackupJob, ImportJob, Imports, LocalBackup
from mealie.services.backups import imports from mealie.services.backups import imports
from mealie.services.backups.exports import backup_all from mealie.services.backups.exports import backup_all
from mealie.services.events import create_backup_event from mealie.services.events import create_backup_event
from sqlalchemy.orm.session import Session 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() logger = get_logger()

View file

@ -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.config import APP_VERSION, app_dirs, settings
from mealie.core.root_logger import LOGGER_FILE from mealie.core.root_logger import LOGGER_FILE
from mealie.core.security import create_file_token from mealie.core.security import create_file_token
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import generate_session 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 mealie.schema.about import AppInfo, AppStatistics, DebugInfo
from sqlalchemy.orm.session import Session 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("") @admin_router.get("")
async def get_debug_info(current_user=Depends(get_current_user)): async def get_debug_info():
""" Returns general information about the application for debugging """ """ Returns general information about the application for debugging """
return DebugInfo( 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)): async def get_app_statistics(session: Session = Depends(generate_session)):
return AppStatistics( return AppStatistics(
total_recipes=db.recipes.count_all(session), 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(): async def get_mealie_version():
""" Returns the current version of mealie""" """ Returns the current version of mealie"""
return AppInfo( return AppInfo(
@ -48,21 +51,21 @@ async def get_mealie_version():
) )
@router.get("/last-recipe-json") @admin_router.get("/last-recipe-json")
async def get_last_recipe_json(current_user=Depends(get_current_user)): async def get_last_recipe_json():
""" Returns a token to download a file """ """ Returns a token to download a file """
return {"fileToken": create_file_token(app_dirs.DEBUG_DIR.joinpath("last_recipe.json"))} return {"fileToken": create_file_token(app_dirs.DEBUG_DIR.joinpath("last_recipe.json"))}
@router.get("/log/{num}") @admin_router.get("/log/{num}")
async def get_log(num: int, current_user=Depends(get_current_user)): async def get_log(num: int):
""" Doc Str """ """ Doc Str """
with open(LOGGER_FILE, "rb") as f: with open(LOGGER_FILE, "rb") as f:
log_text = tail(f, num) log_text = tail(f, num)
return log_text return log_text
@router.get("/log") @admin_router.get("/log")
async def get_log_file(): async def get_log_file():
""" Returns a token to download a file """ """ Returns a token to download a file """
return {"fileToken": create_file_token(LOGGER_FILE)} return {"fileToken": create_file_token(LOGGER_FILE)}

View file

@ -75,6 +75,12 @@ async def get_current_user(token: str = Depends(oauth2_scheme), session=Depends(
return user 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: 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) tokens: list[LongLiveTokenInDB] = db.api_tokens.get(session, id, "parent_id", limit=9999)

View file

@ -1,8 +1,7 @@
from fastapi import APIRouter from fastapi import APIRouter
from . import crud, groups from . import groups
groups_router = APIRouter() groups_router = APIRouter()
groups_router.include_router(crud.router)
groups_router.include_router(groups.router) groups_router.include_router(groups.router)

View file

@ -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.database import db
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user 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.schema.user import GroupBase, GroupInDB, UpdateGroup, UserInDB
from mealie.services.events import create_group_event from mealie.services.events import create_group_event
from sqlalchemy.orm.session import Session 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( async def get_all_groups(
current_user=Depends(get_current_user),
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
""" Returns a list of all groups in the database """ """ Returns a list of all groups in the database """
@ -19,7 +20,7 @@ async def get_all_groups(
return db.groups.get_all(session) 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( async def get_current_user_group(
current_user: UserInDB = Depends(get_current_user), current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
@ -30,11 +31,10 @@ async def get_current_user_group(
return db.groups.get(session, current_user.group, "name") 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( async def create_group(
background_tasks: BackgroundTasks, background_tasks: BackgroundTasks,
group_data: GroupBase, group_data: GroupBase,
current_user=Depends(get_current_user),
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
""" Creates a Group in the Database """ """ Creates a Group in the Database """
@ -46,18 +46,17 @@ async def create_group(
raise HTTPException(status.HTTP_400_BAD_REQUEST) raise HTTPException(status.HTTP_400_BAD_REQUEST)
@router.put("/{id}") @admin_router.put("/{id}")
async def update_group_data( async def update_group_data(
id: int, id: int,
group_data: UpdateGroup, group_data: UpdateGroup,
current_user=Depends(get_current_user),
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
""" Updates a User Group """ """ Updates a User Group """
db.groups.update(session, id, group_data.dict()) db.groups.update(session, id, group_data.dict())
@router.delete("/{id}") @admin_router.delete("/{id}")
async def delete_user_group( async def delete_user_group(
background_tasks: BackgroundTasks, background_tasks: BackgroundTasks,
id: int, id: int,

View file

@ -3,4 +3,5 @@ from mealie.routes.groups import crud
router = APIRouter() router = APIRouter()
router.include_router(crud.router) router.include_router(crud.admin_router)
router.include_router(crud.user_router)

View file

@ -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.database import db
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user from mealie.routes.deps import get_current_user
from mealie.routes.routers import UserAPIRouter
from mealie.schema.meal import MealPlanIn, MealPlanOut from mealie.schema.meal import MealPlanIn, MealPlanOut
from mealie.schema.user import GroupInDB, UserInDB from mealie.schema.user import GroupInDB, UserInDB
from mealie.services.events import create_group_event 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 sqlalchemy.orm.session import Session
from starlette.responses import FileResponse 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]) @router.get("/all", response_model=list[MealPlanOut])

View file

@ -1,8 +1,9 @@
from fastapi import APIRouter, Depends from fastapi import Depends
from mealie.core.root_logger import get_logger from mealie.core.root_logger import get_logger
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user from mealie.routes.deps import get_current_user
from mealie.routes.routers import UserAPIRouter
from mealie.schema.meal import MealPlanOut from mealie.schema.meal import MealPlanOut
from mealie.schema.recipe import Recipe from mealie.schema.recipe import Recipe
from mealie.schema.shopping_list import ListItem, ShoppingListIn, ShoppingListOut from mealie.schema.shopping_list import ListItem, ShoppingListIn, ShoppingListOut
@ -11,7 +12,7 @@ from sqlalchemy.orm.session import Session
logger = get_logger() 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") @router.get("/{id}/shopping-list")

View file

@ -2,16 +2,15 @@ import operator
import shutil import shutil
from typing import List 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.core.config import app_dirs
from mealie.db.db_setup import generate_session 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.schema.migration import MigrationFile, Migrations
from mealie.services.migrations import migration from mealie.services.migrations import migration
from sqlalchemy.orm.session import Session 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]) @router.get("", response_model=List[Migrations])

View file

@ -4,7 +4,12 @@ from mealie.routes.recipe import all_recipe_routes, category_routes, comments, r
recipe_router = APIRouter() recipe_router = APIRouter()
recipe_router.include_router(all_recipe_routes.router) recipe_router.include_router(all_recipe_routes.router)
recipe_router.include_router(recipe_crud_routes.router) recipe_router.include_router(recipe_crud_routes.public_router)
recipe_router.include_router(category_routes.router) recipe_router.include_router(recipe_crud_routes.user_router)
recipe_router.include_router(tag_routes.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) recipe_router.include_router(comments.router)

View file

@ -1,7 +1,7 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import generate_session 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 mealie.schema.recipe import RecipeSummary
from slugify import slugify from slugify import slugify
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
@ -36,21 +36,17 @@ async def get_recipe_summary(
) )
@router.get( @router.get("/api/recipes/summary/untagged", response_model=list[RecipeSummary])
"/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)): async def get_untagged_recipes(count: bool = False, session: Session = Depends(generate_session)):
return db.recipes.count_untagged(session, count=count, override_schema=RecipeSummary) return db.recipes.count_untagged(session, count=count, override_schema=RecipeSummary)
@router.get( @router.get("/api/recipes/summary/uncategorized", response_model=list[RecipeSummary])
"/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)): async def get_uncategorized_recipes(count: bool = False, session: Session = Depends(generate_session)):
return db.recipes.count_uncategorized(session, count=count, override_schema=RecipeSummary) 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)): 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 """ """ 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 # ! 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 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)): 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""" """ 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 # ! This should be refactored into a single database call, but I couldn't figure it out

View file

@ -1,26 +1,29 @@
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import generate_session 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 mealie.schema.category import CategoryIn, RecipeCategoryResponse
from sqlalchemy.orm.session import Session 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)): async def get_all_recipe_categories(session: Session = Depends(generate_session)):
""" Returns a list of available categories in the database """ """ Returns a list of available categories in the database """
return db.categories.get_all_limit_columns(session, ["slug", "name"]) 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)): def get_empty_categories(session: Session = Depends(generate_session)):
""" Returns a list of categories that do not contain any recipes""" """ Returns a list of categories that do not contain any recipes"""
return db.categories.get_empty(session) 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( def get_all_recipes_by_category(
category: str, session: Session = Depends(generate_session), is_user: bool = Depends(is_logged_in) 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 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)): async def create_recipe_category(category: CategoryIn, session: Session = Depends(generate_session)):
""" Creates a Category in the database """ """ 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) 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)): async def update_recipe_category(category: str, new_category: CategoryIn, session: Session = Depends(generate_session)):
""" Updates an existing Tag in the database """ """ 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) 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)): async def delete_recipe_category(category: str, session: Session = Depends(generate_session)):
""" """
Removes a recipe category from the database. Deleting a Removes a recipe category from the database. Deleting a

View file

@ -1,14 +1,15 @@
from http.client import HTTPException 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.database import db
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user 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.comments import CommentIn, CommentOut, CommentSaveToDB
from mealie.schema.user import UserInDB from mealie.schema.user import UserInDB
from sqlalchemy.orm.session import Session 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") @router.post("/recipes/{slug}/comments")
@ -35,7 +36,7 @@ async def update_comment(
old_comment: CommentOut = db.comments.get(session, id) old_comment: CommentOut = db.comments.get(session, id)
if current_user.id != old_comment.user.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) return db.comments.update(session, id, new_comment)
@ -51,4 +52,4 @@ async def delete_comment(
db.comments.delete(session, id) db.comments.delete(session, id)
return return
raise HTTPException(status.HTTP_401_UNAUTHORIZED) raise HTTPException(status.HTTP_403_FORBIDDEN)

View file

@ -1,4 +1,5 @@
import json import json
from mealie.routes.routers import UserAPIRouter
import shutil import shutil
from shutil import copyfileobj from shutil import copyfileobj
from zipfile import ZipFile from zipfile import ZipFile
@ -21,11 +22,12 @@ from slugify import slugify
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
from starlette.responses import FileResponse 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() 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( def create_from_json(
background_tasks: BackgroundTasks, background_tasks: BackgroundTasks,
data: Recipe, data: Recipe,
@ -46,12 +48,12 @@ def create_from_json(
return recipe.slug 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): def test_parse_recipe_url(url: RecipeURLIn):
return scrape_url(url.url) 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( def parse_recipe_url(
background_tasks: BackgroundTasks, background_tasks: BackgroundTasks,
url: RecipeURLIn, url: RecipeURLIn,
@ -74,7 +76,7 @@ def parse_recipe_url(
return recipe.slug 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)): 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 """ """ 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 return recipe
else: 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( async def create_recipe_from_zip(
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
temp_path=Depends(temporary_zip_path), temp_path=Depends(temporary_zip_path),
@ -121,7 +123,7 @@ async def create_recipe_from_zip(
return recipe return recipe
@router.get("/{recipe_slug}/zip") @public_router.get("/{recipe_slug}/zip")
async def get_recipe_as_zip( async def get_recipe_as_zip(
recipe_slug: str, session: Session = Depends(generate_session), temp_path=Depends(temporary_zip_path) 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") 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( def update_recipe(
recipe_slug: str, recipe_slug: str,
data: Recipe, data: Recipe,
@ -154,7 +156,7 @@ def update_recipe(
return recipe return recipe
@router.patch("/{recipe_slug}", dependencies=[Depends(get_current_user)]) @user_router.patch("/{recipe_slug}")
def patch_recipe( def patch_recipe(
recipe_slug: str, recipe_slug: str,
data: Recipe, data: Recipe,
@ -171,7 +173,7 @@ def patch_recipe(
return recipe return recipe
@router.delete("/{recipe_slug}") @user_router.delete("/{recipe_slug}")
def delete_recipe( def delete_recipe(
background_tasks: BackgroundTasks, background_tasks: BackgroundTasks,
recipe_slug: str, recipe_slug: str,
@ -194,7 +196,7 @@ def delete_recipe(
raise HTTPException(status.HTTP_400_BAD_REQUEST) 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( def update_recipe_image(
recipe_slug: str, recipe_slug: str,
image: bytes = File(...), image: bytes = File(...),
@ -208,7 +210,7 @@ def update_recipe_image(
return {"image": new_version} return {"image": new_version}
@router.post("/{recipe_slug}/image", dependencies=[Depends(get_current_user)]) @user_router.post("/{recipe_slug}/image")
def scrape_image_url( def scrape_image_url(
recipe_slug: str, recipe_slug: str,
url: RecipeURLIn, url: RecipeURLIn,
@ -218,7 +220,7 @@ def scrape_image_url(
scrape_image(url.url, recipe_slug) 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( def upload_recipe_asset(
recipe_slug: str, recipe_slug: str,
name: str = Form(...), name: str = Form(...),

View file

@ -1,28 +1,29 @@
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import generate_session 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 mealie.schema.category import RecipeTagResponse, TagIn
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
router = APIRouter(tags=["Recipes"]) public_router = APIRouter(prefix="/api/tags", tags=["Recipe Tags"])
user_router = UserAPIRouter(prefix="/api/tags", tags=["Recipe Tags"])
router = APIRouter(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)): async def get_all_recipe_tags(session: Session = Depends(generate_session)):
""" Returns a list of available tags in the database """ """ Returns a list of available tags in the database """
return db.tags.get_all_limit_columns(session, ["slug", "name"]) 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)): def get_empty_tags(session: Session = Depends(generate_session)):
""" Returns a list of tags that do not contain any recipes""" """ Returns a list of tags that do not contain any recipes"""
return db.tags.get_empty(session) 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( def get_all_recipes_by_tag(
tag: str, session: Session = Depends(generate_session), is_user: bool = Depends(is_logged_in) 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 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)): async def create_recipe_tag(tag: TagIn, session: Session = Depends(generate_session)):
""" Creates a Tag in the database """ """ Creates a Tag in the database """
return db.tags.create(session, tag.dict()) 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)): async def update_recipe_tag(tag: str, new_tag: TagIn, session: Session = Depends(generate_session)):
""" Updates an existing Tag in the database """ """ Updates an existing Tag in the database """
return db.tags.update(session, tag, new_tag.dict()) 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)): async def delete_recipe_tag(tag: str, session: Session = Depends(generate_session)):
"""Removes a recipe tag from the database. Deleting a """Removes a recipe tag from the database. Deleting a
tag does not impact a recipe. The tag will be removed tag does not impact a recipe. The tag will be removed

26
mealie/routes/routers.py Normal file
View 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)])

View file

@ -1,12 +1,13 @@
from fastapi import APIRouter, Depends from fastapi import Depends
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user 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.shopping_list import ShoppingListIn, ShoppingListOut
from mealie.schema.user import UserInDB from mealie.schema.user import UserInDB
from sqlalchemy.orm.session import Session 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) @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) 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)): async def update_shopping_list(id: int, new_data: ShoppingListIn, session: Session = Depends(generate_session)):
""" Update Shopping List in the Database """ """ Update Shopping List in the Database """
return db.shopping_lists.update(session, id, new_data) 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)): async def delete_shopping_list(id: int, session: Session = Depends(generate_session)):
""" Delete Shopping List from the Database """ """ Delete Shopping List from the Database """
return db.shopping_lists.delete(session, id) return db.shopping_lists.delete(session, id)

View file

@ -1,9 +1,10 @@
from fastapi import APIRouter from fastapi import APIRouter
from . import all_settings, custom_pages, site_settings from . import custom_pages, site_settings
settings_router = APIRouter() settings_router = APIRouter()
settings_router.include_router(all_settings.router) settings_router.include_router(custom_pages.public_router)
settings_router.include_router(custom_pages.router) settings_router.include_router(custom_pages.admin_router)
settings_router.include_router(site_settings.router) settings_router.include_router(site_settings.public_router)
settings_router.include_router(site_settings.admin_router)

View file

@ -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)

View file

@ -1,46 +1,41 @@
from mealie.routes.routers import AdminAPIRouter
from typing import Union from typing import Union
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import generate_session 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.settings import CustomPageBase, CustomPageOut
from mealie.schema.user import UserInDB
from sqlalchemy.orm.session import Session 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)): def get_custom_pages(session: Session = Depends(generate_session)):
""" Returns the sites custom pages """ """ Returns the sites custom pages """
return db.custom_pages.get_all(session) return db.custom_pages.get_all(session)
@router.post("") @admin_router.post("")
async def create_new_page( async def create_new_page(
new_page: CustomPageBase, new_page: CustomPageBase,
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user),
): ):
""" Creates a new Custom Page """ """ Creates a new Custom Page """
db.custom_pages.create(session, new_page.dict()) db.custom_pages.create(session, new_page.dict())
@router.put("") @admin_router.put("")
async def update_multiple_pages( async def update_multiple_pages(pages: list[CustomPageOut], session: Session = Depends(generate_session)):
pages: list[CustomPageOut],
session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user),
):
""" Update multiple custom pages """ """ Update multiple custom pages """
for page in pages: for page in pages:
db.custom_pages.update(session, page.id, page.dict()) db.custom_pages.update(session, page.id, page.dict())
@router.get("/{id}") @public_router.get("/{id}")
async def get_single_page( async def get_single_page(
id: Union[int, str], id: Union[int, str],
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
@ -52,23 +47,21 @@ async def get_single_page(
return db.custom_pages.get(session, id, "slug") return db.custom_pages.get(session, id, "slug")
@router.put("/{id}") @admin_router.put("/{id}")
async def update_single_page( async def update_single_page(
data: CustomPageOut, data: CustomPageOut,
id: int, id: int,
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
current_user=Depends(get_current_user),
): ):
""" Removes a custom page from the database """ """ Removes a custom page from the database """
return db.custom_pages.update(session, id, data.dict()) return db.custom_pages.update(session, id, data.dict())
@router.delete("/{id}") @admin_router.delete("/{id}")
async def delete_custom_page( async def delete_custom_page(
id: int, id: int,
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user),
): ):
""" Removes a custom page from the database """ """ Removes a custom page from the database """

View file

@ -2,22 +2,24 @@ from fastapi import APIRouter, Depends, HTTPException, status
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user from mealie.routes.deps import get_current_user
from mealie.routes.routers import AdminAPIRouter
from mealie.schema.settings import SiteSettings from mealie.schema.settings import SiteSettings
from mealie.schema.user import GroupInDB, UserInDB from mealie.schema.user import GroupInDB, UserInDB
from mealie.utils.post_webhooks import post_webhooks from mealie.utils.post_webhooks import post_webhooks
from sqlalchemy.orm.session import Session 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)): def get_main_settings(session: Session = Depends(generate_session)):
""" Returns basic site settings """ """ Returns basic site settings """
return db.settings.get(session, 1) return db.settings.get(session, 1)
@router.put("", dependencies=[Depends(get_current_user)]) @admin_router.put("")
def update_settings( def update_settings(
data: SiteSettings, data: SiteSettings,
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
@ -26,7 +28,7 @@ def update_settings(
db.settings.update(session, 1, data.dict()) db.settings.update(session, 1, data.dict())
@router.post("/webhooks/test") @admin_router.post("/webhooks/test")
def test_webhooks( def test_webhooks(
current_user: UserInDB = Depends(get_current_user), current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session), session: Session = Depends(generate_session),

View file

@ -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.database import db
from mealie.db.db_setup import generate_session 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 mealie.schema.theme import SiteTheme
from sqlalchemy.orm.session import Session 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)): def get_all_themes(session: Session = Depends(generate_session)):
""" Returns all site themes """ """ Returns all site themes """
return db.themes.get_all(session) return db.themes.get_all(session)
@router.post("/themes/create", status_code=status.HTTP_201_CREATED) @user_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)): def create_theme(data: SiteTheme, session: Session = Depends(generate_session)):
""" Creates a site color theme database entry """ """ Creates a site color theme database entry """
db.themes.create(session, data.dict()) 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)): def get_single_theme(id: int, session: Session = Depends(generate_session)):
""" Returns a named theme """ """ Returns a named theme """
return db.themes.get(session, id) 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( def update_theme(
id: int, id: int,
data: SiteTheme, data: SiteTheme,
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
current_user=Depends(get_current_user),
): ):
""" Update a theme database entry """ """ Update a theme database entry """
db.themes.update(session, id, data.dict()) db.themes.update(session, id, data.dict())
@router.delete("/themes/{id}", status_code=status.HTTP_200_OK) @user_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)): def delete_theme(id: int, session: Session = Depends(generate_session)):
""" Deletes theme from the database """ """ Deletes theme from the database """
try: try:
db.themes.delete(session, id) db.themes.delete(session, id)

View file

@ -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.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() logger = get_logger()

View file

@ -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.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() logger = get_logger()

View file

@ -4,7 +4,11 @@ from . import api_tokens, auth, crud, sign_up
user_router = APIRouter() user_router = APIRouter()
user_router.include_router(auth.router) user_router.include_router(auth.public_router)
user_router.include_router(sign_up.router) user_router.include_router(auth.user_router)
user_router.include_router(crud.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) user_router.include_router(api_tokens.router)

View file

@ -1,15 +1,16 @@
from datetime import timedelta from datetime import timedelta
from fastapi import APIRouter, HTTPException, status from fastapi import HTTPException, status
from fastapi.param_functions import Depends from fastapi.param_functions import Depends
from mealie.core.security import create_access_token from mealie.core.security import create_access_token
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user from mealie.routes.deps import get_current_user
from mealie.routes.routers import UserAPIRouter
from mealie.schema.user import CreateToken, LoingLiveTokenIn, LongLiveTokenInDB, UserInDB from mealie.schema.user import CreateToken, LoingLiveTokenIn, LongLiveTokenInDB, UserInDB
from sqlalchemy.orm.session import Session 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) @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) deleted_token = db.api_tokens.delete(session, token_id)
return {"token_delete": deleted_token.name} return {"token_delete": deleted_token.name}
else: else:
raise HTTPException(status.HTTP_401_UNAUTHORIZED) raise HTTPException(status.HTTP_403_FORBIDDEN)

View file

@ -5,15 +5,17 @@ from mealie.core import security
from mealie.core.security import authenticate_user from mealie.core.security import authenticate_user
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user from mealie.routes.deps import get_current_user
from mealie.routes.routers import UserAPIRouter
from mealie.schema.user import UserInDB from mealie.schema.user import UserInDB
from mealie.services.events import create_user_event from mealie.services.events import create_user_event
from sqlalchemy.orm.session import Session 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") @public_router.post("/token/long")
@router.post("/token") @public_router.post("/token")
def get_token( def get_token(
background_tasks: BackgroundTasks, background_tasks: BackgroundTasks,
request: Request, request: Request,
@ -38,7 +40,7 @@ def get_token(
return {"access_token": access_token, "token_type": "bearer"} 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)): async def refresh_token(current_user: UserInDB = Depends(get_current_user)):
""" Use a valid token to get another token""" """ Use a valid token to get another token"""
access_token = security.create_access_token(data=dict(sub=current_user.email)) access_token = security.create_access_token(data=dict(sub=current_user.email))

View file

@ -1,21 +1,34 @@
import shutil 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.responses import FileResponse
from fastapi.routing import APIRouter
from mealie.core import security from mealie.core import security
from mealie.core.config import app_dirs, settings from mealie.core.config import app_dirs, settings
from mealie.core.security import get_password_hash, verify_password from mealie.core.security import get_password_hash, verify_password
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user 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.schema.user import ChangePassword, UserBase, UserFavorites, UserIn, UserInDB, UserOut
from mealie.services.events import create_user_event from mealie.services.events import create_user_event
from sqlalchemy.orm.session import Session 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( async def create_user(
background_tasks: BackgroundTasks, background_tasks: BackgroundTasks,
new_user: UserIn, new_user: UserIn,
@ -30,26 +43,19 @@ async def create_user(
return db.users.create(session, new_user.dict()) return db.users.create(session, new_user.dict())
@router.get("", response_model=list[UserOut]) @admin_router.get("", response_model=list[UserOut])
async def get_all_users( async def get_all_users(session: Session = Depends(generate_session)):
current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session),
):
if not current_user.admin:
raise HTTPException(status.HTTP_403_FORBIDDEN)
return db.users.get_all(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( async def get_logged_in_user(
current_user: UserInDB = Depends(get_current_user), current_user: UserInDB = Depends(get_current_user),
): ):
return current_user.dict() 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( async def get_user_by_id(
id: int, id: int,
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
@ -57,7 +63,7 @@ async def get_user_by_id(
return db.users.get(session, 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( async def reset_user_password(
id: int, id: int,
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
@ -67,7 +73,7 @@ async def reset_user_password(
db.users.update_password(session, id, new_password) db.users.update_password(session, id, new_password)
@router.put("/{id}") @user_router.put("/{id}")
async def update_user( async def update_user(
id: int, id: int,
new_data: UserBase, new_data: UserBase,
@ -75,8 +81,16 @@ async def update_user(
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
token = None assert_user_change_allowed(id)
if current_user.id == id or current_user.admin:
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()) db.users.update(session, id, new_data.dict())
if current_user.id == id: if current_user.id == id:
access_token = security.create_access_token(data=dict(sub=new_data.email)) access_token = security.create_access_token(data=dict(sub=new_data.email))
@ -84,7 +98,7 @@ async def update_user(
return token return token
@router.get("/{id}/image") @public_router.get("/{id}/image")
async def get_user_image(id: str): async def get_user_image(id: str):
""" Returns a users profile picture """ """ Returns a users profile picture """
user_dir = app_dirs.USER_DIR.joinpath(id) 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) 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( async def update_user_image(
id: str, id: str,
profile_image: UploadFile = File(...), profile_image: UploadFile = File(...),
): ):
""" Updates a User Image """ """ Updates a User Image """
assert_user_change_allowed(id)
extension = profile_image.filename.split(".")[-1] extension = profile_image.filename.split(".")[-1]
app_dirs.USER_DIR.joinpath(id).mkdir(parents=True, exist_ok=True) 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) raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
@router.put("/{id}/password") @user_router.put("/{id}/password")
async def update_password( async def update_password(
id: int, id: int,
password_change: ChangePassword, password_change: ChangePassword,
@ -125,24 +141,24 @@ async def update_password(
): ):
""" Resets the User Password""" """ Resets the User Password"""
assert_user_change_allowed(id)
match_passwords = verify_password(password_change.current_password, current_user.password) match_passwords = verify_password(password_change.current_password, current_user.password)
match_id = current_user.id == id
if not (match_passwords and match_id): if not (match_passwords):
raise HTTPException(status.HTTP_401_UNAUTHORIZED) raise HTTPException(status.HTTP_400_BAD_REQUEST)
new_password = get_password_hash(password_change.new_password) new_password = get_password_hash(password_change.new_password)
db.users.update_password(session, id, 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)): 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) return db.users.get(session, id, override_schema=UserFavorites)
@router.post("/{id}/favorites/{slug}") @user_router.post("/{id}/favorites/{slug}")
async def add_favorite( async def add_favorite(
slug: str, slug: str,
current_user: UserInDB = Depends(get_current_user), current_user: UserInDB = Depends(get_current_user),
@ -150,12 +166,13 @@ async def add_favorite(
): ):
""" Adds a Recipe to the users favorites """ """ Adds a Recipe to the users favorites """
assert_user_change_allowed(id)
current_user.favorite_recipes.append(slug) current_user.favorite_recipes.append(slug)
db.users.update(session, current_user.id, current_user) db.users.update(session, current_user.id, current_user)
@router.delete("/{id}/favorites/{slug}") @user_router.delete("/{id}/favorites/{slug}")
async def remove_favorite( async def remove_favorite(
slug: str, slug: str,
current_user: UserInDB = Depends(get_current_user), current_user: UserInDB = Depends(get_current_user),
@ -163,6 +180,7 @@ async def remove_favorite(
): ):
""" Adds a Recipe to the users favorites """ """ 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] current_user.favorite_recipes = [x for x in current_user.favorite_recipes if x != slug]
db.users.update(session, current_user.id, current_user) db.users.update(session, current_user.id, current_user)
@ -170,19 +188,19 @@ async def remove_favorite(
return return
@router.delete("/{id}") @admin_router.delete("/{id}")
async def delete_user( async def delete_user(
background_tasks: BackgroundTasks, background_tasks: BackgroundTasks,
id: int, id: int,
current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
""" Removes a user from the database. Must be the current user or a super user""" """ Removes a user from the database. Must be the current user or a super user"""
assert_user_change_allowed(id)
if id == 1: if id == 1:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="SUPER_USER") raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="SUPER_USER")
if current_user.id == id or current_user.admin:
try: try:
db.users.delete(session, id) db.users.delete(session, id)
background_tasks.add_task(create_user_event, "User Deleted", f"User ID: {id}", session=session) background_tasks.add_task(create_user_event, "User Deleted", f"User ID: {id}", session=session)

View file

@ -4,18 +4,19 @@ from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status
from mealie.core.security import get_password_hash from mealie.core.security import get_password_hash
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import generate_session 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.sign_up import SignUpIn, SignUpOut, SignUpToken
from mealie.schema.user import UserIn, UserInDB from mealie.schema.user import UserIn, UserInDB
from mealie.services.events import create_user_event from mealie.services.events import create_user_event
from sqlalchemy.orm.session import Session 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( async def get_all_open_sign_ups(
current_user=Depends(get_current_user),
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
""" Returns a list of open sign up links """ """ 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) 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( async def create_user_sign_up_key(
background_tasks: BackgroundTasks, background_tasks: BackgroundTasks,
key_data: SignUpIn, key_data: SignUpIn,
current_user: UserInDB = Depends(get_current_user), current_user: UserInDB = Depends(get_admin_user),
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
""" Generates a Random Token that a new user can sign up with """ """ 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 = { sign_up = {
"token": str(uuid.uuid1().hex), "token": str(uuid.uuid1().hex),
"name": key_data.name, "name": key_data.name,
@ -47,7 +45,7 @@ async def create_user_sign_up_key(
return db.sign_ups.create(session, sign_up) return db.sign_ups.create(session, sign_up)
@router.post("/{token}") @public_router.post("/{token}")
async def create_user_with_token( async def create_user_with_token(
background_tasks: BackgroundTasks, background_tasks: BackgroundTasks,
token: str, token: str,
@ -59,7 +57,7 @@ async def create_user_with_token(
# Validate Token # Validate Token
db_entry: SignUpOut = db.sign_ups.get(session, token, limit=1) db_entry: SignUpOut = db.sign_ups.get(session, token, limit=1)
if not db_entry: if not db_entry:
raise HTTPException(status.HTTP_401_UNAUTHORIZED) raise HTTPException(status.HTTP_400_BAD_REQUEST)
# Create User # Create User
new_user.admin = db_entry.admin new_user.admin = db_entry.admin
@ -73,14 +71,10 @@ async def create_user_with_token(
db.sign_ups.delete(session, token) db.sign_ups.delete(session, token)
@router.delete("/{token}") @admin_router.delete("/{token}")
async def delete_token( async def delete_token(
token: str, token: str,
current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
""" Removed a token from the database """ """ Removed a token from the database """
if not current_user.admin:
raise HTTPException(status.HTTP_403_FORBIDDEN)
db.sign_ups.delete(session, token) db.sign_ups.delete(session, token)

View file

@ -49,16 +49,39 @@ def test_image_png():
return TEST_DATA.joinpath("images", "test_image.png") return TEST_DATA.joinpath("images", "test_image.png")
@fixture(scope="session") def login(form_data, api_client: requests, api_routes: AppRoutes):
def token(api_client: requests, api_routes: AppRoutes):
form_data = {"username": "changeme@email.com", "password": settings.DEFAULT_PASSWORD}
response = api_client.post(api_routes.auth_token, form_data) response = api_client.post(api_routes.auth_token, form_data)
assert response.status_code == 200
token = json.loads(response.text).get("access_token") token = json.loads(response.text).get("access_token")
return {"Authorization": f"Bearer {token}"} return {"Authorization": f"Bearer {token}"}
@fixture(scope="session")
def admin_token(api_client: requests, api_routes: AppRoutes):
form_data = {"username": "changeme@email.com", "password": settings.DEFAULT_PASSWORD}
return login(form_data, api_client, api_routes)
@fixture(scope="session")
def user_token(admin_token, api_client: requests, api_routes: AppRoutes):
# Create the user
create_data = {
"fullName": "User",
"email": "user@email.com",
"password": "useruser",
"group": "Home",
"admin": False,
"tokens": [],
}
response = api_client.post(api_routes.users, json=create_data, headers=admin_token)
assert response.status_code == 201
# Log in as this user
form_data = {"username": "user@email.com", "password": "useruser"}
return login(form_data, api_client, api_routes)
@fixture(scope="session") @fixture(scope="session")
def raw_recipe(): def raw_recipe():
return get_raw_recipe() return get_raw_recipe()

View file

@ -10,35 +10,35 @@ recipe_test_data = get_recipe_test_cases()
@pytest.mark.parametrize("recipe_data", recipe_test_data) @pytest.mark.parametrize("recipe_data", recipe_test_data)
def test_create_by_url(api_client: TestClient, api_routes: AppRoutes, recipe_data: RecipeSiteTestCase, token): def test_create_by_url(api_client: TestClient, api_routes: AppRoutes, recipe_data: RecipeSiteTestCase, user_token):
api_client.delete(api_routes.recipes_recipe_slug(recipe_data.expected_slug), headers=token) api_client.delete(api_routes.recipes_recipe_slug(recipe_data.expected_slug), headers=user_token)
response = api_client.post(api_routes.recipes_create_url, json={"url": recipe_data.url}, headers=token) response = api_client.post(api_routes.recipes_create_url, json={"url": recipe_data.url}, headers=user_token)
assert response.status_code == 201 assert response.status_code == 201
assert json.loads(response.text) == recipe_data.expected_slug assert json.loads(response.text) == recipe_data.expected_slug
def test_create_by_json(api_client: TestClient, api_routes: AppRoutes, token, raw_recipe): def test_create_by_json(api_client: TestClient, api_routes: AppRoutes, user_token, raw_recipe):
recipe_url = api_routes.recipes_recipe_slug("banana-bread") recipe_url = api_routes.recipes_recipe_slug("banana-bread")
api_client.delete(recipe_url, headers=token) api_client.delete(recipe_url, headers=user_token)
response = api_client.post(api_routes.recipes_create, json=raw_recipe, headers=token) response = api_client.post(api_routes.recipes_create, json=raw_recipe, headers=user_token)
assert response.status_code == 201 assert response.status_code == 201
assert json.loads(response.text) == "banana-bread" assert json.loads(response.text) == "banana-bread"
def test_create_no_image(api_client: TestClient, api_routes: AppRoutes, token, raw_recipe_no_image): def test_create_no_image(api_client: TestClient, api_routes: AppRoutes, user_token, raw_recipe_no_image):
response = api_client.post(api_routes.recipes_create, json=raw_recipe_no_image, headers=token) response = api_client.post(api_routes.recipes_create, json=raw_recipe_no_image, headers=user_token)
assert response.status_code == 201 assert response.status_code == 201
assert json.loads(response.text) == "banana-bread-no-image" assert json.loads(response.text) == "banana-bread-no-image"
@pytest.mark.parametrize("recipe_data", recipe_test_data) @pytest.mark.parametrize("recipe_data", recipe_test_data)
def test_read_update(api_client: TestClient, api_routes: AppRoutes, recipe_data: RecipeSiteTestCase, token): def test_read_update(api_client: TestClient, api_routes: AppRoutes, recipe_data: RecipeSiteTestCase, user_token):
recipe_url = api_routes.recipes_recipe_slug(recipe_data.expected_slug) recipe_url = api_routes.recipes_recipe_slug(recipe_data.expected_slug)
response = api_client.get(recipe_url, headers=token) response = api_client.get(recipe_url, headers=user_token)
assert response.status_code == 200 assert response.status_code == 200
recipe = json.loads(response.text) recipe = json.loads(response.text)
@ -54,7 +54,7 @@ def test_read_update(api_client: TestClient, api_routes: AppRoutes, recipe_data:
test_categories = ["one", "two", "three"] test_categories = ["one", "two", "three"]
recipe["recipeCategory"] = test_categories recipe["recipeCategory"] = test_categories
response = api_client.put(recipe_url, json=recipe, headers=token) response = api_client.put(recipe_url, json=recipe, headers=user_token)
assert response.status_code == 200 assert response.status_code == 200
assert json.loads(response.text).get("slug") == recipe_data.expected_slug assert json.loads(response.text).get("slug") == recipe_data.expected_slug
@ -69,9 +69,9 @@ def test_read_update(api_client: TestClient, api_routes: AppRoutes, recipe_data:
@pytest.mark.parametrize("recipe_data", recipe_test_data) @pytest.mark.parametrize("recipe_data", recipe_test_data)
def test_rename(api_client: TestClient, api_routes: AppRoutes, recipe_data: RecipeSiteTestCase, token): def test_rename(api_client: TestClient, api_routes: AppRoutes, recipe_data: RecipeSiteTestCase, user_token):
recipe_url = api_routes.recipes_recipe_slug(recipe_data.expected_slug) recipe_url = api_routes.recipes_recipe_slug(recipe_data.expected_slug)
response = api_client.get(recipe_url, headers=token) response = api_client.get(recipe_url, headers=user_token)
assert response.status_code == 200 assert response.status_code == 200
recipe = json.loads(response.text) recipe = json.loads(response.text)
@ -79,7 +79,7 @@ def test_rename(api_client: TestClient, api_routes: AppRoutes, recipe_data: Reci
new_slug = slugify(new_name) new_slug = slugify(new_name)
recipe["name"] = new_name recipe["name"] = new_name
response = api_client.put(recipe_url, json=recipe, headers=token) response = api_client.put(recipe_url, json=recipe, headers=user_token)
assert response.status_code == 200 assert response.status_code == 200
assert json.loads(response.text).get("slug") == new_slug assert json.loads(response.text).get("slug") == new_slug
@ -88,7 +88,7 @@ def test_rename(api_client: TestClient, api_routes: AppRoutes, recipe_data: Reci
@pytest.mark.parametrize("recipe_data", recipe_test_data) @pytest.mark.parametrize("recipe_data", recipe_test_data)
def test_delete(api_client: TestClient, api_routes: AppRoutes, recipe_data: RecipeSiteTestCase, token): def test_delete(api_client: TestClient, api_routes: AppRoutes, recipe_data: RecipeSiteTestCase, user_token):
recipe_url = api_routes.recipes_recipe_slug(recipe_data.expected_slug) recipe_url = api_routes.recipes_recipe_slug(recipe_data.expected_slug)
response = api_client.delete(recipe_url, headers=token) response = api_client.delete(recipe_url, headers=user_token)
assert response.status_code == 200 assert response.status_code == 200

View file

@ -10,8 +10,8 @@ def page_data():
return {"name": "My New Page", "position": 0, "categories": []} return {"name": "My New Page", "position": 0, "categories": []}
def test_create_page(api_client: TestClient, api_routes: AppRoutes, token, page_data): def test_create_page(api_client: TestClient, api_routes: AppRoutes, admin_token, page_data):
response = api_client.post(api_routes.site_settings_custom_pages, json=page_data, headers=token) response = api_client.post(api_routes.site_settings_custom_pages, json=page_data, headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200
@ -25,16 +25,16 @@ def test_read_page(api_client: TestClient, api_routes: AppRoutes, page_data):
assert json.loads(response.text) == page_data assert json.loads(response.text) == page_data
def test_update_page(api_client: TestClient, api_routes: AppRoutes, page_data, token): def test_update_page(api_client: TestClient, api_routes: AppRoutes, page_data, admin_token):
page_data["id"] = 1 page_data["id"] = 1
page_data["name"] = "My New Name" page_data["name"] = "My New Name"
response = api_client.put(api_routes.site_settings_custom_pages_id(1), json=page_data, headers=token) response = api_client.put(api_routes.site_settings_custom_pages_id(1), json=page_data, headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200
def test_delete_page(api_client: TestClient, api_routes: AppRoutes, token): def test_delete_page(api_client: TestClient, api_routes: AppRoutes, admin_token):
response = api_client.delete(api_routes.site_settings_custom_pages_id(1), headers=token) response = api_client.delete(api_routes.site_settings_custom_pages_id(1), headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200

View file

@ -10,20 +10,20 @@ def group_data():
return {"name": "Test Group"} return {"name": "Test Group"}
def test_create_group(api_client: TestClient, api_routes: AppRoutes, token): def test_create_group(api_client: TestClient, api_routes: AppRoutes, admin_token):
response = api_client.post(api_routes.groups, json={"name": "Test Group"}, headers=token) response = api_client.post(api_routes.groups, json={"name": "Test Group"}, headers=admin_token)
assert response.status_code == 201 assert response.status_code == 201
def test_get_self_group(api_client: TestClient, api_routes: AppRoutes, token): def test_get_self_group(api_client: TestClient, api_routes: AppRoutes, admin_token):
response = api_client.get(api_routes.groups, headers=token) response = api_client.get(api_routes.groups, headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200
assert len(json.loads(response.text)) >= 2 assert len(json.loads(response.text)) >= 2
def test_update_group(api_client: TestClient, api_routes: AppRoutes, token): def test_update_group(api_client: TestClient, api_routes: AppRoutes, admin_token):
new_data = { new_data = {
"name": "New Group Name", "name": "New Group Name",
"id": 2, "id": 2,
@ -36,23 +36,23 @@ def test_update_group(api_client: TestClient, api_routes: AppRoutes, token):
"shoppingLists": [], "shoppingLists": [],
} }
# Test Update # Test Update
response = api_client.put(api_routes.groups_id(2), json=new_data, headers=token) response = api_client.put(api_routes.groups_id(2), json=new_data, headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200
# Validate Changes # Validate Changes
response = api_client.get(api_routes.groups, headers=token) response = api_client.get(api_routes.groups, headers=admin_token)
all_groups = json.loads(response.text) all_groups = json.loads(response.text)
id_2 = filter(lambda x: x["id"] == 2, all_groups) id_2 = filter(lambda x: x["id"] == 2, all_groups)
assert next(id_2) == new_data assert next(id_2) == new_data
def test_home_group_not_deletable(api_client: TestClient, api_routes: AppRoutes, token): def test_home_group_not_deletable(api_client: TestClient, api_routes: AppRoutes, admin_token):
response = api_client.delete(api_routes.groups_id(1), headers=token) response = api_client.delete(api_routes.groups_id(1), headers=admin_token)
assert response.status_code == 400 assert response.status_code == 400
def test_delete_group(api_client: TestClient, api_routes: AppRoutes, token): def test_delete_group(api_client: TestClient, api_routes: AppRoutes, admin_token):
response = api_client.delete(api_routes.groups_id(2), headers=token) response = api_client.delete(api_routes.groups_id(2), headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200

View file

@ -19,9 +19,9 @@ def backup_data():
} }
def test_import(api_client: TestClient, api_routes: AppRoutes, backup_data, token): def test_import(api_client: TestClient, api_routes: AppRoutes, backup_data, admin_token):
import_route = api_routes.backups_file_name_import("test_backup_2021-Apr-27.zip") import_route = api_routes.backups_file_name_import("test_backup_2021-Apr-27.zip")
response = api_client.post(import_route, json=backup_data, headers=token) response = api_client.post(import_route, json=backup_data, headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200
for _, value in json.loads(response.content).items(): for _, value in json.loads(response.content).items():
for v in value: for v in value:

View file

@ -6,15 +6,15 @@ from tests.app_routes import AppRoutes
@fixture @fixture
def long_live_token(api_client: TestClient, api_routes: AppRoutes, token): def long_live_token(api_client: TestClient, api_routes: AppRoutes, admin_token):
response = api_client.post(api_routes.users_api_tokens, json={"name": "Test Fixture Token"}, headers=token) response = api_client.post(api_routes.users_api_tokens, json={"name": "Test Fixture Token"}, headers=admin_token)
assert response.status_code == 201 assert response.status_code == 201
return {"Authorization": f"Bearer {json.loads(response.text).get('token')}"} return {"Authorization": f"Bearer {json.loads(response.text).get('token')}"}
def test_api_token_creation(api_client: TestClient, api_routes: AppRoutes, token): def test_api_token_creation(api_client: TestClient, api_routes: AppRoutes, admin_token):
response = api_client.post(api_routes.users_api_tokens, json={"name": "Test API Token"}, headers=token) response = api_client.post(api_routes.users_api_tokens, json={"name": "Test API Token"}, headers=admin_token)
assert response.status_code == 201 assert response.status_code == 201
@ -24,9 +24,9 @@ def test_use_token(api_client: TestClient, api_routes: AppRoutes, long_live_toke
assert response.status_code == 200 assert response.status_code == 200
def test_delete_token(api_client: TestClient, api_routes: AppRoutes, token): def test_delete_token(api_client: TestClient, api_routes: AppRoutes, admin_token):
response = api_client.delete(api_routes.users_api_tokens_token_id(1), headers=token) response = api_client.delete(api_routes.users_api_tokens_token_id(1), headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200
response = api_client.delete(api_routes.users_api_tokens_token_id(2), headers=token) response = api_client.delete(api_routes.users_api_tokens_token_id(2), headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200

View file

@ -25,8 +25,8 @@ def get_meal_plan_template(first=None, second=None):
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def slug_1(api_client: TestClient, api_routes: AppRoutes, token, recipe_store: list[RecipeSiteTestCase]): def slug_1(api_client: TestClient, api_routes: AppRoutes, admin_token, recipe_store: list[RecipeSiteTestCase]):
slug_1 = api_client.post(api_routes.recipes_create_url, json={"url": recipe_store[0].url}, headers=token) slug_1 = api_client.post(api_routes.recipes_create_url, json={"url": recipe_store[0].url}, headers=admin_token)
slug_1 = json.loads(slug_1.content) slug_1 = json.loads(slug_1.content)
yield slug_1 yield slug_1
@ -35,8 +35,8 @@ def slug_1(api_client: TestClient, api_routes: AppRoutes, token, recipe_store: l
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def slug_2(api_client: TestClient, api_routes: AppRoutes, token, recipe_store: list[RecipeSiteTestCase]): def slug_2(api_client: TestClient, api_routes: AppRoutes, admin_token, recipe_store: list[RecipeSiteTestCase]):
slug_2 = api_client.post(api_routes.recipes_create_url, json={"url": recipe_store[1].url}, headers=token) slug_2 = api_client.post(api_routes.recipes_create_url, json={"url": recipe_store[1].url}, headers=admin_token)
slug_2 = json.loads(slug_2.content) slug_2 = json.loads(slug_2.content)
yield slug_2 yield slug_2
@ -44,15 +44,15 @@ def slug_2(api_client: TestClient, api_routes: AppRoutes, token, recipe_store: l
api_client.delete(api_routes.recipes_recipe_slug(slug_2)) api_client.delete(api_routes.recipes_recipe_slug(slug_2))
def test_create_mealplan(api_client: TestClient, api_routes: AppRoutes, slug_1, slug_2, token): def test_create_mealplan(api_client: TestClient, api_routes: AppRoutes, slug_1, slug_2, admin_token):
meal_plan = get_meal_plan_template(slug_1, slug_2) meal_plan = get_meal_plan_template(slug_1, slug_2)
response = api_client.post(api_routes.meal_plans_create, json=meal_plan, headers=token) response = api_client.post(api_routes.meal_plans_create, json=meal_plan, headers=admin_token)
assert response.status_code == 201 assert response.status_code == 201
def test_read_mealplan(api_client: TestClient, api_routes: AppRoutes, slug_1, slug_2, token): def test_read_mealplan(api_client: TestClient, api_routes: AppRoutes, slug_1, slug_2, admin_token):
response = api_client.get(api_routes.meal_plans_all, headers=token) response = api_client.get(api_routes.meal_plans_all, headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200
@ -65,9 +65,9 @@ def test_read_mealplan(api_client: TestClient, api_routes: AppRoutes, slug_1, sl
assert meals[1]["meals"][0]["slug"] == meal_plan_template["planDays"][1]["meals"][0]["slug"] assert meals[1]["meals"][0]["slug"] == meal_plan_template["planDays"][1]["meals"][0]["slug"]
def test_update_mealplan(api_client: TestClient, api_routes: AppRoutes, slug_1, slug_2, token): def test_update_mealplan(api_client: TestClient, api_routes: AppRoutes, slug_1, slug_2, admin_token):
response = api_client.get(api_routes.meal_plans_all, headers=token) response = api_client.get(api_routes.meal_plans_all, headers=admin_token)
existing_mealplan = json.loads(response.text) existing_mealplan = json.loads(response.text)
existing_mealplan = existing_mealplan[0] existing_mealplan = existing_mealplan[0]
@ -77,11 +77,11 @@ def test_update_mealplan(api_client: TestClient, api_routes: AppRoutes, slug_1,
existing_mealplan["planDays"][0]["meals"][0]["slug"] = slug_2 existing_mealplan["planDays"][0]["meals"][0]["slug"] = slug_2
existing_mealplan["planDays"][1]["meals"][0]["slug"] = slug_1 existing_mealplan["planDays"][1]["meals"][0]["slug"] = slug_1
response = api_client.put(api_routes.meal_plans_plan_id(plan_uid), json=existing_mealplan, headers=token) response = api_client.put(api_routes.meal_plans_plan_id(plan_uid), json=existing_mealplan, headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200
response = api_client.get(api_routes.meal_plans_all, headers=token) response = api_client.get(api_routes.meal_plans_all, headers=admin_token)
existing_mealplan = json.loads(response.text) existing_mealplan = json.loads(response.text)
existing_mealplan = existing_mealplan[0] existing_mealplan = existing_mealplan[0]
@ -89,14 +89,14 @@ def test_update_mealplan(api_client: TestClient, api_routes: AppRoutes, slug_1,
assert existing_mealplan["planDays"][1]["meals"][0]["slug"] == slug_1 assert existing_mealplan["planDays"][1]["meals"][0]["slug"] == slug_1
def test_delete_mealplan(api_client: TestClient, api_routes: AppRoutes, token): def test_delete_mealplan(api_client: TestClient, api_routes: AppRoutes, admin_token):
response = api_client.get(api_routes.meal_plans_all, headers=token) response = api_client.get(api_routes.meal_plans_all, headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200
existing_mealplan = json.loads(response.text) existing_mealplan = json.loads(response.text)
existing_mealplan = existing_mealplan[0] existing_mealplan = existing_mealplan[0]
plan_uid = existing_mealplan.get("uid") plan_uid = existing_mealplan.get("uid")
response = api_client.delete(api_routes.meal_plans_plan_id(plan_uid), headers=token) response = api_client.delete(api_routes.meal_plans_plan_id(plan_uid), headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200

View file

@ -22,22 +22,22 @@ def chowdown_zip():
zip_copy.unlink() zip_copy.unlink()
def test_upload_chowdown_zip(api_client: TestClient, api_routes: AppRoutes, chowdown_zip: Path, token): def test_upload_chowdown_zip(api_client: TestClient, api_routes: AppRoutes, chowdown_zip: Path, admin_token):
upload_url = api_routes.migrations_import_type_upload("chowdown") upload_url = api_routes.migrations_import_type_upload("chowdown")
response = api_client.post(upload_url, files={"archive": chowdown_zip.open("rb")}, headers=token) response = api_client.post(upload_url, files={"archive": chowdown_zip.open("rb")}, headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200
assert app_dirs.MIGRATION_DIR.joinpath("chowdown", chowdown_zip.name).is_file() assert app_dirs.MIGRATION_DIR.joinpath("chowdown", chowdown_zip.name).is_file()
def test_import_chowdown_directory(api_client: TestClient, api_routes: AppRoutes, chowdown_zip: Path, token): def test_import_chowdown_directory(api_client: TestClient, api_routes: AppRoutes, chowdown_zip: Path, admin_token):
delete_url = api_routes.recipes_recipe_slug("roasted-okra") delete_url = api_routes.recipes_recipe_slug("roasted-okra")
api_client.delete(delete_url, headers=token) # TODO: Manage Test Data better api_client.delete(delete_url, headers=admin_token) # TODO: Manage Test Data better
selection = chowdown_zip.name selection = chowdown_zip.name
import_url = api_routes.migrations_import_type_file_name_import("chowdown", selection) import_url = api_routes.migrations_import_type_file_name_import("chowdown", selection)
response = api_client.post(import_url, headers=token) response = api_client.post(import_url, headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200
@ -47,10 +47,10 @@ def test_import_chowdown_directory(api_client: TestClient, api_routes: AppRoutes
assert report.get("status") is True assert report.get("status") is True
def test_delete_chowdown_migration_data(api_client: TestClient, api_routes: AppRoutes, chowdown_zip: Path, token): def test_delete_chowdown_migration_data(api_client: TestClient, api_routes: AppRoutes, chowdown_zip: Path, admin_token):
selection = chowdown_zip.name selection = chowdown_zip.name
delete_url = api_routes.migrations_import_type_file_name_delete("chowdown", selection) delete_url = api_routes.migrations_import_type_file_name_delete("chowdown", selection)
response = api_client.delete(delete_url, headers=token) response = api_client.delete(delete_url, headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200
assert not app_dirs.MIGRATION_DIR.joinpath(chowdown_zip.name).is_file() assert not app_dirs.MIGRATION_DIR.joinpath(chowdown_zip.name).is_file()
@ -70,19 +70,19 @@ def nextcloud_zip():
zip_copy.unlink() zip_copy.unlink()
def test_upload_nextcloud_zip(api_client: TestClient, api_routes: AppRoutes, nextcloud_zip, token): def test_upload_nextcloud_zip(api_client: TestClient, api_routes: AppRoutes, nextcloud_zip, admin_token):
upload_url = api_routes.migrations_import_type_upload("nextcloud") upload_url = api_routes.migrations_import_type_upload("nextcloud")
response = api_client.post(upload_url, files={"archive": nextcloud_zip.open("rb")}, headers=token) response = api_client.post(upload_url, files={"archive": nextcloud_zip.open("rb")}, headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200
assert app_dirs.MIGRATION_DIR.joinpath("nextcloud", nextcloud_zip.name).is_file() assert app_dirs.MIGRATION_DIR.joinpath("nextcloud", nextcloud_zip.name).is_file()
def test_import_nextcloud_directory(api_client: TestClient, api_routes: AppRoutes, nextcloud_zip, token): def test_import_nextcloud_directory(api_client: TestClient, api_routes: AppRoutes, nextcloud_zip, admin_token):
selection = nextcloud_zip.name selection = nextcloud_zip.name
import_url = api_routes.migrations_import_type_file_name_import("nextcloud", selection) import_url = api_routes.migrations_import_type_file_name_import("nextcloud", selection)
response = api_client.post(import_url, headers=token) response = api_client.post(import_url, headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200
@ -91,10 +91,12 @@ def test_import_nextcloud_directory(api_client: TestClient, api_routes: AppRoute
assert report.get("status") is True assert report.get("status") is True
def test_delete__nextcloud_migration_data(api_client: TestClient, api_routes: AppRoutes, nextcloud_zip: Path, token): def test_delete__nextcloud_migration_data(
api_client: TestClient, api_routes: AppRoutes, nextcloud_zip: Path, admin_token
):
selection = nextcloud_zip.name selection = nextcloud_zip.name
delete_url = api_routes.migrations_import_type_file_name_delete("nextcloud", selection) delete_url = api_routes.migrations_import_type_file_name_delete("nextcloud", selection)
response = api_client.delete(delete_url, headers=token) response = api_client.delete(delete_url, headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200
assert not app_dirs.MIGRATION_DIR.joinpath(nextcloud_zip.name).is_file() assert not app_dirs.MIGRATION_DIR.joinpath(nextcloud_zip.name).is_file()

View file

@ -19,11 +19,11 @@ def test_default_settings(api_client: TestClient, api_routes: AppRoutes, default
assert json.loads(response.content) == default_settings assert json.loads(response.content) == default_settings
def test_update_settings(api_client: TestClient, api_routes: AppRoutes, default_settings, token): def test_update_settings(api_client: TestClient, api_routes: AppRoutes, default_settings, admin_token):
default_settings["language"] = "fr" default_settings["language"] = "fr"
default_settings["showRecent"] = False default_settings["showRecent"] = False
response = api_client.put(api_routes.site_settings, json=default_settings, headers=token) response = api_client.put(api_routes.site_settings, json=default_settings, headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200

View file

@ -7,10 +7,10 @@ from tests.app_routes import AppRoutes
@pytest.fixture() @pytest.fixture()
def active_link(api_client: TestClient, api_routes: AppRoutes, token): def active_link(api_client: TestClient, api_routes: AppRoutes, admin_token):
data = {"name": "Fixture Token", "admin": True} data = {"name": "Fixture Token", "admin": True}
response = api_client.post(api_routes.users_sign_ups, json=data, headers=token) response = api_client.post(api_routes.users_sign_ups, json=data, headers=admin_token)
return SignUpToken(**json.loads(response.text)) return SignUpToken(**json.loads(response.text))
@ -26,10 +26,10 @@ def sign_up_user():
} }
def test_create_sign_up_link(api_client: TestClient, api_routes: AppRoutes, token): def test_create_sign_up_link(api_client: TestClient, api_routes: AppRoutes, admin_token):
data = {"name": "Test Token", "admin": False} data = {"name": "Test Token", "admin": False}
response = api_client.post(api_routes.users_sign_ups, json=data, headers=token) response = api_client.post(api_routes.users_sign_ups, json=data, headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200
@ -47,11 +47,11 @@ def test_new_user_signup(api_client: TestClient, api_routes: AppRoutes, active_l
def test_delete_sign_up_link( def test_delete_sign_up_link(
api_client: TestClient, api_routes: AppRoutes, token, active_link: SignUpToken, sign_up_user api_client: TestClient, api_routes: AppRoutes, admin_token, active_link: SignUpToken, sign_up_user
): ):
response = api_client.delete(api_routes.users_sign_ups_token(active_link.token), headers=token) response = api_client.delete(api_routes.users_sign_ups_token(active_link.token), headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200
# Validate Token is Gone # Validate admin_token is Gone
response = api_client.get(api_routes.users_sign_ups, headers=token) response = api_client.get(api_routes.users_sign_ups, headers=admin_token)
assert sign_up_user not in json.loads(response.content) assert sign_up_user not in json.loads(response.content)

View file

@ -28,38 +28,38 @@ def new_theme():
} }
def test_default_theme(api_client: TestClient, api_routes: AppRoutes, default_theme): def test_default_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, user_token):
response = api_client.get(api_routes.themes_id(1)) response = api_client.get(api_routes.themes_id(1), headers=user_token)
assert response.status_code == 200 assert response.status_code == 200
assert json.loads(response.content) == default_theme assert json.loads(response.content) == default_theme
def test_create_theme(api_client: TestClient, api_routes: AppRoutes, new_theme, token): def test_create_theme(api_client: TestClient, api_routes: AppRoutes, new_theme, user_token):
response = api_client.post(api_routes.themes_create, json=new_theme, headers=token) response = api_client.post(api_routes.themes_create, json=new_theme, headers=user_token)
assert response.status_code == 201 assert response.status_code == 201
response = api_client.get(api_routes.themes_id(new_theme.get("id")), headers=token) response = api_client.get(api_routes.themes_id(new_theme.get("id")), headers=user_token)
assert response.status_code == 200 assert response.status_code == 200
assert json.loads(response.content) == new_theme assert json.loads(response.content) == new_theme
def test_read_all_themes(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme): def test_read_all_themes(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme, user_token):
response = api_client.get(api_routes.themes) response = api_client.get(api_routes.themes, headers=user_token)
assert response.status_code == 200 assert response.status_code == 200
response_dict = json.loads(response.content) response_dict = json.loads(response.content)
assert default_theme in response_dict assert default_theme in response_dict
assert new_theme in response_dict assert new_theme in response_dict
def test_read_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme): def test_read_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme, user_token):
for theme in [default_theme, new_theme]: for theme in [default_theme, new_theme]:
response = api_client.get(api_routes.themes_id(theme.get("id"))) response = api_client.get(api_routes.themes_id(theme.get("id")), headers=user_token)
assert response.status_code == 200 assert response.status_code == 200
assert json.loads(response.content) == theme assert json.loads(response.content) == theme
def test_update_theme(api_client: TestClient, api_routes: AppRoutes, token, new_theme): def test_update_theme(api_client: TestClient, api_routes: AppRoutes, user_token, new_theme):
theme_colors = { theme_colors = {
"primary": "#E12345", "primary": "#E12345",
"accent": "#012345", "accent": "#012345",
@ -72,14 +72,14 @@ def test_update_theme(api_client: TestClient, api_routes: AppRoutes, token, new_
new_theme["colors"] = theme_colors new_theme["colors"] = theme_colors
new_theme["name"] = "New Theme Name" new_theme["name"] = "New Theme Name"
response = api_client.put(api_routes.themes_id(new_theme.get("id")), json=new_theme, headers=token) response = api_client.put(api_routes.themes_id(new_theme.get("id")), json=new_theme, headers=user_token)
assert response.status_code == 200 assert response.status_code == 200
response = api_client.get(api_routes.themes_id(new_theme.get("id"))) response = api_client.get(api_routes.themes_id(new_theme.get("id")), headers=user_token)
assert json.loads(response.content) == new_theme assert json.loads(response.content) == new_theme
def test_delete_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme, token): def test_delete_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme, user_token):
for theme in [default_theme, new_theme]: for theme in [default_theme, new_theme]:
response = api_client.delete(api_routes.themes_id(theme.get("id")), headers=token) response = api_client.delete(api_routes.themes_id(theme.get("id")), headers=user_token)
assert response.status_code == 200 assert response.status_code == 200

View file

@ -9,7 +9,7 @@ from tests.app_routes import AppRoutes
@fixture(scope="session") @fixture(scope="session")
def default_user(): def admin_user():
return UserOut( return UserOut(
id=1, id=1,
fullName="Change Me", fullName="Change Me",
@ -24,7 +24,7 @@ def default_user():
@fixture(scope="session") @fixture(scope="session")
def new_user(): def new_user():
return UserOut( return UserOut(
id=3, id=4,
fullName="My New User", fullName="My New User",
username="My New User", username="My New User",
email="newuser@email.com", email="newuser@email.com",
@ -41,27 +41,27 @@ def test_failed_login(api_client: TestClient, api_routes: AppRoutes):
assert response.status_code == 401 assert response.status_code == 401
def test_superuser_login(api_client: TestClient, api_routes: AppRoutes, token): def test_superuser_login(api_client: TestClient, api_routes: AppRoutes, admin_token):
form_data = {"username": "changeme@email.com", "password": "MyPassword"} form_data = {"username": "changeme@email.com", "password": "MyPassword"}
response = api_client.post(api_routes.auth_token, form_data) response = api_client.post(api_routes.auth_token, form_data)
assert response.status_code == 200 assert response.status_code == 200
new_token = json.loads(response.text).get("access_token") new_token = json.loads(response.text).get("access_token")
response = api_client.get(api_routes.users_self, headers=token) response = api_client.get(api_routes.users_self, headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200
return {"Authorization": f"Bearer {new_token}"} return {"Authorization": f"Bearer {new_token}"}
def test_init_superuser(api_client: TestClient, api_routes: AppRoutes, token, default_user: UserOut): def test_init_superuser(api_client: TestClient, api_routes: AppRoutes, admin_token, admin_user: UserOut):
response = api_client.get(api_routes.users_id(1), headers=token) response = api_client.get(api_routes.users_id(1), headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200
assert json.loads(response.text) == default_user.dict(by_alias=True) assert json.loads(response.text) == admin_user.dict(by_alias=True)
def test_create_user(api_client: TestClient, api_routes: AppRoutes, token, new_user): def test_create_user(api_client: TestClient, api_routes: AppRoutes, admin_token, new_user):
create_data = { create_data = {
"fullName": "My New User", "fullName": "My New User",
"email": "newuser@email.com", "email": "newuser@email.com",
@ -71,32 +71,74 @@ def test_create_user(api_client: TestClient, api_routes: AppRoutes, token, new_u
"tokens": [], "tokens": [],
} }
response = api_client.post(api_routes.users, json=create_data, headers=token) response = api_client.post(api_routes.users, json=create_data, headers=admin_token)
assert response.status_code == 201 assert response.status_code == 201
assert json.loads(response.text) == new_user.dict(by_alias=True) assert json.loads(response.text) == new_user.dict(by_alias=True)
assert True
def test_get_all_users(api_client: TestClient, api_routes: AppRoutes, token, new_user, default_user): def test_create_user_as_non_admin(api_client: TestClient, api_routes: AppRoutes, user_token):
response = api_client.get(api_routes.users, headers=token) create_data = {
"fullName": "My New User",
"email": "newuser@email.com",
"password": "MyStrongPassword",
"group": "Home",
"admin": False,
"tokens": [],
}
response = api_client.post(api_routes.users, json=create_data, headers=user_token)
assert response.status_code == 403
def test_get_all_users(api_client: TestClient, api_routes: AppRoutes, admin_token, new_user, admin_user):
response = api_client.get(api_routes.users, headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200
all_users = json.loads(response.text) all_users = json.loads(response.text)
assert default_user.dict(by_alias=True) in all_users assert admin_user.dict(by_alias=True) in all_users
assert new_user.dict(by_alias=True) in all_users assert new_user.dict(by_alias=True) in all_users
def test_update_user(api_client: TestClient, api_routes: AppRoutes, token): def test_update_user(api_client: TestClient, api_routes: AppRoutes, admin_token):
update_data = {"id": 1, "fullName": "Updated Name", "email": "changeme@email.com", "group": "Home", "admin": True} update_data = {"id": 1, "fullName": "Updated Name", "email": "changeme@email.com", "group": "Home", "admin": True}
response = api_client.put(api_routes.users_id(1), headers=token, json=update_data) response = api_client.put(api_routes.users_id(1), headers=admin_token, json=update_data)
assert response.status_code == 200 assert response.status_code == 200
assert json.loads(response.text).get("access_token") assert json.loads(response.text).get("access_token")
def test_reset_user_password(api_client: TestClient, api_routes: AppRoutes, token): def test_update_other_user_as_not_admin(api_client: TestClient, api_routes: AppRoutes, user_token):
response = api_client.put(api_routes.users_id_reset_password(3), headers=token) update_data = {"id": 1, "fullName": "Updated Name", "email": "changeme@email.com", "group": "Home", "admin": True}
response = api_client.put(api_routes.users_id(1), headers=user_token, json=update_data)
assert response.status_code == 403
def test_update_self_as_not_admin(api_client: TestClient, api_routes: AppRoutes, user_token):
update_data = {"id": 3, "fullName": "User fullname", "email": "user@email.com", "group": "Home", "admin": False}
response = api_client.put(api_routes.users_id(3), headers=user_token, json=update_data)
assert response.status_code == 200
def test_self_demote_admin(api_client: TestClient, api_routes: AppRoutes, admin_token):
update_data = {"id": 1, "fullName": "Updated Name", "email": "changeme@email.com", "group": "Home", "admin": False}
response = api_client.put(api_routes.users_id(1), headers=admin_token, json=update_data)
assert response.status_code == 403
def test_self_promote_admin(api_client: TestClient, api_routes: AppRoutes, user_token):
update_data = {"id": 3, "fullName": "Updated Name", "email": "user@email.com", "group": "Home", "admin": True}
response = api_client.put(api_routes.users_id(3), headers=user_token, json=update_data)
assert response.status_code == 403
def test_reset_user_password(api_client: TestClient, api_routes: AppRoutes, admin_token):
response = api_client.put(api_routes.users_id_reset_password(4), headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200
@ -106,23 +148,23 @@ def test_reset_user_password(api_client: TestClient, api_routes: AppRoutes, toke
assert response.status_code == 200 assert response.status_code == 200
def test_delete_user(api_client: TestClient, api_routes: AppRoutes, token): def test_delete_user(api_client: TestClient, api_routes: AppRoutes, admin_token):
response = api_client.delete(api_routes.users_id(2), headers=token) response = api_client.delete(api_routes.users_id(2), headers=admin_token)
assert response.status_code == 200 assert response.status_code == 200
def test_update_user_image( def test_update_user_image(
api_client: TestClient, api_routes: AppRoutes, test_image_jpg: Path, test_image_png: Path, token api_client: TestClient, api_routes: AppRoutes, test_image_jpg: Path, test_image_png: Path, admin_token
): ):
response = api_client.post( response = api_client.post(
api_routes.users_id_image(2), files={"profile_image": test_image_jpg.open("rb")}, headers=token api_routes.users_id_image(2), files={"profile_image": test_image_jpg.open("rb")}, headers=admin_token
) )
assert response.status_code == 200 assert response.status_code == 200
response = api_client.post( response = api_client.post(
api_routes.users_id_image(2), files={"profile_image": test_image_png.open("rb")}, headers=token api_routes.users_id_image(2), files={"profile_image": test_image_png.open("rb")}, headers=admin_token
) )
assert response.status_code == 200 assert response.status_code == 200