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)
# Settings Routes
app.include_router(settings_router)
app.include_router(theme_routes.router)
app.include_router(theme_routes.public_router)
app.include_router(theme_routes.user_router)
# Backups/Imports Routes
app.include_router(backup_routes.router)
# Migration Routes
app.include_router(migration_routes.router)
app.include_router(debug_routes.router)
# Debug routes
app.include_router(debug_routes.public_router)
app.include_router(debug_routes.admin_router)
# Utility routes
app.include_router(utility_routes.router)

View file

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

View file

@ -2,19 +2,19 @@ import operator
import shutil
from pathlib import Path
from fastapi import APIRouter, BackgroundTasks, Depends, File, HTTPException, UploadFile, status
from fastapi import BackgroundTasks, Depends, File, HTTPException, UploadFile, status
from mealie.core.config import app_dirs
from mealie.core.root_logger import get_logger
from mealie.core.security import create_file_token
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.routers import AdminAPIRouter
from mealie.schema.backup import BackupJob, ImportJob, Imports, LocalBackup
from mealie.services.backups import imports
from mealie.services.backups.exports import backup_all
from mealie.services.events import create_backup_event
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/backups", tags=["Backups"], dependencies=[Depends(get_current_user)])
router = AdminAPIRouter(prefix="/api/backups", tags=["Backups"])
logger = get_logger()

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

View file

@ -75,6 +75,12 @@ async def get_current_user(token: str = Depends(oauth2_scheme), session=Depends(
return user
async def get_admin_user(current_user=Depends(get_current_user)) -> UserInDB:
if not current_user.admin:
raise HTTPException(status.HTTP_403_FORBIDDEN)
return current_user
def validate_long_live_token(session: Session, client_token: str, id: int) -> UserInDB:
tokens: list[LongLiveTokenInDB] = db.api_tokens.get(session, id, "parent_id", limit=9999)

View file

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

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

View file

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

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.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.routers import UserAPIRouter
from mealie.schema.meal import MealPlanIn, MealPlanOut
from mealie.schema.user import GroupInDB, UserInDB
from mealie.services.events import create_group_event
@ -10,7 +11,7 @@ from mealie.services.meal_services import get_todays_meal, set_mealplan_dates
from sqlalchemy.orm.session import Session
from starlette.responses import FileResponse
router = APIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
router = UserAPIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
@router.get("/all", response_model=list[MealPlanOut])

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.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.routers import UserAPIRouter
from mealie.schema.meal import MealPlanOut
from mealie.schema.recipe import Recipe
from mealie.schema.shopping_list import ListItem, ShoppingListIn, ShoppingListOut
@ -11,7 +12,7 @@ from sqlalchemy.orm.session import Session
logger = get_logger()
router = APIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
router = UserAPIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
@router.get("/{id}/shopping-list")

View file

@ -2,16 +2,15 @@ import operator
import shutil
from typing import List
from fastapi import APIRouter, Depends, File, UploadFile, status
from fastapi import Depends, File, HTTPException, UploadFile, status
from mealie.core.config import app_dirs
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.routers import AdminAPIRouter
from mealie.schema.migration import MigrationFile, Migrations
from mealie.services.migrations import migration
from sqlalchemy.orm.session import Session
from fastapi import HTTPException
router = APIRouter(prefix="/api/migrations", tags=["Migration"], dependencies=[Depends(get_current_user)])
router = AdminAPIRouter(prefix="/api/migrations", tags=["Migration"])
@router.get("", response_model=List[Migrations])

View file

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

View file

@ -1,7 +1,7 @@
from fastapi import APIRouter, Depends
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user, is_logged_in
from mealie.routes.deps import is_logged_in
from mealie.schema.recipe import RecipeSummary
from slugify import slugify
from sqlalchemy.orm.session import Session
@ -36,21 +36,17 @@ async def get_recipe_summary(
)
@router.get(
"/api/recipes/summary/untagged", response_model=list[RecipeSummary], dependencies=[Depends(get_current_user)]
)
@router.get("/api/recipes/summary/untagged", response_model=list[RecipeSummary])
async def get_untagged_recipes(count: bool = False, session: Session = Depends(generate_session)):
return db.recipes.count_untagged(session, count=count, override_schema=RecipeSummary)
@router.get(
"/api/recipes/summary/uncategorized", response_model=list[RecipeSummary], dependencies=[Depends(get_current_user)]
)
@router.get("/api/recipes/summary/uncategorized", response_model=list[RecipeSummary])
async def get_uncategorized_recipes(count: bool = False, session: Session = Depends(generate_session)):
return db.recipes.count_uncategorized(session, count=count, override_schema=RecipeSummary)
@router.post("/api/recipes/category", deprecated=True, dependencies=[Depends(get_current_user)])
@router.post("/api/recipes/category", deprecated=True)
def filter_by_category(categories: list, session: Session = Depends(generate_session)):
""" pass a list of categories and get a list of recipes associated with those categories """
# ! This should be refactored into a single database call, but I couldn't figure it out
@ -60,7 +56,7 @@ def filter_by_category(categories: list, session: Session = Depends(generate_ses
return in_category
@router.post("/api/recipes/tag", deprecated=True, dependencies=[Depends(get_current_user)])
@router.post("/api/recipes/tag", deprecated=True)
async def filter_by_tags(tags: list, session: Session = Depends(generate_session)):
""" pass a list of tags and get a list of recipes associated with those tags"""
# ! This should be refactored into a single database call, but I couldn't figure it out

View file

@ -1,26 +1,29 @@
from fastapi import APIRouter, Depends, HTTPException, status
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user, is_logged_in
from mealie.routes.deps import is_logged_in
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
from mealie.schema.category import CategoryIn, RecipeCategoryResponse
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/categories", tags=["Recipe Categories"])
public_router = APIRouter(prefix="/api/categories", tags=["Recipe Categories"])
user_router = UserAPIRouter(prefix="/api/categories", tags=["Recipe Categories"])
admin_router = AdminAPIRouter(prefix="/api/categories", tags=["Recipe Categories"])
@router.get("")
@public_router.get("")
async def get_all_recipe_categories(session: Session = Depends(generate_session)):
""" Returns a list of available categories in the database """
return db.categories.get_all_limit_columns(session, ["slug", "name"])
@router.get("/empty")
@public_router.get("/empty")
def get_empty_categories(session: Session = Depends(generate_session)):
""" Returns a list of categories that do not contain any recipes"""
return db.categories.get_empty(session)
@router.get("/{category}", response_model=RecipeCategoryResponse)
@public_router.get("/{category}", response_model=RecipeCategoryResponse)
def get_all_recipes_by_category(
category: str, session: Session = Depends(generate_session), is_user: bool = Depends(is_logged_in)
):
@ -35,7 +38,7 @@ def get_all_recipes_by_category(
return category_obj
@router.post("", dependencies=[Depends(get_current_user)])
@user_router.post("")
async def create_recipe_category(category: CategoryIn, session: Session = Depends(generate_session)):
""" Creates a Category in the database """
@ -45,7 +48,7 @@ async def create_recipe_category(category: CategoryIn, session: Session = Depend
raise HTTPException(status.HTTP_400_BAD_REQUEST)
@router.put("/{category}", response_model=RecipeCategoryResponse, dependencies=[Depends(get_current_user)])
@admin_router.put("/{category}", response_model=RecipeCategoryResponse)
async def update_recipe_category(category: str, new_category: CategoryIn, session: Session = Depends(generate_session)):
""" Updates an existing Tag in the database """
@ -55,7 +58,7 @@ async def update_recipe_category(category: str, new_category: CategoryIn, sessio
raise HTTPException(status.HTTP_400_BAD_REQUEST)
@router.delete("/{category}", dependencies=[Depends(get_current_user)])
@admin_router.delete("/{category}")
async def delete_recipe_category(category: str, session: Session = Depends(generate_session)):
"""
Removes a recipe category from the database. Deleting a

View file

@ -1,14 +1,15 @@
from http.client import HTTPException
from fastapi import APIRouter, Depends, status
from fastapi import Depends, status
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.routers import UserAPIRouter
from mealie.schema.comments import CommentIn, CommentOut, CommentSaveToDB
from mealie.schema.user import UserInDB
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api", tags=["Recipe Comments"])
router = UserAPIRouter(prefix="/api", tags=["Recipe Comments"])
@router.post("/recipes/{slug}/comments")
@ -35,7 +36,7 @@ async def update_comment(
old_comment: CommentOut = db.comments.get(session, id)
if current_user.id != old_comment.user.id:
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
raise HTTPException(status.HTTP_403_FORBIDDEN)
return db.comments.update(session, id, new_comment)
@ -51,4 +52,4 @@ async def delete_comment(
db.comments.delete(session, id)
return
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
raise HTTPException(status.HTTP_403_FORBIDDEN)

View file

@ -1,4 +1,5 @@
import json
from mealie.routes.routers import UserAPIRouter
import shutil
from shutil import copyfileobj
from zipfile import ZipFile
@ -21,11 +22,12 @@ from slugify import slugify
from sqlalchemy.orm.session import Session
from starlette.responses import FileResponse
router = APIRouter(prefix="/api/recipes", tags=["Recipe CRUD"])
user_router = UserAPIRouter(prefix="/api/recipes", tags=["Recipe CRUD"])
public_router = APIRouter(prefix="/api/recipes", tags=["Recipe CRUD"])
logger = get_logger()
@router.post("/create", status_code=201, response_model=str)
@user_router.post("/create", status_code=201, response_model=str)
def create_from_json(
background_tasks: BackgroundTasks,
data: Recipe,
@ -46,12 +48,12 @@ def create_from_json(
return recipe.slug
@router.post("/test-scrape-url", dependencies=[Depends(get_current_user)])
@user_router.post("/test-scrape-url")
def test_parse_recipe_url(url: RecipeURLIn):
return scrape_url(url.url)
@router.post("/create-url", status_code=201, response_model=str)
@user_router.post("/create-url", status_code=201, response_model=str)
def parse_recipe_url(
background_tasks: BackgroundTasks,
url: RecipeURLIn,
@ -74,7 +76,7 @@ def parse_recipe_url(
return recipe.slug
@router.get("/{recipe_slug}", response_model=Recipe)
@public_router.get("/{recipe_slug}", response_model=Recipe)
def get_recipe(recipe_slug: str, session: Session = Depends(generate_session), is_user: bool = Depends(is_logged_in)):
""" Takes in a recipe slug, returns all data for a recipe """
@ -88,10 +90,10 @@ def get_recipe(recipe_slug: str, session: Session = Depends(generate_session), i
return recipe
else:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, {"details": "unauthorized"})
raise HTTPException(status.HTTP_403_FORBIDDEN)
@router.post("/create-from-zip", dependencies=[Depends(get_current_user)])
@user_router.post("/create-from-zip")
async def create_recipe_from_zip(
session: Session = Depends(generate_session),
temp_path=Depends(temporary_zip_path),
@ -121,7 +123,7 @@ async def create_recipe_from_zip(
return recipe
@router.get("/{recipe_slug}/zip")
@public_router.get("/{recipe_slug}/zip")
async def get_recipe_as_zip(
recipe_slug: str, session: Session = Depends(generate_session), temp_path=Depends(temporary_zip_path)
):
@ -139,7 +141,7 @@ async def get_recipe_as_zip(
return FileResponse(temp_path, filename=f"{recipe_slug}.zip")
@router.put("/{recipe_slug}", dependencies=[Depends(get_current_user)])
@user_router.put("/{recipe_slug}")
def update_recipe(
recipe_slug: str,
data: Recipe,
@ -154,7 +156,7 @@ def update_recipe(
return recipe
@router.patch("/{recipe_slug}", dependencies=[Depends(get_current_user)])
@user_router.patch("/{recipe_slug}")
def patch_recipe(
recipe_slug: str,
data: Recipe,
@ -171,7 +173,7 @@ def patch_recipe(
return recipe
@router.delete("/{recipe_slug}")
@user_router.delete("/{recipe_slug}")
def delete_recipe(
background_tasks: BackgroundTasks,
recipe_slug: str,
@ -194,7 +196,7 @@ def delete_recipe(
raise HTTPException(status.HTTP_400_BAD_REQUEST)
@router.put("/{recipe_slug}/image", dependencies=[Depends(get_current_user)])
@user_router.put("/{recipe_slug}/image")
def update_recipe_image(
recipe_slug: str,
image: bytes = File(...),
@ -208,7 +210,7 @@ def update_recipe_image(
return {"image": new_version}
@router.post("/{recipe_slug}/image", dependencies=[Depends(get_current_user)])
@user_router.post("/{recipe_slug}/image")
def scrape_image_url(
recipe_slug: str,
url: RecipeURLIn,
@ -218,7 +220,7 @@ def scrape_image_url(
scrape_image(url.url, recipe_slug)
@router.post("/{recipe_slug}/assets", response_model=RecipeAsset, dependencies=[Depends(get_current_user)])
@user_router.post("/{recipe_slug}/assets", response_model=RecipeAsset)
def upload_recipe_asset(
recipe_slug: str,
name: str = Form(...),

View file

@ -1,28 +1,29 @@
from fastapi import APIRouter, Depends, HTTPException, status
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user, is_logged_in
from mealie.routes.deps import is_logged_in
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
from mealie.schema.category import RecipeTagResponse, TagIn
from sqlalchemy.orm.session import Session
router = APIRouter(tags=["Recipes"])
router = APIRouter(prefix="/api/tags", tags=["Recipe Tags"])
public_router = APIRouter(prefix="/api/tags", tags=["Recipe Tags"])
user_router = UserAPIRouter(prefix="/api/tags", tags=["Recipe Tags"])
admin_router = AdminAPIRouter(prefix="/api/tags", tags=["Recipe Tags"])
@router.get("")
@public_router.get("")
async def get_all_recipe_tags(session: Session = Depends(generate_session)):
""" Returns a list of available tags in the database """
return db.tags.get_all_limit_columns(session, ["slug", "name"])
@router.get("/empty")
@public_router.get("/empty")
def get_empty_tags(session: Session = Depends(generate_session)):
""" Returns a list of tags that do not contain any recipes"""
return db.tags.get_empty(session)
@router.get("/{tag}", response_model=RecipeTagResponse)
@public_router.get("/{tag}", response_model=RecipeTagResponse)
def get_all_recipes_by_tag(
tag: str, session: Session = Depends(generate_session), is_user: bool = Depends(is_logged_in)
):
@ -36,21 +37,21 @@ def get_all_recipes_by_tag(
return tag_obj
@router.post("", dependencies=[Depends(get_current_user)])
@user_router.post("")
async def create_recipe_tag(tag: TagIn, session: Session = Depends(generate_session)):
""" Creates a Tag in the database """
return db.tags.create(session, tag.dict())
@router.put("/{tag}", response_model=RecipeTagResponse, dependencies=[Depends(get_current_user)])
@admin_router.put("/{tag}", response_model=RecipeTagResponse)
async def update_recipe_tag(tag: str, new_tag: TagIn, session: Session = Depends(generate_session)):
""" Updates an existing Tag in the database """
return db.tags.update(session, tag, new_tag.dict())
@router.delete("/{tag}", dependencies=[Depends(get_current_user)])
@admin_router.delete("/{tag}")
async def delete_recipe_tag(tag: str, session: Session = Depends(generate_session)):
"""Removes a recipe tag from the database. Deleting a
tag does not impact a recipe. The tag will be removed

26
mealie/routes/routers.py Normal file
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.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.routers import UserAPIRouter
from mealie.schema.shopping_list import ShoppingListIn, ShoppingListOut
from mealie.schema.user import UserInDB
from sqlalchemy.orm.session import Session
shopping_list_router = APIRouter(prefix="/api/shopping-lists", tags=["Shopping Lists"])
shopping_list_router = UserAPIRouter(prefix="/api/shopping-lists", tags=["Shopping Lists"])
@shopping_list_router.post("", response_model=ShoppingListOut)
@ -28,13 +29,13 @@ async def get_shopping_list(id: int, session: Session = Depends(generate_session
return db.shopping_lists.get(session, id)
@shopping_list_router.put("/{id}", dependencies=[Depends(get_current_user)], response_model=ShoppingListOut)
@shopping_list_router.put("/{id}", response_model=ShoppingListOut)
async def update_shopping_list(id: int, new_data: ShoppingListIn, session: Session = Depends(generate_session)):
""" Update Shopping List in the Database """
return db.shopping_lists.update(session, id, new_data)
@shopping_list_router.delete("/{id}", dependencies=[Depends(get_current_user)])
@shopping_list_router.delete("/{id}")
async def delete_shopping_list(id: int, session: Session = Depends(generate_session)):
""" Delete Shopping List from the Database """
return db.shopping_lists.delete(session, id)

View file

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

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

View file

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

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

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.routes.deps import get_current_user
router = APIRouter(prefix="/api/foods", dependencies=[Depends(get_current_user)])
router = UserAPIRouter(prefix="/api/foods")
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.routes.deps import get_current_user
router = APIRouter(prefix="/api/units", dependencies=[Depends(get_current_user)])
router = UserAPIRouter(prefix="/api/units")
logger = get_logger()

View file

@ -4,7 +4,11 @@ from . import api_tokens, auth, crud, sign_up
user_router = APIRouter()
user_router.include_router(auth.router)
user_router.include_router(sign_up.router)
user_router.include_router(crud.router)
user_router.include_router(auth.public_router)
user_router.include_router(auth.user_router)
user_router.include_router(sign_up.public_router)
user_router.include_router(sign_up.admin_router)
user_router.include_router(crud.public_router)
user_router.include_router(crud.user_router)
user_router.include_router(crud.admin_router)
user_router.include_router(api_tokens.router)

View file

@ -1,15 +1,16 @@
from datetime import timedelta
from fastapi import APIRouter, HTTPException, status
from fastapi import HTTPException, status
from fastapi.param_functions import Depends
from mealie.core.security import create_access_token
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.routers import UserAPIRouter
from mealie.schema.user import CreateToken, LoingLiveTokenIn, LongLiveTokenInDB, UserInDB
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/users", tags=["User API Tokens"])
router = UserAPIRouter(prefix="/api/users", tags=["User API Tokens"])
@router.post("/api-tokens", status_code=status.HTTP_201_CREATED)
@ -53,4 +54,4 @@ async def delete_api_token(
deleted_token = db.api_tokens.delete(session, token_id)
return {"token_delete": deleted_token.name}
else:
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
raise HTTPException(status.HTTP_403_FORBIDDEN)

View file

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

View file

@ -1,21 +1,34 @@
import shutil
from fastapi import APIRouter, BackgroundTasks, Depends, File, HTTPException, UploadFile, status
from fastapi import BackgroundTasks, Depends, File, HTTPException, UploadFile, status
from fastapi.responses import FileResponse
from fastapi.routing import APIRouter
from mealie.core import security
from mealie.core.config import app_dirs, settings
from mealie.core.security import get_password_hash, verify_password
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
from mealie.schema.user import ChangePassword, UserBase, UserFavorites, UserIn, UserInDB, UserOut
from mealie.services.events import create_user_event
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/users", tags=["Users"])
public_router = APIRouter(prefix="/api/users", tags=["Users"])
user_router = UserAPIRouter(prefix="/api/users", tags=["Users"])
admin_router = AdminAPIRouter(prefix="/api/users", tags=["Users"])
@router.post("", response_model=UserOut, status_code=201)
async def assert_user_change_allowed(
id: int,
current_user: UserInDB = Depends(get_current_user),
):
if current_user.id != id and not current_user.admin:
# only admins can edit other users
raise HTTPException(status.HTTP_403_FORBIDDEN, detail="NOT_AN_ADMIN")
@admin_router.post("", response_model=UserOut, status_code=201)
async def create_user(
background_tasks: BackgroundTasks,
new_user: UserIn,
@ -30,26 +43,19 @@ async def create_user(
return db.users.create(session, new_user.dict())
@router.get("", response_model=list[UserOut])
async def get_all_users(
current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session),
):
if not current_user.admin:
raise HTTPException(status.HTTP_403_FORBIDDEN)
@admin_router.get("", response_model=list[UserOut])
async def get_all_users(session: Session = Depends(generate_session)):
return db.users.get_all(session)
@router.get("/self", response_model=UserOut)
@user_router.get("/self", response_model=UserOut)
async def get_logged_in_user(
current_user: UserInDB = Depends(get_current_user),
):
return current_user.dict()
@router.get("/{id}", response_model=UserOut, dependencies=[Depends(get_current_user)])
@admin_router.get("/{id}", response_model=UserOut)
async def get_user_by_id(
id: int,
session: Session = Depends(generate_session),
@ -57,7 +63,7 @@ async def get_user_by_id(
return db.users.get(session, id)
@router.put("/{id}/reset-password", dependencies=[Depends(get_current_user)])
@user_router.put("/{id}/reset-password")
async def reset_user_password(
id: int,
session: Session = Depends(generate_session),
@ -67,7 +73,7 @@ async def reset_user_password(
db.users.update_password(session, id, new_password)
@router.put("/{id}")
@user_router.put("/{id}")
async def update_user(
id: int,
new_data: UserBase,
@ -75,16 +81,24 @@ async def update_user(
session: Session = Depends(generate_session),
):
token = None
if current_user.id == id or current_user.admin:
db.users.update(session, id, new_data.dict())
assert_user_change_allowed(id)
if not current_user.admin and (new_data.admin or current_user.group != new_data.group):
# prevent a regular user from doing admin tasks on themself
raise HTTPException(status.HTTP_403_FORBIDDEN)
if current_user.id == id and current_user.admin and not new_data.admin:
# prevent an admin from demoting themself
raise HTTPException(status.HTTP_403_FORBIDDEN)
db.users.update(session, id, new_data.dict())
if current_user.id == id:
access_token = security.create_access_token(data=dict(sub=new_data.email))
token = {"access_token": access_token, "token_type": "bearer"}
return token
@router.get("/{id}/image")
@public_router.get("/{id}/image")
async def get_user_image(id: str):
""" Returns a users profile picture """
user_dir = app_dirs.USER_DIR.joinpath(id)
@ -94,13 +108,15 @@ async def get_user_image(id: str):
raise HTTPException(status.HTTP_404_NOT_FOUND)
@router.post("/{id}/image", dependencies=[Depends(get_current_user)])
@user_router.post("/{id}/image")
async def update_user_image(
id: str,
profile_image: UploadFile = File(...),
):
""" Updates a User Image """
assert_user_change_allowed(id)
extension = profile_image.filename.split(".")[-1]
app_dirs.USER_DIR.joinpath(id).mkdir(parents=True, exist_ok=True)
@ -116,7 +132,7 @@ async def update_user_image(
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
@router.put("/{id}/password")
@user_router.put("/{id}/password")
async def update_password(
id: int,
password_change: ChangePassword,
@ -125,24 +141,24 @@ async def update_password(
):
""" Resets the User Password"""
assert_user_change_allowed(id)
match_passwords = verify_password(password_change.current_password, current_user.password)
match_id = current_user.id == id
if not (match_passwords and match_id):
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
if not (match_passwords):
raise HTTPException(status.HTTP_400_BAD_REQUEST)
new_password = get_password_hash(password_change.new_password)
db.users.update_password(session, id, new_password)
@router.get("/{id}/favorites", response_model=UserFavorites)
@user_router.get("/{id}/favorites", response_model=UserFavorites)
async def get_favorites(id: str, session: Session = Depends(generate_session)):
""" Adds a Recipe to the users favorites """
""" Get user's favorite recipes """
return db.users.get(session, id, override_schema=UserFavorites)
@router.post("/{id}/favorites/{slug}")
@user_router.post("/{id}/favorites/{slug}")
async def add_favorite(
slug: str,
current_user: UserInDB = Depends(get_current_user),
@ -150,12 +166,13 @@ async def add_favorite(
):
""" Adds a Recipe to the users favorites """
assert_user_change_allowed(id)
current_user.favorite_recipes.append(slug)
db.users.update(session, current_user.id, current_user)
@router.delete("/{id}/favorites/{slug}")
@user_router.delete("/{id}/favorites/{slug}")
async def remove_favorite(
slug: str,
current_user: UserInDB = Depends(get_current_user),
@ -163,6 +180,7 @@ async def remove_favorite(
):
""" Adds a Recipe to the users favorites """
assert_user_change_allowed(id)
current_user.favorite_recipes = [x for x in current_user.favorite_recipes if x != slug]
db.users.update(session, current_user.id, current_user)
@ -170,21 +188,21 @@ async def remove_favorite(
return
@router.delete("/{id}")
@admin_router.delete("/{id}")
async def delete_user(
background_tasks: BackgroundTasks,
id: int,
current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session),
):
""" Removes a user from the database. Must be the current user or a super user"""
assert_user_change_allowed(id)
if id == 1:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="SUPER_USER")
if current_user.id == id or current_user.admin:
try:
db.users.delete(session, id)
background_tasks.add_task(create_user_event, "User Deleted", f"User ID: {id}", session=session)
except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST)
try:
db.users.delete(session, id)
background_tasks.add_task(create_user_event, "User Deleted", f"User ID: {id}", session=session)
except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST)

View file

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

View file

@ -49,16 +49,39 @@ def test_image_png():
return TEST_DATA.joinpath("images", "test_image.png")
@fixture(scope="session")
def token(api_client: requests, api_routes: AppRoutes):
form_data = {"username": "changeme@email.com", "password": settings.DEFAULT_PASSWORD}
def login(form_data, api_client: requests, api_routes: AppRoutes):
response = api_client.post(api_routes.auth_token, form_data)
assert response.status_code == 200
token = json.loads(response.text).get("access_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")
def 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)
def test_create_by_url(api_client: TestClient, api_routes: AppRoutes, recipe_data: RecipeSiteTestCase, token):
api_client.delete(api_routes.recipes_recipe_slug(recipe_data.expected_slug), headers=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=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 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")
api_client.delete(recipe_url, headers=token)
response = api_client.post(api_routes.recipes_create, json=raw_recipe, headers=token)
api_client.delete(recipe_url, headers=user_token)
response = api_client.post(api_routes.recipes_create, json=raw_recipe, headers=user_token)
assert response.status_code == 201
assert json.loads(response.text) == "banana-bread"
def test_create_no_image(api_client: TestClient, api_routes: AppRoutes, token, raw_recipe_no_image):
response = api_client.post(api_routes.recipes_create, json=raw_recipe_no_image, headers=token)
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=user_token)
assert response.status_code == 201
assert json.loads(response.text) == "banana-bread-no-image"
@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)
response = api_client.get(recipe_url, headers=token)
response = api_client.get(recipe_url, headers=user_token)
assert response.status_code == 200
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"]
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 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)
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)
response = api_client.get(recipe_url, headers=token)
response = api_client.get(recipe_url, headers=user_token)
assert response.status_code == 200
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)
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 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)
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)
response = api_client.delete(recipe_url, headers=token)
response = api_client.delete(recipe_url, headers=user_token)
assert response.status_code == 200

View file

@ -10,8 +10,8 @@ def page_data():
return {"name": "My New Page", "position": 0, "categories": []}
def test_create_page(api_client: TestClient, api_routes: AppRoutes, token, page_data):
response = api_client.post(api_routes.site_settings_custom_pages, json=page_data, headers=token)
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=admin_token)
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
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["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
def test_delete_page(api_client: TestClient, api_routes: AppRoutes, token):
response = api_client.delete(api_routes.site_settings_custom_pages_id(1), headers=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=admin_token)
assert response.status_code == 200

View file

@ -10,20 +10,20 @@ def group_data():
return {"name": "Test Group"}
def test_create_group(api_client: TestClient, api_routes: AppRoutes, token):
response = api_client.post(api_routes.groups, json={"name": "Test Group"}, headers=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=admin_token)
assert response.status_code == 201
def test_get_self_group(api_client: TestClient, api_routes: AppRoutes, token):
response = api_client.get(api_routes.groups, headers=token)
def test_get_self_group(api_client: TestClient, api_routes: AppRoutes, admin_token):
response = api_client.get(api_routes.groups, headers=admin_token)
assert response.status_code == 200
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 = {
"name": "New Group Name",
"id": 2,
@ -36,23 +36,23 @@ def test_update_group(api_client: TestClient, api_routes: AppRoutes, token):
"shoppingLists": [],
}
# 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
# 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)
id_2 = filter(lambda x: x["id"] == 2, all_groups)
assert next(id_2) == new_data
def test_home_group_not_deletable(api_client: TestClient, api_routes: AppRoutes, token):
response = api_client.delete(api_routes.groups_id(1), headers=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=admin_token)
assert response.status_code == 400
def test_delete_group(api_client: TestClient, api_routes: AppRoutes, token):
response = api_client.delete(api_routes.groups_id(2), headers=token)
def test_delete_group(api_client: TestClient, api_routes: AppRoutes, admin_token):
response = api_client.delete(api_routes.groups_id(2), headers=admin_token)
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")
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
for _, value in json.loads(response.content).items():
for v in value:

View file

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

View file

@ -25,8 +25,8 @@ def get_meal_plan_template(first=None, second=None):
@pytest.fixture(scope="session")
def slug_1(api_client: TestClient, api_routes: AppRoutes, token, recipe_store: list[RecipeSiteTestCase]):
slug_1 = api_client.post(api_routes.recipes_create_url, json={"url": recipe_store[0].url}, headers=token)
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=admin_token)
slug_1 = json.loads(slug_1.content)
yield slug_1
@ -35,8 +35,8 @@ def slug_1(api_client: TestClient, api_routes: AppRoutes, token, recipe_store: l
@pytest.fixture(scope="session")
def slug_2(api_client: TestClient, api_routes: AppRoutes, token, recipe_store: list[RecipeSiteTestCase]):
slug_2 = api_client.post(api_routes.recipes_create_url, json={"url": recipe_store[1].url}, headers=token)
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=admin_token)
slug_2 = json.loads(slug_2.content)
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))
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)
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
def test_read_mealplan(api_client: TestClient, api_routes: AppRoutes, slug_1, slug_2, token):
response = api_client.get(api_routes.meal_plans_all, headers=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=admin_token)
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"]
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 = 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"][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
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 = 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
def test_delete_mealplan(api_client: TestClient, api_routes: AppRoutes, token):
response = api_client.get(api_routes.meal_plans_all, headers=token)
def test_delete_mealplan(api_client: TestClient, api_routes: AppRoutes, admin_token):
response = api_client.get(api_routes.meal_plans_all, headers=admin_token)
assert response.status_code == 200
existing_mealplan = json.loads(response.text)
existing_mealplan = existing_mealplan[0]
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

View file

@ -22,22 +22,22 @@ def chowdown_zip():
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")
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 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")
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
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
@ -47,10 +47,10 @@ def test_import_chowdown_directory(api_client: TestClient, api_routes: AppRoutes
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
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 not app_dirs.MIGRATION_DIR.joinpath(chowdown_zip.name).is_file()
@ -70,19 +70,19 @@ def nextcloud_zip():
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")
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 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
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
@ -91,10 +91,12 @@ def test_import_nextcloud_directory(api_client: TestClient, api_routes: AppRoute
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
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 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
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["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

View file

@ -7,10 +7,10 @@ from tests.app_routes import AppRoutes
@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}
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))
@ -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}
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
@ -47,11 +47,11 @@ def test_new_user_signup(api_client: TestClient, api_routes: AppRoutes, active_l
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
# Validate Token is Gone
response = api_client.get(api_routes.users_sign_ups, headers=token)
# Validate admin_token is Gone
response = api_client.get(api_routes.users_sign_ups, headers=admin_token)
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):
response = api_client.get(api_routes.themes_id(1))
def test_default_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, user_token):
response = api_client.get(api_routes.themes_id(1), headers=user_token)
assert response.status_code == 200
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
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 json.loads(response.content) == new_theme
def test_read_all_themes(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme):
response = api_client.get(api_routes.themes)
def test_read_all_themes(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme, user_token):
response = api_client.get(api_routes.themes, headers=user_token)
assert response.status_code == 200
response_dict = json.loads(response.content)
assert default_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]:
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 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 = {
"primary": "#E12345",
"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["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
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
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]:
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

View file

@ -9,7 +9,7 @@ from tests.app_routes import AppRoutes
@fixture(scope="session")
def default_user():
def admin_user():
return UserOut(
id=1,
fullName="Change Me",
@ -24,7 +24,7 @@ def default_user():
@fixture(scope="session")
def new_user():
return UserOut(
id=3,
id=4,
fullName="My New User",
username="My New User",
email="newuser@email.com",
@ -41,27 +41,27 @@ def test_failed_login(api_client: TestClient, api_routes: AppRoutes):
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"}
response = api_client.post(api_routes.auth_token, form_data)
assert response.status_code == 200
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
return {"Authorization": f"Bearer {new_token}"}
def test_init_superuser(api_client: TestClient, api_routes: AppRoutes, token, default_user: UserOut):
response = api_client.get(api_routes.users_id(1), headers=token)
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=admin_token)
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 = {
"fullName": "My New User",
"email": "newuser@email.com",
@ -71,32 +71,74 @@ def test_create_user(api_client: TestClient, api_routes: AppRoutes, token, new_u
"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 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):
response = api_client.get(api_routes.users, headers=token)
def test_create_user_as_non_admin(api_client: TestClient, api_routes: AppRoutes, user_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
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
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}
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 json.loads(response.text).get("access_token")
def test_reset_user_password(api_client: TestClient, api_routes: AppRoutes, token):
response = api_client.put(api_routes.users_id_reset_password(3), headers=token)
def test_update_other_user_as_not_admin(api_client: TestClient, api_routes: AppRoutes, user_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
@ -106,23 +148,23 @@ def test_reset_user_password(api_client: TestClient, api_routes: AppRoutes, toke
assert response.status_code == 200
def test_delete_user(api_client: TestClient, api_routes: AppRoutes, token):
response = api_client.delete(api_routes.users_id(2), headers=token)
def test_delete_user(api_client: TestClient, api_routes: AppRoutes, admin_token):
response = api_client.delete(api_routes.users_id(2), headers=admin_token)
assert response.status_code == 200
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(
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
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