Refactor/conver to controllers (#923)

* add dependency injection for get_repositories

* convert events api to controller

* update generic typing

* add abstract controllers

* update test naming

* migrate admin services to controllers

* add additional admin route tests

* remove print

* add public shared dependencies

* add types

* fix typo

* add static variables for recipe json keys

* add coverage gutters config

* update controller routers

* add generic success response

* add category/tag/tool tests

* add token refresh test

* add coverage utilities

* covert comments to controller

* add todo

* add helper properties

* delete old service

* update test notes

* add unit test for pretty_stats

* remove dead code from post_webhooks

* update group routes to use controllers

* add additional group test coverage

* abstract common permission checks

* convert ingredient parser to controller

* update recipe crud to use controller

* remove dead-code

* add class lifespan tracker for debugging

* convert bulk export to controller

* migrate tools router to controller

* update recipe share to controller

* move customer router to _base

* ignore prints in flake8

* convert units and foods to new controllers

* migrate user routes to controllers

* centralize error handling

* fix invalid ref

* reorder fields

* update routers to share common handling

* update tests

* remove prints

* fix cookbooks delete

* fix cookbook get

* add controller for mealplanner

* cover report routes to controller

* remove __future__ imports

* remove dead code

* remove all base_http children and remove dead code
This commit is contained in:
Hayden
2022-01-13 13:06:52 -09:00
committed by GitHub
parent 5823a32daf
commit c4540f1395
164 changed files with 3111 additions and 3213 deletions

View File

@@ -1,6 +1,6 @@
from fastapi import APIRouter
from . import api_tokens, crud, favorites, images, passwords, registration
from . import api_tokens, crud, favorites, forgot_password, images, registration
# Must be used because of the way FastAPI works with nested routes
user_prefix = "/users"
@@ -8,16 +8,9 @@ user_prefix = "/users"
router = APIRouter()
router.include_router(registration.router, prefix=user_prefix, tags=["Users: Registration"])
router.include_router(crud.user_router, prefix=user_prefix, tags=["Users: CRUD"])
router.include_router(crud.admin_router, prefix=user_prefix, tags=["Users: CRUD"])
router.include_router(passwords.user_router, prefix=user_prefix, tags=["Users: Passwords"])
router.include_router(passwords.public_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"])
router.include_router(crud.user_router)
router.include_router(crud.admin_router)
router.include_router(forgot_password.router, prefix=user_prefix, tags=["Users: Passwords"])
router.include_router(images.router, prefix=user_prefix, tags=["Users: Images"])
router.include_router(api_tokens.router)
router.include_router(favorites.router, prefix=user_prefix, tags=["Users: Favorites"])

View File

@@ -1,61 +1,50 @@
from datetime import timedelta
from fastapi import HTTPException, status
from fastapi.param_functions import Depends
from sqlalchemy.orm.session import Session
from mealie.core.dependencies import get_current_user
from mealie.core.security import create_access_token
from mealie.db.db_setup import generate_session
from mealie.repos.all_repositories import get_repositories
from mealie.routes.routers import UserAPIRouter
from mealie.schema.user import CreateToken, LoingLiveTokenIn, LongLiveTokenInDB, PrivateUser
from mealie.routes._base import BaseUserController, controller
from mealie.routes._base.routers import UserAPIRouter
from mealie.schema.user import CreateToken, LoingLiveTokenIn, LongLiveTokenInDB
router = UserAPIRouter()
router = UserAPIRouter(prefix="/users", tags=["Users: Tokens"])
@router.post("/api-tokens", status_code=status.HTTP_201_CREATED)
async def create_api_token(
token_name: LoingLiveTokenIn,
current_user: PrivateUser = Depends(get_current_user),
session: Session = Depends(generate_session),
):
"""Create api_token in the Database"""
@controller(router)
class UserApiTokensController(BaseUserController):
@router.post("/api-tokens", status_code=status.HTTP_201_CREATED)
def create_api_token(
self,
token_name: LoingLiveTokenIn,
):
"""Create api_token in the Database"""
token_data = {"long_token": True, "id": str(current_user.id)}
token_data = {"long_token": True, "id": str(self.user.id)}
five_years = timedelta(1825)
token = create_access_token(token_data, five_years)
five_years = timedelta(1825)
token = create_access_token(token_data, five_years)
token_model = CreateToken(
name=token_name.name,
token=token,
user_id=current_user.id,
)
token_model = CreateToken(
name=token_name.name,
token=token,
user_id=self.user.id,
)
db = get_repositories(session)
new_token_in_db = self.repos.api_tokens.create(token_model)
new_token_in_db = db.api_tokens.create(token_model)
if new_token_in_db:
return {"token": token}
if new_token_in_db:
return {"token": token}
@router.delete("/api-tokens/{token_id}")
def delete_api_token(self, token_id: int):
"""Delete api_token from the Database"""
token: LongLiveTokenInDB = self.repos.api_tokens.get(token_id)
if not token:
raise HTTPException(status.HTTP_404_NOT_FOUND, f"Could not locate token with id '{token_id}' in database")
@router.delete("/api-tokens/{token_id}")
async def delete_api_token(
token_id: int,
current_user: PrivateUser = Depends(get_current_user),
session: Session = Depends(generate_session),
):
"""Delete api_token from the Database"""
db = get_repositories(session)
token: LongLiveTokenInDB = db.api_tokens.get(token_id)
if not token:
raise HTTPException(status.HTTP_404_NOT_FOUND, f"Could not locate token with id '{token_id}' in database")
if token.user.email == current_user.email:
deleted_token = db.api_tokens.delete(token_id)
return {"token_delete": deleted_token.name}
else:
raise HTTPException(status.HTTP_403_FORBIDDEN)
if token.user.email == self.user.email:
deleted_token = self.repos.api_tokens.delete(token_id)
return {"token_delete": deleted_token.name}
else:
raise HTTPException(status.HTTP_403_FORBIDDEN)

View File

@@ -1,100 +1,79 @@
from fastapi import BackgroundTasks, Depends, HTTPException, status
from fastapi import HTTPException, status
from pydantic import UUID4
from sqlalchemy.orm.session import Session
from mealie.core import security
from mealie.core.dependencies import get_current_user
from mealie.core.security import hash_password
from mealie.db.db_setup import generate_session
from mealie.repos.all_repositories import get_repositories
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
from mealie.core.security import hash_password, verify_password
from mealie.routes._base import BaseAdminController, controller
from mealie.routes._base.abc_controller import BaseUserController
from mealie.routes._base.mixins import CrudMixins
from mealie.routes._base.routers import AdminAPIRouter, UserAPIRouter
from mealie.routes.users._helpers import assert_user_change_allowed
from mealie.schema.user import PrivateUser, UserBase, UserIn, UserOut
from mealie.services.events import create_user_event
from mealie.schema.user import ChangePassword, UserBase, UserIn, UserOut
user_router = UserAPIRouter(prefix="")
admin_router = AdminAPIRouter(prefix="")
user_router = UserAPIRouter(prefix="/users", tags=["Users: CRUD"])
admin_router = AdminAPIRouter(prefix="/users", tags=["Users: Admin CRUD"])
@admin_router.get("", response_model=list[UserOut])
async def get_all_users(session: Session = Depends(generate_session)):
db = get_repositories(session)
return db.users.get_all()
@controller(admin_router)
class AdminUserController(BaseAdminController):
@property
def mixins(self) -> CrudMixins:
return CrudMixins[UserIn, UserOut, UserBase](self.repos.users, self.deps.logger)
@admin_router.get("", response_model=list[UserOut])
def get_all_users(self):
return self.repos.users.get_all()
@admin_router.post("", response_model=UserOut, status_code=201)
def create_user(self, new_user: UserIn):
new_user.password = hash_password(new_user.password)
return self.mixins.create_one(new_user)
@admin_router.get("/{item_id}", response_model=UserOut)
def get_user(self, item_id: UUID4):
return self.mixins.get_one(item_id)
@admin_router.delete("/{item_id}")
def delete_user(self, item_id: UUID4):
"""Removes a user from the database. Must be the current user or a super user"""
assert_user_change_allowed(item_id, self.user)
if item_id == 1: # TODO: identify super_user
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="SUPER_USER")
self.mixins.delete_one(item_id)
@admin_router.post("", response_model=UserOut, status_code=201)
async def create_user(
background_tasks: BackgroundTasks,
new_user: UserIn,
current_user: PrivateUser = Depends(get_current_user),
session: Session = Depends(generate_session),
):
@controller(user_router)
class UserController(BaseUserController):
@user_router.get("/self", response_model=UserOut)
def get_logged_in_user(self):
return self.user
new_user.password = hash_password(new_user.password)
background_tasks.add_task(
create_user_event, "User Created", f"Created by {current_user.full_name}", session=session
)
@user_router.put("/{item_id}")
def update_user(self, item_id: UUID4, new_data: UserBase):
assert_user_change_allowed(item_id, self.user)
db = get_repositories(session)
return db.users.create(new_user.dict())
if not self.user.admin and (new_data.admin or self.user.group != new_data.group):
# prevent a regular user from doing admin tasks on themself
raise HTTPException(status.HTTP_403_FORBIDDEN)
if self.user.id == item_id and self.user.admin and not new_data.admin:
# prevent an admin from demoting themself
raise HTTPException(status.HTTP_403_FORBIDDEN)
@admin_router.get("/{id}", response_model=UserOut)
async def get_user(id: UUID4, session: Session = Depends(generate_session)):
db = get_repositories(session)
return db.users.get(id)
self.repos.users.update(item_id, new_data.dict())
if self.user.id == item_id:
access_token = security.create_access_token(data=dict(sub=new_data.email))
return {"access_token": access_token, "token_type": "bearer"}
@admin_router.delete("/{id}")
def delete_user(
id: UUID4,
background_tasks: BackgroundTasks,
session: Session = Depends(generate_session),
current_user: PrivateUser = Depends(get_current_user),
):
"""Removes a user from the database. Must be the current user or a super user"""
@user_router.put("/{item_id}/password")
def update_password(self, password_change: ChangePassword):
"""Resets the User Password"""
if not verify_password(password_change.current_password, self.user.password):
raise HTTPException(status.HTTP_400_BAD_REQUEST)
assert_user_change_allowed(id, current_user)
if id == 1: # TODO: identify super_user
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="SUPER_USER")
try:
db = get_repositories(session)
db.users.delete(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)
async def get_logged_in_user(
current_user: PrivateUser = Depends(get_current_user),
):
return current_user.dict()
@user_router.put("/{id}")
async def update_user(
id: UUID4,
new_data: UserBase,
current_user: PrivateUser = Depends(get_current_user),
session: Session = Depends(generate_session),
):
assert_user_change_allowed(id, current_user)
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 = get_repositories(session)
db.users.update(id, new_data.dict())
if current_user.id == id:
access_token = security.create_access_token(data=dict(sub=new_data.email))
return {"access_token": access_token, "token_type": "bearer"}
self.user.password = hash_password(password_change.new_password)
return self.repos.users.update_password(self.user.id, self.user.password)

View File

@@ -1,48 +1,31 @@
from fastapi import Depends
from sqlalchemy.orm.session import Session
from pydantic import UUID4
from mealie.core.dependencies import get_current_user
from mealie.db.db_setup import generate_session
from mealie.repos.all_repositories import get_repositories
from mealie.routes.routers import UserAPIRouter
from mealie.routes._base import BaseUserController, controller
from mealie.routes._base.routers import UserAPIRouter
from mealie.routes.users._helpers import assert_user_change_allowed
from mealie.schema.user import PrivateUser, UserFavorites
from mealie.schema.user import UserFavorites
user_router = UserAPIRouter()
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"""
db = get_repositories(session)
return db.users.get(id, override_schema=UserFavorites)
@controller(router)
class UserFavoritesController(BaseUserController):
@router.get("/{id}/favorites", response_model=UserFavorites)
async def get_favorites(self, id: UUID4):
"""Get user's favorite recipes"""
return self.repos.users.get(id, override_schema=UserFavorites)
@router.post("/{id}/favorites/{slug}")
def add_favorite(self, id: UUID4, slug: str):
"""Adds a Recipe to the users favorites"""
assert_user_change_allowed(id, self.user)
self.user.favorite_recipes.append(slug)
self.repos.users.update(self.user.id, self.user)
@user_router.post("/{id}/favorites/{slug}")
def add_favorite(
slug: str,
current_user: PrivateUser = Depends(get_current_user),
session: Session = Depends(generate_session),
):
"""Adds a Recipe to the users favorites"""
current_user.favorite_recipes.append(slug)
db = get_repositories(session)
db.users.update(current_user.id, current_user)
@user_router.delete("/{id}/favorites/{slug}")
def remove_favorite(
slug: str,
current_user: PrivateUser = 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 = get_repositories(session)
db.users.update(current_user.id, current_user)
return
@router.delete("/{id}/favorites/{slug}")
def remove_favorite(self, id: UUID4, slug: str):
"""Adds a Recipe to the users favorites"""
assert_user_change_allowed(id, self.user)
self.user.favorite_recipes = [x for x in self.user.favorite_recipes if x != slug]
self.repos.users.update(self.user.id, self.user)
return

View File

@@ -0,0 +1,22 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm.session import Session
from mealie.db.db_setup import generate_session
from mealie.schema.user.user_passwords import ForgotPassword, ResetPassword
from mealie.services.user_services.password_reset_service import PasswordResetService
router = APIRouter(prefix="")
@router.post("/forgot-password")
def forgot_password(email: ForgotPassword, session: Session = Depends(generate_session)):
"""Sends an email with a reset link to the user"""
f_service = PasswordResetService(session)
return f_service.send_reset_email(email.email)
@router.post("/reset-password")
def reset_password(reset_password: ResetPassword, session: Session = Depends(generate_session)):
"""Resets the user password"""
f_service = PasswordResetService(session)
return f_service.reset_password(reset_password.token, reset_password.password)

View File

@@ -2,48 +2,41 @@ import shutil
from pathlib import Path
from fastapi import Depends, File, HTTPException, UploadFile, status
from fastapi.routing import APIRouter
from pydantic import UUID4
from sqlalchemy.orm.session import Session
from mealie import utils
from mealie.core.dependencies import get_current_user
from mealie.core.dependencies.dependencies import temporary_dir
from mealie.db.db_setup import generate_session
from mealie.repos.all_repositories import get_repositories
from mealie.routes.routers import UserAPIRouter
from mealie.routes._base import BaseUserController, controller
from mealie.routes._base.routers import UserAPIRouter
from mealie.routes.users._helpers import assert_user_change_allowed
from mealie.schema.user import PrivateUser
from mealie.services.image import minify
public_router = APIRouter(prefix="", tags=["Users: Images"])
user_router = UserAPIRouter(prefix="", tags=["Users: Images"])
router = UserAPIRouter(prefix="", tags=["Users: Images"])
@user_router.post("/{id}/image")
def update_user_image(
id: UUID4,
profile: UploadFile = File(...),
temp_dir: Path = Depends(temporary_dir),
current_user: PrivateUser = Depends(get_current_user),
session: Session = Depends(generate_session),
):
"""Updates a User Image"""
assert_user_change_allowed(id, current_user)
@controller(router)
class UserImageController(BaseUserController):
@router.post("/{id}/image")
def update_user_image(
self,
id: UUID4,
profile: UploadFile = File(...),
temp_dir: Path = Depends(temporary_dir),
):
"""Updates a User Image"""
assert_user_change_allowed(id, self.user)
temp_img = temp_dir.joinpath(profile.filename)
temp_img = temp_dir.joinpath(profile.filename)
with temp_img.open("wb") as buffer:
shutil.copyfileobj(profile.file, buffer)
with temp_img.open("wb") as buffer:
shutil.copyfileobj(profile.file, buffer)
image = minify.to_webp(temp_img)
dest = PrivateUser.get_directory(id) / "profile.webp"
image = minify.to_webp(temp_img)
dest = PrivateUser.get_directory(id) / "profile.webp"
shutil.copyfile(image, dest)
shutil.copyfile(image, dest)
self.repos.users.patch(id, {"cache_key": utils.new_cache_key()})
db = get_repositories(session)
db.users.patch(id, {"cache_key": utils.new_cache_key()})
if not dest.is_file:
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
if not dest.is_file:
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@@ -1,44 +0,0 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm.session import Session
from mealie.core.config import get_app_settings
from mealie.core.security import hash_password
from mealie.db.db_setup import generate_session
from mealie.repos.all_repositories import get_repositories
from mealie.routes.routers import UserAPIRouter
from mealie.schema.user import ChangePassword
from mealie.schema.user.user_passwords import ForgotPassword, ResetPassword
from mealie.services.user_services import UserService
from mealie.services.user_services.password_reset_service import PasswordResetService
user_router = UserAPIRouter(prefix="")
public_router = APIRouter(prefix="")
settings = get_app_settings()
@user_router.put("/{id}/reset-password")
async def reset_user_password(id: int, session: Session = Depends(generate_session)):
new_password = hash_password(settings.DEFAULT_PASSWORD)
db = get_repositories(session)
db.users.update_password(id, new_password)
@user_router.put("/{item_id}/password")
def update_password(password_change: ChangePassword, user_service: UserService = Depends(UserService.write_existing)):
"""Resets the User Password"""
return user_service.change_password(password_change)
@public_router.post("/forgot-password")
def forgot_password(email: ForgotPassword, session: Session = Depends(generate_session)):
"""Sends an email with a reset link to the user"""
f_service = PasswordResetService(session)
return f_service.send_reset_email(email.email)
@public_router.post("/reset-password")
def reset_password(reset_password: ResetPassword, session: Session = Depends(generate_session)):
"""Resets the user password"""
f_service = PasswordResetService(session)
return f_service.reset_password(reset_password.token, reset_password.password)

View File

@@ -1,5 +1,7 @@
from fastapi import APIRouter, Depends, status
from fastapi import APIRouter, status
from mealie.repos.all_repositories import get_repositories
from mealie.routes._base import BasePublicController, controller
from mealie.schema.user.registration import CreateUserRegistration
from mealie.schema.user.user import UserOut
from mealie.services.user_services.registration_service import RegistrationService
@@ -7,8 +9,9 @@ from mealie.services.user_services.registration_service import RegistrationServi
router = APIRouter(prefix="/register")
@router.post("", response_model=UserOut, status_code=status.HTTP_201_CREATED)
def register_new_user(
data: CreateUserRegistration, registration_service: RegistrationService = Depends(RegistrationService.public)
):
return registration_service.register_user(data)
@controller(router)
class RegistrationController(BasePublicController):
@router.post("", response_model=UserOut, status_code=status.HTTP_201_CREATED)
def register_new_user(self, data: CreateUserRegistration):
registration_service = RegistrationService(self.deps.logger, get_repositories(self.deps.session))
return registration_service.register_user(data)