refactor(backend): ♻️ organize and tag user routes by path

This commit is contained in:
hay-kot 2021-08-21 15:05:00 -08:00
parent 7d40f8d74d
commit 05f2eab1ea
15 changed files with 232 additions and 182 deletions

View File

@ -1,10 +1,8 @@
{ {
"python.formatting.provider": "black", "python.formatting.provider": "black",
"python.pythonPath": ".venv/bin/python3.9", "python.pythonPath": ".venv/bin/python3.9",
"python.linting.pylintEnabled": false,
"python.linting.enabled": true, "python.linting.enabled": true,
"python.testing.unittestEnabled": false, "python.testing.unittestEnabled": false,
"python.testing.nosetestsEnabled": false,
"python.testing.pytestEnabled": true, "python.testing.pytestEnabled": true,
"python.testing.autoTestDiscoverOnSaveEnabled": false, "python.testing.autoTestDiscoverOnSaveEnabled": false,
"python.testing.pytestArgs": ["tests"], "python.testing.pytestArgs": ["tests"],
@ -20,7 +18,9 @@
"editor.formatOnSave": true, "editor.formatOnSave": true,
"eslint.workingDirectories": ["./frontend"], "eslint.workingDirectories": ["./frontend"],
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": true "source.fixAll.eslint": true,
"source.organizeImports": false
}, },
"vetur.validation.template": false "vetur.validation.template": false,
"python.linting.pylintEnabled": false
} }

View File

@ -4,7 +4,7 @@ from fastapi.middleware.gzip import GZipMiddleware
from mealie.core.config import APP_VERSION, settings from mealie.core.config import APP_VERSION, settings
from mealie.core.root_logger import get_logger from mealie.core.root_logger import get_logger
from mealie.routes import backup_routes, debug_routes, migration_routes, theme_routes, utility_routes from mealie.routes import backup_routes, debug_routes, migration_routes, router, theme_routes, utility_routes
from mealie.routes.about import about_router from mealie.routes.about import about_router
from mealie.routes.groups import groups_router from mealie.routes.groups import groups_router
from mealie.routes.mealplans import meal_plan_router from mealie.routes.mealplans import meal_plan_router
@ -12,7 +12,6 @@ from mealie.routes.media import media_router
from mealie.routes.recipe import recipe_router from mealie.routes.recipe import recipe_router
from mealie.routes.shopping_list import shopping_list_router from mealie.routes.shopping_list import shopping_list_router
from mealie.routes.site_settings import settings_router from mealie.routes.site_settings import settings_router
from mealie.routes.users import user_router
from mealie.services.events import create_general_event from mealie.services.events import create_general_event
from mealie.services.recipe.all_recipes import subscripte_to_recipe_events from mealie.services.recipe.all_recipes import subscripte_to_recipe_events
@ -35,7 +34,7 @@ def start_scheduler():
def api_routers(): def api_routers():
# Authentication # Authentication
app.include_router(user_router) app.include_router(router)
app.include_router(groups_router) app.include_router(groups_router)
app.include_router(shopping_list_router) app.include_router(shopping_list_router)
# Recipes # Recipes

View File

@ -0,0 +1,8 @@
from fastapi import APIRouter
from . import auth, users
router = APIRouter(prefix="/api")
router.include_router(auth.router)
router.include_router(users.router)

View File

@ -0,0 +1,8 @@
from fastapi import APIRouter
from . import auth
router = APIRouter(prefix="/auth")
router.include_router(auth.public_router)
router.include_router(auth.user_router)

View File

@ -10,8 +10,8 @@ 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
public_router = APIRouter(prefix="/api/auth", tags=["Authentication"]) public_router = APIRouter(tags=["Authentication"])
user_router = UserAPIRouter(prefix="/api/auth", tags=["Authentication"]) user_router = UserAPIRouter(tags=["Authentication"])
@public_router.post("/token/long") @public_router.post("/token/long")

View File

@ -1,6 +1,6 @@
from fastapi import Depends from fastapi import Depends
from fastapi.routing import APIRouter from fastapi.routing import APIRouter
from mealie.core.config import APP_VERSION, app_dirs, settings from mealie.core.config import APP_VERSION, 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
@ -50,12 +50,6 @@ async def get_mealie_version():
) )
@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"))}
@admin_router.get("/log/{num}") @admin_router.get("/log/{num}")
async def get_log(num: int): async def get_log(num: int):
""" Doc Str """ """ Doc Str """

View File

@ -1,8 +1,8 @@
from typing import List, Optional
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from mealie.routes.deps import get_admin_user, get_current_user from mealie.routes.deps import get_admin_user, get_current_user
from typing import List, Optional
class AdminAPIRouter(APIRouter): class AdminAPIRouter(APIRouter):
""" Router for functions to be protected behind admin authentication """ """ Router for functions to be protected behind admin authentication """

View File

@ -1,14 +1,23 @@
from fastapi import APIRouter from fastapi import APIRouter
from . import api_tokens, auth, crud, sign_up from . import api_tokens, crud, favorites, images, passwords, sign_up
user_router = APIRouter() # Must be used because of the way FastAPI works with nested routes
user_prefix = "/users"
user_router.include_router(auth.public_router) router = APIRouter()
user_router.include_router(auth.user_router)
user_router.include_router(sign_up.public_router) router.include_router(sign_up.admin_router, prefix=user_prefix, tags=["Users: Sign-Up"])
user_router.include_router(sign_up.admin_router) router.include_router(sign_up.public_router, prefix=user_prefix, tags=["Users: Sign-Up"])
user_router.include_router(crud.public_router)
user_router.include_router(crud.user_router) router.include_router(crud.user_router, prefix=user_prefix, tags=["Users: CRUD"])
user_router.include_router(crud.admin_router) router.include_router(crud.admin_router, prefix=user_prefix, tags=["Users: CRUD"])
user_router.include_router(api_tokens.router)
router.include_router(passwords.user_router, prefix=user_prefix, tags=["Users: Passwords"])
router.include_router(images.public_router, prefix=user_prefix, tags=["Users: Images"])
router.include_router(images.user_router, prefix=user_prefix, tags=["Users: Images"])
router.include_router(api_tokens.router, prefix=user_prefix, tags=["Users: Tokens"])
router.include_router(favorites.user_router, prefix=user_prefix, tags=["Users: Favorites"])

View File

@ -0,0 +1,8 @@
from fastapi import HTTPException, status
from mealie.schema.user.user import UserInDB
def assert_user_change_allowed(id: int, current_user: UserInDB):
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")

View File

@ -10,7 +10,7 @@ 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 = UserAPIRouter(prefix="/api/users", tags=["User API Tokens"]) router = UserAPIRouter()
@router.post("/api-tokens", status_code=status.HTTP_201_CREATED) @router.post("/api-tokens", status_code=status.HTTP_201_CREATED)

View File

@ -1,28 +1,22 @@
import shutil from fastapi import BackgroundTasks, Depends, HTTPException, 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 import security
from mealie.core.config import app_dirs, settings from mealie.core.security import get_password_hash
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.routes.routers import AdminAPIRouter, UserAPIRouter
from mealie.schema.user import ChangePassword, UserBase, UserFavorites, UserIn, UserInDB, UserOut from mealie.routes.users._helpers import assert_user_change_allowed
from mealie.schema.user import UserBase, 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
public_router = APIRouter(prefix="/api/users", tags=["Users"]) user_router = UserAPIRouter(prefix="")
user_router = UserAPIRouter(prefix="/api/users", tags=["Users"]) admin_router = AdminAPIRouter(prefix="")
admin_router = AdminAPIRouter(prefix="/api/users", tags=["Users"])
def assert_user_change_allowed(id: int, current_user: UserInDB): @admin_router.get("", response_model=list[UserOut])
if current_user.id != id and not current_user.admin: async def get_all_users(session: Session = Depends(generate_session)):
# only admins can edit other users return db.users.get_all(session)
raise HTTPException(status.HTTP_403_FORBIDDEN, detail="NOT_AN_ADMIN")
@admin_router.post("", response_model=UserOut, status_code=201) @admin_router.post("", response_model=UserOut, status_code=201)
@ -40,9 +34,33 @@ async def create_user(
return db.users.create(session, new_user.dict()) return db.users.create(session, new_user.dict())
@admin_router.get("", response_model=list[UserOut]) @admin_router.get("/{id}", response_model=UserOut)
async def get_all_users(session: Session = Depends(generate_session)): async def get_user(
return db.users.get_all(session) id: int,
session: Session = Depends(generate_session),
):
return db.users.get(session, id)
@admin_router.delete("/{id}")
def delete_user(
background_tasks: BackgroundTasks,
id: int,
session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user),
):
""" Removes a user from the database. Must be the current user or a super user"""
assert_user_change_allowed(id, current_user)
if id == 1:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="SUPER_USER")
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)
@user_router.get("/self", response_model=UserOut) @user_router.get("/self", response_model=UserOut)
@ -52,24 +70,6 @@ async def get_logged_in_user(
return current_user.dict() return current_user.dict()
@admin_router.get("/{id}", response_model=UserOut)
async def get_user_by_id(
id: int,
session: Session = Depends(generate_session),
):
return db.users.get(session, id)
@user_router.put("/{id}/reset-password")
async def reset_user_password(
id: int,
session: Session = Depends(generate_session),
):
new_password = get_password_hash(settings.DEFAULT_PASSWORD)
db.users.update_password(session, id, new_password)
@user_router.put("/{id}") @user_router.put("/{id}")
async def update_user( async def update_user(
id: int, id: int,
@ -91,117 +91,4 @@ async def update_user(
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))
token = {"access_token": access_token, "token_type": "bearer"} return {"access_token": access_token, "token_type": "bearer"}
return token
@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)
for recipe_image in user_dir.glob("profile_image.*"):
return FileResponse(recipe_image)
else:
raise HTTPException(status.HTTP_404_NOT_FOUND)
@user_router.post("/{id}/image")
def update_user_image(
id: str,
profile_image: UploadFile = File(...),
current_user: UserInDB = Depends(get_current_user),
):
""" Updates a User Image """
assert_user_change_allowed(id, current_user)
extension = profile_image.filename.split(".")[-1]
app_dirs.USER_DIR.joinpath(id).mkdir(parents=True, exist_ok=True)
[x.unlink() for x in app_dirs.USER_DIR.joinpath(id).glob("profile_image.*")]
dest = app_dirs.USER_DIR.joinpath(id, f"profile_image.{extension}")
with dest.open("wb") as buffer:
shutil.copyfileobj(profile_image.file, buffer)
if not dest.is_file:
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
@user_router.put("/{id}/password")
def update_password(
id: int,
password_change: ChangePassword,
current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session),
):
""" Resets the User Password"""
assert_user_change_allowed(id, current_user)
match_passwords = verify_password(password_change.current_password, current_user.password)
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)
@user_router.get("/{id}/favorites", response_model=UserFavorites)
async def get_favorites(id: str, session: Session = Depends(generate_session)):
""" Get user's favorite recipes """
return db.users.get(session, id, override_schema=UserFavorites)
@user_router.post("/{id}/favorites/{slug}")
def add_favorite(
slug: str,
current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session),
):
""" Adds a Recipe to the users favorites """
assert_user_change_allowed(id, current_user)
current_user.favorite_recipes.append(slug)
db.users.update(session, current_user.id, current_user)
@user_router.delete("/{id}/favorites/{slug}")
def remove_favorite(
slug: str,
current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session),
):
""" Adds a Recipe to the users favorites """
assert_user_change_allowed(id, current_user)
current_user.favorite_recipes = [x for x in current_user.favorite_recipes if x != slug]
db.users.update(session, current_user.id, current_user)
return
@admin_router.delete("/{id}")
def delete_user(
background_tasks: BackgroundTasks,
id: int,
session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user),
):
""" Removes a user from the database. Must be the current user or a super user"""
assert_user_change_allowed(id, current_user)
if id == 1:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="SUPER_USER")
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

@ -0,0 +1,47 @@
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.routes.users._helpers import assert_user_change_allowed
from mealie.schema.user import UserFavorites, UserInDB
from sqlalchemy.orm.session import Session
user_router = UserAPIRouter()
@user_router.get("/{id}/favorites", response_model=UserFavorites)
async def get_favorites(id: str, session: Session = Depends(generate_session)):
""" Get user's favorite recipes """
return db.users.get(session, id, override_schema=UserFavorites)
@user_router.post("/{id}/favorites/{slug}")
def add_favorite(
slug: str,
current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session),
):
""" Adds a Recipe to the users favorites """
assert_user_change_allowed(id, current_user)
current_user.favorite_recipes.append(slug)
db.users.update(session, current_user.id, current_user)
@user_router.delete("/{id}/favorites/{slug}")
def remove_favorite(
slug: str,
current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session),
):
""" Adds a Recipe to the users favorites """
assert_user_change_allowed(id, current_user)
current_user.favorite_recipes = [x for x in current_user.favorite_recipes if x != slug]
db.users.update(session, current_user.id, current_user)
return

View File

@ -0,0 +1,48 @@
import shutil
from fastapi import Depends, File, HTTPException, UploadFile, status
from fastapi.responses import FileResponse
from fastapi.routing import APIRouter
from mealie.core.config import app_dirs
from mealie.routes.deps import get_current_user
from mealie.routes.routers import UserAPIRouter
from mealie.routes.users._helpers import assert_user_change_allowed
from mealie.schema.user import UserInDB
public_router = APIRouter(prefix="", tags=["Users: Images"])
user_router = UserAPIRouter(prefix="", tags=["Users: Images"])
@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)
for recipe_image in user_dir.glob("profile_image.*"):
return FileResponse(recipe_image)
else:
raise HTTPException(status.HTTP_404_NOT_FOUND)
@user_router.post("/{id}/image")
def update_user_image(
id: str,
profile_image: UploadFile = File(...),
current_user: UserInDB = Depends(get_current_user),
):
""" Updates a User Image """
assert_user_change_allowed(id, current_user)
extension = profile_image.filename.split(".")[-1]
app_dirs.USER_DIR.joinpath(id).mkdir(parents=True, exist_ok=True)
[x.unlink() for x in app_dirs.USER_DIR.joinpath(id).glob("profile_image.*")]
dest = app_dirs.USER_DIR.joinpath(id, f"profile_image.{extension}")
with dest.open("wb") as buffer:
shutil.copyfileobj(profile_image.file, buffer)
if not dest.is_file:
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@ -0,0 +1,41 @@
from fastapi import Depends, HTTPException, status
from mealie.core.config import 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 UserAPIRouter
from mealie.routes.users._helpers import assert_user_change_allowed
from mealie.schema.user import ChangePassword, UserInDB
from sqlalchemy.orm.session import Session
user_router = UserAPIRouter(prefix="")
@user_router.put("/{id}/reset-password")
async def reset_user_password(
id: int,
session: Session = Depends(generate_session),
):
new_password = get_password_hash(settings.DEFAULT_PASSWORD)
db.users.update_password(session, id, new_password)
@user_router.put("/{id}/password")
def update_password(
id: int,
password_change: ChangePassword,
current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session),
):
""" Resets the User Password"""
assert_user_change_allowed(id, current_user)
match_passwords = verify_password(password_change.current_password, current_user.password)
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)

View File

@ -6,12 +6,13 @@ 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_admin_user from mealie.routes.deps import get_admin_user
from mealie.routes.routers import AdminAPIRouter from mealie.routes.routers import AdminAPIRouter
from mealie.schema.user import SignUpIn, SignUpOut, SignUpToken, UserIn, UserInDB from mealie.schema.user import (SignUpIn, SignUpOut, SignUpToken, 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
public_router = APIRouter(prefix="/api/users/sign-ups", tags=["User Signup"]) public_router = APIRouter(prefix="/sign-ups")
admin_router = AdminAPIRouter(prefix="/api/users/sign-ups", tags=["User Signup"]) admin_router = AdminAPIRouter(prefix="/sign-ups")
@admin_router.get("", response_model=list[SignUpOut]) @admin_router.get("", response_model=list[SignUpOut])