feature/multi-tenancy and move caddy server (#980)

* update to GUIDs

* fix cookbook id relationships

* update webhook keys

* cleanup naming and attribute orders

* remove old database tables

* fix meal-plan images

* remove dashbaord and events api

* use recipe-id instead of id

* cleanup documentation assets

* cleanup docs for v1 beta-release

* add depends_on for docker-compose

* use docker volumes for examples

* move caddy to frontend container
This commit is contained in:
Hayden
2022-02-20 14:17:51 -09:00
committed by GitHub
parent 14cc541f7a
commit 602f248541
91 changed files with 187 additions and 1170 deletions

View File

@@ -6,10 +6,8 @@ from mealie.core.config import get_app_settings
from mealie.core.root_logger import get_logger
from mealie.core.settings.static import APP_VERSION
from mealie.routes import backup_routes, router, utility_routes
from mealie.routes.about import about_router
from mealie.routes.handlers import register_debug_handler
from mealie.routes.media import media_router
from mealie.services.events import create_general_event
from mealie.services.scheduler import SchedulerRegistry, SchedulerService, tasks
logger = get_logger()
@@ -56,7 +54,6 @@ def start_scheduler():
SchedulerService.start()
SchedulerRegistry.register_daily(
tasks.purge_events_database,
tasks.purge_group_registration,
tasks.auto_backup,
tasks.purge_password_reset_tokens,
@@ -72,7 +69,6 @@ def start_scheduler():
def api_routers():
app.include_router(router)
app.include_router(media_router)
app.include_router(about_router)
app.include_router(backup_routes.router)
app.include_router(utility_routes.router)
@@ -103,8 +99,6 @@ def system_startup():
)
)
create_general_event("Application Startup", f"Mealie API started on port {settings.API_PORT}")
def main():
uvicorn.run(

View File

@@ -7,7 +7,6 @@ from mealie.repos.repository_factory import AllRepositories
from mealie.repos.seed.init_users import default_user_init
from mealie.repos.seed.seeders import IngredientFoodsSeeder, IngredientUnitsSeeder, MultiPurposeLabelSeeder
from mealie.schema.user.user import GroupBase
from mealie.services.events import create_general_event
from mealie.services.group_services.group_utils import create_new_group
logger = root_logger.get_logger("init_db")
@@ -62,7 +61,6 @@ def main():
else:
logger.info("Database Doesn't Exists, Initializing...")
init_db(db)
create_general_event("Initialize Database", "Initialize database with default values", session)
if __name__ == "__main__":

View File

@@ -1,7 +1,5 @@
from .event import *
from .group import *
from .labels import *
from .recipe.recipe import *
from .server import *
from .sign_up import *
from .users import *

View File

@@ -1,18 +0,0 @@
from sqlalchemy import Column, DateTime, Integer, String
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
from ._model_utils import auto_init
class Event(SqlAlchemyBase, BaseMixins):
__tablename__ = "events"
id = Column(Integer, primary_key=True)
title = Column(String)
text = Column(String)
time_stamp = Column(DateTime)
category = Column(String)
@auto_init()
def __init__(self, **_) -> None:
pass

View File

@@ -7,7 +7,7 @@ from ..recipe.category import Category, cookbooks_to_categories
class CookBook(SqlAlchemyBase, BaseMixins):
__tablename__ = "cookbooks"
id = Column(Integer, primary_key=True)
id = Column(guid.GUID, primary_key=True, default=guid.GUID.generate)
position = Column(Integer, nullable=False, default=1)
name = Column(String, nullable=False)

View File

@@ -9,7 +9,7 @@ from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import GUID, auto_init
from ..group.invite_tokens import GroupInviteToken
from ..group.webhooks import GroupWebhooksModel
from ..recipe.category import Category, group2categories
from ..recipe.category import Category, group_to_categories
from ..server.task import ServerTaskModel
from .cookbook import CookBook
from .mealplan import GroupMealPlan
@@ -21,7 +21,7 @@ class Group(SqlAlchemyBase, BaseMixins):
id = sa.Column(GUID, primary_key=True, default=GUID.generate)
name = sa.Column(sa.String, index=True, nullable=False, unique=True)
users = orm.relationship("User", back_populates="group")
categories = orm.relationship(Category, secondary=group2categories, single_parent=True, uselist=True)
categories = orm.relationship(Category, secondary=group_to_categories, single_parent=True, uselist=True)
invite_tokens = orm.relationship(
GroupInviteToken, back_populates="group", cascade="all, delete-orphan", uselist=True

View File

@@ -9,6 +9,8 @@ from .._model_utils import auto_init
class GroupPreferencesModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "group_preferences"
id = sa.Column(GUID, primary_key=True, default=GUID.generate)
group_id = sa.Column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
group = orm.relationship("Group", back_populates="preferences")

View File

@@ -1,4 +1,4 @@
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm
from sqlalchemy import Boolean, Column, ForeignKey, String, orm
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import GUID, auto_init
@@ -6,7 +6,7 @@ from .._model_utils import GUID, auto_init
class GroupWebhooksModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "webhook_urls"
id = Column(Integer, primary_key=True)
id = Column(GUID, primary_key=True, default=GUID.generate)
group = orm.relationship("Group", back_populates="webhooks", single_parent=True)
group_id = Column(GUID, ForeignKey("groups.id"), index=True)

View File

@@ -11,8 +11,8 @@ from .._model_utils.guid import GUID
logger = root_logger.get_logger()
group2categories = sa.Table(
"group2categories",
group_to_categories = sa.Table(
"group_to_categories",
SqlAlchemyBase.metadata,
sa.Column("group_id", GUID, sa.ForeignKey("groups.id")),
sa.Column("category_id", GUID, sa.ForeignKey("categories.id")),
@@ -35,7 +35,7 @@ recipes_to_categories = sa.Table(
cookbooks_to_categories = sa.Table(
"cookbooks_to_categories",
SqlAlchemyBase.metadata,
sa.Column("cookbook_id", sa.Integer, sa.ForeignKey("cookbooks.id")),
sa.Column("cookbook_id", GUID, sa.ForeignKey("cookbooks.id")),
sa.Column("category_id", GUID, sa.ForeignKey("categories.id")),
)

View File

@@ -27,12 +27,12 @@ plan_rules_to_tags = sa.Table(
class Tag(SqlAlchemyBase, BaseMixins):
__tablename__ = "tags"
__table_args__ = (sa.UniqueConstraint("slug", "group_id", name="tags_slug_group_id_key"),)
id = sa.Column(guid.GUID, primary_key=True, default=guid.GUID.generate)
# ID Relationships
group_id = sa.Column(guid.GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
group = orm.relationship("Group", back_populates="tags", foreign_keys=[group_id])
id = sa.Column(guid.GUID, primary_key=True, default=guid.GUID.generate)
name = sa.Column(sa.String, index=True, nullable=False)
slug = sa.Column(sa.String, index=True, nullable=False)
recipes = orm.relationship("RecipeModel", secondary=recipes_to_tags, back_populates="tags")

View File

@@ -15,8 +15,8 @@ recipes_to_tools = Table(
class Tool(SqlAlchemyBase, BaseMixins):
__tablename__ = "tools"
id = Column(GUID, primary_key=True, default=GUID.generate)
__table_args__ = (UniqueConstraint("slug", "group_id", name="tools_slug_group_id_key"),)
id = Column(GUID, primary_key=True, default=GUID.generate)
# ID Relationships
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False)

View File

@@ -1,17 +0,0 @@
from sqlalchemy import Boolean, Column, Integer, String
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
from ._model_utils import auto_init
class SignUp(SqlAlchemyBase, BaseMixins):
__tablename__ = "sign_ups"
id = Column(Integer, primary_key=True)
token = Column(String, nullable=False, index=True)
name = Column(String, index=True)
admin = Column(Boolean, default=False)
@auto_init()
def __init__(self, **_) -> None:
pass

View File

@@ -2,7 +2,6 @@ from functools import cached_property
from sqlalchemy.orm import Session
from mealie.db.models.event import Event
from mealie.db.models.group import Group, GroupMealPlan, ReportEntryModel, ReportModel
from mealie.db.models.group.cookbook import CookBook
from mealie.db.models.group.events import GroupEventNotifierModel
@@ -26,12 +25,10 @@ from mealie.db.models.recipe.shared import RecipeShareTokenModel
from mealie.db.models.recipe.tag import Tag
from mealie.db.models.recipe.tool import Tool
from mealie.db.models.server.task import ServerTaskModel
from mealie.db.models.sign_up import SignUp
from mealie.db.models.users import LongLiveToken, User
from mealie.db.models.users.password_reset import PasswordResetModel
from mealie.repos.repository_meal_plan_rules import RepositoryMealPlanRules
from mealie.schema.cookbook.cookbook import ReadCookBook
from mealie.schema.events import Event as EventSchema
from mealie.schema.group.group_events import GroupEventNotifierOut
from mealie.schema.group.group_exports import GroupDataExport
from mealie.schema.group.group_preferences import ReadGroupPreferences
@@ -52,7 +49,7 @@ from mealie.schema.recipe.recipe_ingredient import IngredientFood, IngredientUni
from mealie.schema.recipe.recipe_share_token import RecipeShareToken
from mealie.schema.reports.reports import ReportEntryOut, ReportOut
from mealie.schema.server import ServerTask
from mealie.schema.user import GroupInDB, LongLiveTokenInDB, PrivateUser, SignUpOut
from mealie.schema.user import GroupInDB, LongLiveTokenInDB, PrivateUser
from mealie.schema.user.user_passwords import PrivatePasswordResetToken
from .repository_generic import RepositoryGeneric
@@ -114,7 +111,6 @@ class AllRepositories:
@cached_property
def categories(self) -> RepositoryCategories:
# TODO: Fix Typing for Category Repository
return RepositoryCategories(self.session, PK_ID, Category, CategoryOut)
@cached_property
@@ -125,17 +121,6 @@ class AllRepositories:
def recipe_share_tokens(self) -> RepositoryGeneric[RecipeShareToken, RecipeShareTokenModel]:
return RepositoryGeneric(self.session, PK_ID, RecipeShareTokenModel, RecipeShareToken)
# ================================================================
# Site
@cached_property
def sign_up(self) -> RepositoryGeneric[SignUpOut, SignUp]:
return RepositoryGeneric(self.session, PK_ID, SignUp, SignUpOut)
@cached_property
def events(self) -> RepositoryGeneric[EventSchema, Event]:
return RepositoryGeneric(self.session, PK_ID, Event, EventSchema)
# ================================================================
# User

View File

@@ -1,7 +0,0 @@
from fastapi import APIRouter
from . import events
about_router = APIRouter(prefix="/api/about")
about_router.include_router(events.router, tags=["Events: CRUD"])

View File

@@ -1,25 +0,0 @@
from mealie.routes._base.routers import AdminAPIRouter
from mealie.schema.events import EventsOut
from .._base import BaseAdminController, controller
router = AdminAPIRouter(prefix="/events")
@controller(router)
class EventsController(BaseAdminController):
@router.get("", response_model=EventsOut)
async def get_events(self):
"""Get event from the Database"""
return EventsOut(total=self.repos.events.count_all(), events=self.repos.events.get_all(order_by="time_stamp"))
@router.delete("")
async def delete_events(self):
"""Get event from the Database"""
self.repos.events.delete_all()
return {"message": "All events deleted"}
@router.delete("/{item_id}")
async def delete_event(self, item_id: int):
"""Delete event from the Database"""
return self.repos.events.delete(item_id)

View File

@@ -1,7 +1,7 @@
from datetime import timedelta
from typing import Optional
from fastapi import APIRouter, BackgroundTasks, Depends, Form, Request, status
from fastapi import APIRouter, Depends, Form, status
from fastapi.exceptions import HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from pydantic import BaseModel
@@ -13,7 +13,6 @@ from mealie.core.security import authenticate_user
from mealie.db.db_setup import generate_session
from mealie.routes._base.routers import UserAPIRouter
from mealie.schema.user import PrivateUser
from mealie.services.events import create_user_event
public_router = APIRouter(tags=["Users: Authentication"])
user_router = UserAPIRouter(tags=["Users: Authentication"])
@@ -50,8 +49,6 @@ class MealieAuthToken(BaseModel):
@public_router.post("/token")
def get_token(
background_tasks: BackgroundTasks,
request: Request,
data: CustomOAuth2Form = Depends(),
session: Session = Depends(generate_session),
):
@@ -61,9 +58,6 @@ def get_token(
user: PrivateUser = authenticate_user(session, email, password)
if not user:
background_tasks.add_task(
create_user_event, "Failed Login", f"Username: {email}, Source IP: '{request.client.host}'"
)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
headers={"WWW-Authenticate": "Bearer"},

View File

@@ -1,8 +1,7 @@
import operator
import shutil
from pathlib import Path
from fastapi import BackgroundTasks, Depends, File, HTTPException, UploadFile, status
from fastapi import Depends, File, HTTPException, UploadFile, status
from sqlalchemy.orm.session import Session
from mealie.core.config import get_app_dirs
@@ -16,7 +15,6 @@ from mealie.schema.admin import AllBackups, BackupFile, CreateBackup, ImportJob
from mealie.schema.user.user import PrivateUser
from mealie.services.backups import imports
from mealie.services.backups.exports import backup_all
from mealie.services.events import create_backup_event
router = AdminAPIRouter(prefix="/api/backups", tags=["Backups"])
logger = get_logger()
@@ -38,9 +36,7 @@ def available_imports():
@router.post("/export/database", status_code=status.HTTP_201_CREATED)
def export_database(
background_tasks: BackgroundTasks, data: CreateBackup, session: Session = Depends(generate_session)
):
def export_database(data: CreateBackup, session: Session = Depends(generate_session)):
"""Generates a backup of the recipe database in json format."""
try:
export_path = backup_all(
@@ -52,9 +48,7 @@ def export_database(
export_groups=data.options.groups,
export_notifications=data.options.notifications,
)
background_tasks.add_task(
create_backup_event, "Database Backup", f"Manual Backup Created '{Path(export_path).name}'", session
)
return {"export_path": export_path}
except Exception as e:
logger.error(e)
@@ -83,15 +77,13 @@ async def download_backup_file(file_name: str):
@router.post("/{file_name}/import", status_code=status.HTTP_200_OK)
def import_database(
background_tasks: BackgroundTasks,
file_name: str,
import_data: ImportJob,
session: Session = Depends(generate_session),
user: PrivateUser = Depends(get_current_user),
):
"""Import a database backup file generated from Mealie."""
db_import = imports.import_database(
return imports.import_database(
user=user,
session=session,
archive=import_data.name,
@@ -103,9 +95,6 @@ def import_database(
rebase=import_data.rebase,
)
background_tasks.add_task(create_backup_event, "Database Restore", f"Restore File: {file_name}", session)
return db_import
@router.delete("/{file_name}/delete", status_code=status.HTTP_200_OK)
def delete_backup(file_name: str):

View File

@@ -2,6 +2,7 @@ from functools import cached_property
from typing import Type
from fastapi import APIRouter
from pydantic import UUID4
from mealie.core.exceptions import mealie_registered_exceptions
from mealie.routes._base import BaseUserController, controller
@@ -54,17 +55,16 @@ class GroupCookbookController(BaseUserController):
return updated
@router.get("/{item_id}", response_model=RecipeCookBook)
def get_one(self, item_id: str):
try:
item_id = int(item_id)
return self.mixins.get_one(item_id)
except Exception:
def get_one(self, item_id: UUID4 | str):
if isinstance(item_id, str):
self.mixins.get_one(item_id, key="slug")
else:
return self.mixins.get_one(item_id)
@router.put("/{item_id}", response_model=RecipeCookBook)
def update_one(self, item_id: int, data: CreateCookBook):
def update_one(self, item_id: str, data: CreateCookBook):
return self.mixins.update_one(data, item_id)
@router.delete("/{item_id}", response_model=RecipeCookBook)
def delete_one(self, item_id: int):
def delete_one(self, item_id: str):
return self.mixins.delete_one(item_id)

View File

@@ -1,6 +1,7 @@
from functools import cached_property
from fastapi import APIRouter, Depends
from pydantic import UUID4
from mealie.routes._base.abc_controller import BaseUserController
from mealie.routes._base.controller import controller
@@ -32,13 +33,13 @@ class ReadWebhookController(BaseUserController):
return self.mixins.create_one(save)
@router.get("/{item_id}", response_model=ReadWebhook)
def get_one(self, item_id: int):
def get_one(self, item_id: UUID4):
return self.mixins.get_one(item_id)
@router.put("/{item_id}", response_model=ReadWebhook)
def update_one(self, item_id: int, data: CreateWebhook):
def update_one(self, item_id: UUID4, data: CreateWebhook):
return self.mixins.update_one(data, item_id)
@router.delete("/{item_id}", response_model=ReadWebhook)
def delete_one(self, item_id: int):
def delete_one(self, item_id: UUID4):
return self.mixins.delete_one(item_id) # type: ignore

View File

@@ -48,9 +48,6 @@ else
add_user
init
# Web Server
caddy start --config /app/Caddyfile
# Start API
# uvicorn mealie.app:app --host 0.0.0.0 --port 9000
gunicorn mealie.app:app -b 0.0.0.0:9000 -k uvicorn.workers.UvicornWorker -c /app/gunicorn_conf.py --preload

View File

@@ -28,7 +28,7 @@ class SaveCookBook(CreateCookBook):
class UpdateCookBook(SaveCookBook):
id: int
id: UUID4
class ReadCookBook(UpdateCookBook):

View File

@@ -1,2 +0,0 @@
# GENERATED CODE - DO NOT MODIFY BY HAND
from .events import *

View File

@@ -1,37 +0,0 @@
from datetime import datetime
from enum import Enum
from typing import Optional
from fastapi_camelcase import CamelModel
from pydantic import Field
class EventCategory(str, Enum):
general = "general"
recipe = "recipe"
backup = "backup"
scheduled = "scheduled"
migration = "migration"
group = "group"
user = "user"
class Event(CamelModel):
id: Optional[int]
title: str
text: str
time_stamp: datetime = Field(default_factory=datetime.now)
category: EventCategory = EventCategory.general
class Config:
orm_mode = True
class EventsOut(CamelModel):
total: int
events: list[Event]
class TestEvent(CamelModel):
id: Optional[int]
test_url: Optional[str]

View File

@@ -1,6 +1,7 @@
from uuid import UUID
from fastapi_camelcase import CamelModel
from pydantic import UUID4
class UpdateGroupPreferences(CamelModel):
@@ -21,7 +22,7 @@ class CreateGroupPreferences(UpdateGroupPreferences):
class ReadGroupPreferences(CreateGroupPreferences):
id: int
id: UUID4
class Config:
orm_mode = True

View File

@@ -1,6 +1,7 @@
from uuid import UUID
from fastapi_camelcase import CamelModel
from pydantic import UUID4
class CreateWebhook(CamelModel):
@@ -15,7 +16,7 @@ class SaveWebhook(CreateWebhook):
class ReadWebhook(SaveWebhook):
id: int
id: UUID4
class Config:
orm_mode = True

View File

@@ -1,6 +1,5 @@
# GENERATED CODE - DO NOT MODIFY BY HAND
from .auth import *
from .registration import *
from .sign_up import *
from .user import *
from .user_passwords import *

View File

@@ -1,17 +0,0 @@
from fastapi_camelcase import CamelModel
class SignUpIn(CamelModel):
name: str
admin: bool
class SignUpToken(SignUpIn):
token: str
class SignUpOut(SignUpToken):
id: int
class Config:
orm_mode = True

View File

@@ -1,37 +0,0 @@
from sqlalchemy.orm.session import Session
from mealie.db.db_setup import create_session
from mealie.repos.all_repositories import get_repositories
from mealie.schema.events import Event, EventCategory
def save_event(title, text, category, session: Session):
event = Event(title=title, text=text, category=category)
session = session or create_session()
db = get_repositories(session)
db.events.create(event.dict())
def create_general_event(title, text, session=None):
category = EventCategory.general
save_event(title=title, text=text, category=category, session=session)
def create_recipe_event(title, text, session=None, **_):
category = EventCategory.recipe
save_event(title=title, text=text, category=category, session=session)
def create_backup_event(title, text, session=None):
category = EventCategory.backup
save_event(title=title, text=text, category=category, session=session)
def create_group_event(title, text, session=None):
category = EventCategory.group
save_event(title=title, text=text, category=category, session=session)
def create_user_event(title, text, session=None):
category = EventCategory.user
save_event(title=title, text=text, category=category, session=session)

View File

@@ -1,5 +1,4 @@
from .auto_backup import *
from .purge_events import *
from .purge_group_exports import *
from .purge_password_reset import *
from .purge_registration import *

View File

@@ -4,7 +4,6 @@ from mealie.core.config import get_app_dirs
app_dirs = get_app_dirs()
from mealie.db.db_setup import create_session
from mealie.services.backups.exports import backup_all
from mealie.services.events import create_backup_event
logger = root_logger.get_logger()
@@ -17,6 +16,5 @@ def auto_backup():
session = create_session()
backup_all(session=session, tag="Auto", templates=templates)
logger.info("generating automated backup")
create_backup_event("Automated Backup", "Automated backup created", session)
session.close()
logger.info("automated backup generated")

View File

@@ -1,19 +0,0 @@
import datetime
from mealie.core import root_logger
from mealie.db.db_setup import create_session
from mealie.db.models.event import Event
logger = root_logger.get_logger()
def purge_events_database():
"""Purges all events after 100"""
logger.info("purging events in database")
expiration_days = 7
limit = datetime.datetime.now() - datetime.timedelta(days=expiration_days)
session = create_session()
session.query(Event).filter(Event.time_stamp <= limit).delete()
session.commit()
session.close()
logger.info("events purges")