+
-
+
{{ item.icon }}
@@ -31,36 +38,70 @@
-
-
\ No newline at end of file
diff --git a/frontend/src/locales/messages/en.json b/frontend/src/locales/messages/en.json
index e0b2e2cad..429f28e82 100644
--- a/frontend/src/locales/messages/en.json
+++ b/frontend/src/locales/messages/en.json
@@ -12,8 +12,9 @@
"take-me-home": "Take me Home"
},
"new-recipe": {
- "from-url": "From URL",
+ "from-url": "Import a Recipe",
"recipe-url": "Recipe URL",
+ "url-form-hint": "Copy and paste a link from your favorite recipe website",
"error-message": "Looks like there was an error parsing the URL. Check the log and debug/last_recipe.json to see what went wrong.",
"bulk-add": "Bulk Add",
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Paste in your recipe data. Each line will be treated as an item in a list"
@@ -97,6 +98,8 @@
},
"settings": {
"general-settings": "General Settings",
+ "change-password": "Change Password",
+ "admin-settings": "Admin Settings",
"local-api": "Local API",
"language": "Language",
"add-a-new-theme": "Add a New Theme",
diff --git a/frontend/src/components/Settings/Backup/index.vue b/frontend/src/pages/Admin/Backup/index.vue
similarity index 87%
rename from frontend/src/components/Settings/Backup/index.vue
rename to frontend/src/pages/Admin/Backup/index.vue
index 56d2998f6..1e6163183 100644
--- a/frontend/src/components/Settings/Backup/index.vue
+++ b/frontend/src/pages/Admin/Backup/index.vue
@@ -48,11 +48,11 @@
+
+
\ No newline at end of file
diff --git a/frontend/src/components/Settings/MealPlanner/index.vue b/frontend/src/pages/Admin/MealPlanner/index.vue
similarity index 98%
rename from frontend/src/components/Settings/MealPlanner/index.vue
rename to frontend/src/pages/Admin/MealPlanner/index.vue
index 5d2a856db..feeb60841 100644
--- a/frontend/src/components/Settings/MealPlanner/index.vue
+++ b/frontend/src/pages/Admin/MealPlanner/index.vue
@@ -97,7 +97,7 @@
\ No newline at end of file
diff --git a/frontend/src/components/Settings/General/index.vue b/frontend/src/pages/Admin/Settings/index.vue
similarity index 62%
rename from frontend/src/components/Settings/General/index.vue
rename to frontend/src/pages/Admin/Settings/index.vue
index 808c16694..13d2bc18f 100644
--- a/frontend/src/components/Settings/General/index.vue
+++ b/frontend/src/pages/Admin/Settings/index.vue
@@ -1,7 +1,7 @@
-
- {{ $t("settings.general-settings") }}
+
+ {{ $t("settings.admin-settings") }}
@@ -10,23 +10,7 @@
-
-
- {{ $t("settings.language") }}
-
-
-
-
-
-
-
+
@@ -34,7 +18,7 @@
+
+
\ No newline at end of file
diff --git a/frontend/src/pages/LoginPage.vue b/frontend/src/pages/LoginPage.vue
new file mode 100644
index 000000000..3d8da7d63
--- /dev/null
+++ b/frontend/src/pages/LoginPage.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/pages/RecipeNewPage.vue b/frontend/src/pages/RecipeNewPage.vue
index d841fe77b..a6d4b93eb 100644
--- a/frontend/src/pages/RecipeNewPage.vue
+++ b/frontend/src/pages/RecipeNewPage.vue
@@ -12,7 +12,7 @@
-
-
-
-
\ No newline at end of file
diff --git a/frontend/src/routes.js b/frontend/src/routes.js
deleted file mode 100644
index 5fbbde2a2..000000000
--- a/frontend/src/routes.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import HomePage from "./pages/HomePage";
-import Page404 from "./pages/404Page";
-import SearchPage from "./pages/SearchPage";
-import RecipePage from "./pages/RecipePage";
-import RecipeNewPage from "./pages/RecipeNewPage";
-import SettingsPage from "./pages/SettingsPage";
-import AllRecipesPage from "./pages/AllRecipesPage";
-import CategoryPage from "./pages/CategoryPage";
-import MeaplPlanPage from "./pages/MealPlanPage";
-import Debug from "./pages/Debug";
-import MealPlanThisWeekPage from "./pages/MealPlanThisWeekPage";
-import api from "@/api";
-
-export const routes = [
- { path: "/", component: HomePage },
- { path: "/mealie", component: HomePage },
- { path: "/debug", component: Debug },
- { path: "/search", component: SearchPage },
- { path: "/recipes/all", component: AllRecipesPage },
- { path: "/recipes/:category", component: CategoryPage },
- { path: "/recipe/:recipe", component: RecipePage },
- { path: "/new/", component: RecipeNewPage },
- { path: "/settings/site", component: SettingsPage },
- { path: "/meal-plan/planner", component: MeaplPlanPage },
- { path: "/meal-plan/this-week", component: MealPlanThisWeekPage },
- {
- path: "/meal-plan/today",
- beforeEnter: async (_to, _from, next) => {
- await todaysMealRoute().then(redirect => {
- next(redirect);
- });
- },
- },
- { path: "*", component: Page404 },
-];
-
-async function todaysMealRoute() {
- const response = await api.mealPlans.today();
- return "/recipe/" + response.data;
-}
diff --git a/frontend/src/routes/admin.js b/frontend/src/routes/admin.js
new file mode 100644
index 000000000..ee7f47e24
--- /dev/null
+++ b/frontend/src/routes/admin.js
@@ -0,0 +1,48 @@
+import Admin from "@/pages/Admin";
+import Backup from "@/pages/Admin/Backup";
+import Theme from "@/pages/Admin/Theme";
+import MealPlanner from "@/pages/Admin/MealPlanner";
+import Migration from "@/pages/Admin/Migration";
+import Profile from "@/pages/Admin/Profile";
+import ManageUsers from "@/pages/Admin/ManageUsers";
+import Settings from "@/pages/Admin/Settings";
+
+export default {
+ path: "/admin",
+ component: Admin,
+ children: [
+ {
+ path: "",
+ component: Profile,
+ },
+ {
+ path: "profile",
+ component: Profile,
+ },
+
+ {
+ path: "backups",
+ component: Backup,
+ },
+ {
+ path: "themes",
+ component: Theme,
+ },
+ {
+ path: "meal-planner",
+ component: MealPlanner,
+ },
+ {
+ path: "migrations",
+ component: Migration,
+ },
+ {
+ path: "manage-users",
+ component: ManageUsers,
+ },
+ {
+ path: "settings",
+ component: Settings,
+ },
+ ],
+};
diff --git a/frontend/src/routes/index.js b/frontend/src/routes/index.js
new file mode 100644
index 000000000..60319b728
--- /dev/null
+++ b/frontend/src/routes/index.js
@@ -0,0 +1,51 @@
+import HomePage from "../pages/HomePage";
+import Page404 from "../pages/404Page";
+import SearchPage from "../pages/SearchPage";
+import RecipePage from "../pages/RecipePage";
+import RecipeNewPage from "../pages/RecipeNewPage";
+import AllRecipesPage from "../pages/AllRecipesPage";
+import CategoryPage from "../pages/CategoryPage";
+import MeaplPlanPage from "../pages/MealPlanPage";
+import Debug from "../pages/Debug";
+import LoginPage from "../pages/LoginPage";
+import MealPlanThisWeekPage from "../pages/MealPlanThisWeekPage";
+import api from "@/api";
+import Admin from "./admin";
+import { store } from "../store/store";
+
+export const routes = [
+ { path: "/", name: "home", component: HomePage },
+ {
+ path: "/logout",
+ beforeEnter: (_to, _from, next) => {
+ store.commit("setToken", "");
+ store.commit("setIsLoggedIn", false);
+ next("/");
+ },
+ },
+ { path: "/mealie", component: HomePage },
+ { path: "/login", component: LoginPage },
+ { path: "/debug", component: Debug },
+ { path: "/search", component: SearchPage },
+ { path: "/recipes/all", component: AllRecipesPage },
+ { path: "/recipes/:category", component: CategoryPage },
+ { path: "/recipe/:recipe", component: RecipePage },
+ { path: "/new/", component: RecipeNewPage },
+ { path: "/meal-plan/planner", component: MeaplPlanPage },
+ { path: "/meal-plan/this-week", component: MealPlanThisWeekPage },
+ Admin,
+ {
+ path: "/meal-plan/today",
+ beforeEnter: async (_to, _from, next) => {
+ await todaysMealRoute().then(redirect => {
+ next(redirect);
+ });
+ },
+ },
+ { path: "*", component: Page404 },
+];
+
+async function todaysMealRoute() {
+ const response = await api.mealPlans.today();
+ return "/recipe/" + response.data;
+}
diff --git a/frontend/src/store/modules/userSettings.js b/frontend/src/store/modules/userSettings.js
index 2908837fc..39c8406de 100644
--- a/frontend/src/store/modules/userSettings.js
+++ b/frontend/src/store/modules/userSettings.js
@@ -1,5 +1,6 @@
import api from "@/api";
import Vuetify from "../../plugins/vuetify";
+import axios from "axios";
function inDarkMode(payload) {
let isDark;
@@ -18,6 +19,8 @@ const state = {
activeTheme: {},
darkMode: "system",
isDark: false,
+ isLoggedIn: false,
+ token: "",
};
const mutations = {
@@ -35,6 +38,14 @@ const mutations = {
state.darkMode = payload;
}
},
+ setIsLoggedIn(state, payload) {
+ state.isLoggedIn = payload;
+ },
+ setToken(state, payload) {
+ state.isLoggedIn = true;
+ axios.defaults.headers.common["Authorization"] = `Bearer ${payload}`;
+ state.token = payload;
+ },
};
const actions = {
@@ -47,6 +58,7 @@ const actions = {
}
},
+
async initTheme({ dispatch, getters }) {
//If theme is empty resetTheme
if (Object.keys(getters.getActiveTheme).length === 0) {
@@ -63,6 +75,8 @@ const getters = {
getActiveTheme: state => state.activeTheme,
getDarkMode: state => state.darkMode,
getIsDark: state => state.isDark,
+ getIsLoggedIn: state => state.isLoggedIn,
+ getToken: state => state.token,
};
export default {
diff --git a/frontend/src/store/store.js b/frontend/src/store/store.js
index a4af49226..19979f278 100644
--- a/frontend/src/store/store.js
+++ b/frontend/src/store/store.js
@@ -20,11 +20,6 @@ const store = new Vuex.Store({
homePage,
},
state: {
- // Home Page Settings
- // Snackbar
- snackActive: false,
- snackText: "",
- snackType: "warning",
// All Recipe Data Store
recentRecipes: [],
@@ -33,15 +28,6 @@ const store = new Vuex.Store({
},
mutations: {
- setSnackBar(state, payload) {
- state.snackText = payload.text;
- state.snackType = payload.type;
- state.snackActive = true;
- },
- setSnackActive(state, payload) {
- state.snackActive = payload;
- },
-
setRecentRecipes(state, payload) {
state.recentRecipes = payload;
},
@@ -68,11 +54,6 @@ const store = new Vuex.Store({
},
getters: {
- //
- getSnackText: state => state.snackText,
- getSnackActive: state => state.snackActive,
- getSnackType: state => state.snackType,
-
getRecentRecipes: state => state.recentRecipes,
getMealPlanCategories: state => state.mealPlanCategories,
},
diff --git a/frontend/vue.config.js b/frontend/vue.config.js
index d24793322..1507b9431 100644
--- a/frontend/vue.config.js
+++ b/frontend/vue.config.js
@@ -1,3 +1,4 @@
+const path = require("path");
module.exports = {
transpileDependencies: ["vuetify"],
publicPath: process.env.NODE_ENV === "production" ? "/" : "/",
@@ -18,4 +19,11 @@ module.exports = {
enableInSFC: true,
},
},
+ configureWebpack: {
+ resolve: {
+ alias: {
+ "@": path.resolve("src"),
+ },
+ },
+ },
};
diff --git a/mealie/app.py b/mealie/app.py
index 1ac1fae83..250f0401a 100644
--- a/mealie/app.py
+++ b/mealie/app.py
@@ -2,7 +2,9 @@ import uvicorn
from fastapi import FastAPI
# import utils.startup as startup
-from app_config import APP_VERSION, PORT, PRODUCTION, docs_url, redoc_url
+from core.config import APP_VERSION, PORT, SECRET, docs_url, redoc_url
+from db.db_setup import sql_exists
+from db.init_db import init_db
from routes import (
backup_routes,
debug_routes,
@@ -17,8 +19,8 @@ from routes.recipe import (
recipe_crud_routes,
tag_routes,
)
-from services.settings_services import default_settings_init
-from utils.logger import logger
+from routes.users import users
+from fastapi.logger import logger
app = FastAPI(
title="Mealie",
@@ -29,16 +31,17 @@ app = FastAPI(
)
+def data_base_first_run():
+ init_db()
+
+
def start_scheduler():
import services.scheduler.scheduled_jobs
-def init_settings():
- default_settings_init()
- import services.theme_services
-
-
def api_routers():
+ # Authentication
+ app.include_router(users.router)
# Recipes
app.include_router(all_recipe_routes.router)
app.include_router(category_routes.router)
@@ -56,10 +59,11 @@ def api_routers():
app.include_router(debug_routes.router)
+if not sql_exists:
+ data_base_first_run()
api_routers()
start_scheduler()
-init_settings()
if __name__ == "__main__":
logger.info("-----SYSTEM STARTUP-----")
diff --git a/mealie/app_config.py b/mealie/core/config.py
similarity index 92%
rename from mealie/app_config.py
rename to mealie/core/config.py
index 257421a20..d322ca232 100644
--- a/mealie/app_config.py
+++ b/mealie/core/config.py
@@ -15,9 +15,12 @@ def ensure_dirs():
ENV = CWD.joinpath(".env")
dotenv.load_dotenv(ENV)
+
+SECRET = "super-secret-key"
+
# General
APP_VERSION = "v0.3.0"
-DB_VERSION = "v0.2.1"
+DB_VERSION = "v0.3.0"
PRODUCTION = os.environ.get("ENV")
PORT = int(os.getenv("mealie_port", 9000))
API = os.getenv("api_docs", True)
@@ -30,7 +33,7 @@ else:
redoc_url = None
# Helpful Globals
-DATA_DIR = CWD.parent.joinpath("app_data")
+DATA_DIR = CWD.parent.parent.joinpath("app_data")
if PRODUCTION:
DATA_DIR = Path("/app/data")
@@ -59,6 +62,8 @@ REQUIRED_DIRS = [
ensure_dirs()
+LOGGER_FILE = DATA_DIR.joinpath("mealie.log")
+
# DATABASE ENV
SQLITE_FILE = None
diff --git a/mealie/core/security.py b/mealie/core/security.py
new file mode 100644
index 000000000..f6cc9a805
--- /dev/null
+++ b/mealie/core/security.py
@@ -0,0 +1,29 @@
+from passlib.context import CryptContext
+
+pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+
+
+def verify_password(plain_password: str, hashed_password: str) -> bool:
+ """Compares a plain string to a hashed password
+
+ Args:
+ plain_password (str): raw password string
+ hashed_password (str): hashed password from the database
+
+ Returns:
+ bool: Returns True if a match return False
+ """
+ return pwd_context.verify(plain_password, hashed_password)
+
+
+def get_password_hash(password: str) -> str:
+ """Takes in a raw password and hashes it. Used prior to saving
+ a new password to the database.
+
+ Args:
+ password (str): Password String
+
+ Returns:
+ str: Hashed Password
+ """
+ return pwd_context.hash(password)
diff --git a/mealie/db/database.py b/mealie/db/database.py
index 04d7abfa4..b74c968c7 100644
--- a/mealie/db/database.py
+++ b/mealie/db/database.py
@@ -1,10 +1,11 @@
from sqlalchemy.orm.session import Session
from db.db_base import BaseDocument
-from db.sql.meal_models import MealPlanModel
-from db.sql.recipe_models import Category, RecipeModel, Tag
-from db.sql.settings_models import SiteSettingsModel
-from db.sql.theme_models import SiteThemeModel
+from db.models.mealplan import MealPlanModel
+from db.models.recipe import Category, RecipeModel, Tag
+from db.models.settings import SiteSettingsModel
+from db.models.theme import SiteThemeModel
+from db.models.users import User
"""
# TODO
@@ -55,6 +56,12 @@ class _Themes(BaseDocument):
self.sql_model = SiteThemeModel
+class _Users(BaseDocument):
+ def __init__(self) -> None:
+ self.primary_key = "id"
+ self.sql_model = User
+
+
class Database:
def __init__(self) -> None:
self.recipes = _Recipes()
@@ -63,6 +70,7 @@ class Database:
self.themes = _Themes()
self.categories = _Categories()
self.tags = _Tags()
+ self.users = _Users()
db = Database()
diff --git a/mealie/db/db_base.py b/mealie/db/db_base.py
index 40d9144de..6e1204c5c 100644
--- a/mealie/db/db_base.py
+++ b/mealie/db/db_base.py
@@ -3,7 +3,7 @@ from typing import List
from sqlalchemy.orm import load_only
from sqlalchemy.orm.session import Session
-from db.sql.model_base import SqlAlchemyBase
+from db.models.model_base import SqlAlchemyBase
class BaseDocument:
@@ -108,7 +108,10 @@ class BaseDocument:
db_entries = [x.dict() for x in result]
if limit == 1:
- return db_entries[0]
+ try:
+ return db_entries[0]
+ except IndexError:
+ return None
return db_entries
@@ -124,9 +127,8 @@ class BaseDocument:
"""
new_document = self.sql_model(session=session, **document)
session.add(new_document)
- return_data = new_document.dict()
session.commit()
-
+ return_data = new_document.dict()
return return_data
def update(self, session: Session, match_value: str, new_data: str) -> dict:
diff --git a/mealie/db/db_setup.py b/mealie/db/db_setup.py
index aa315a012..e8084321a 100644
--- a/mealie/db/db_setup.py
+++ b/mealie/db/db_setup.py
@@ -1,7 +1,7 @@
-from app_config import SQLITE_FILE, USE_SQL
+from core.config import SQLITE_FILE, USE_SQL
from sqlalchemy.orm.session import Session
-from db.sql.db_session import sql_global_init
+from db.models.db_session import sql_global_init
sql_exists = True
diff --git a/mealie/db/init_db.py b/mealie/db/init_db.py
new file mode 100644
index 000000000..cf44ca5bc
--- /dev/null
+++ b/mealie/db/init_db.py
@@ -0,0 +1,63 @@
+from core.security import get_password_hash
+from fastapi.logger import logger
+from schema.settings import SiteSettings, Webhooks
+from sqlalchemy.orm import Session
+from sqlalchemy.orm.session import Session
+
+from db.database import db
+from db.db_setup import create_session
+
+
+def init_db(db: Session = None) -> None:
+ if not db:
+ db = create_session()
+
+ default_settings_init(db)
+ default_theme_init(db)
+ default_user_init(db)
+
+ db.close()
+
+
+def default_theme_init(session: Session):
+ default_theme = {
+ "name": "default",
+ "colors": {
+ "primary": "#E58325",
+ "accent": "#00457A",
+ "secondary": "#973542",
+ "success": "#5AB1BB",
+ "info": "#4990BA",
+ "warning": "#FF4081",
+ "error": "#EF5350",
+ },
+ }
+
+ try:
+ db.themes.create(session, default_theme)
+ logger.info("Generating default theme...")
+ except:
+ logger.info("Default Theme Exists.. skipping generation")
+
+
+def default_settings_init(session: Session):
+ try:
+ webhooks = Webhooks()
+ default_entry = SiteSettings(name="main", webhooks=webhooks)
+ document = db.settings.create(session, default_entry.dict())
+ logger.info(f"Created Site Settings: \n {document}")
+ except:
+ pass
+
+
+def default_user_init(session: Session):
+ default_user = {
+ "full_name": "Change Me",
+ "email": "changeme@email.com",
+ "password": get_password_hash("MyPassword"),
+ "family": "public",
+ "admin": True,
+ }
+
+ logger.info("Generating Default User")
+ db.users.create(session, default_user)
diff --git a/mealie/db/models/_all_models.py b/mealie/db/models/_all_models.py
new file mode 100644
index 000000000..e0deb2401
--- /dev/null
+++ b/mealie/db/models/_all_models.py
@@ -0,0 +1,5 @@
+from db.models.mealplan import *
+from db.models.recipe import *
+from db.models.settings import *
+from db.models.theme import *
+from db.models.users import *
diff --git a/mealie/db/sql/db_session.py b/mealie/db/models/db_session.py
similarity index 88%
rename from mealie/db/sql/db_session.py
rename to mealie/db/models/db_session.py
index 8376cc593..925ed27ef 100644
--- a/mealie/db/sql/db_session.py
+++ b/mealie/db/models/db_session.py
@@ -1,7 +1,7 @@
from pathlib import Path
import sqlalchemy as sa
-from db.sql.model_base import SqlAlchemyBase
+from db.models.model_base import SqlAlchemyBase
from sqlalchemy.orm import sessionmaker
@@ -18,7 +18,7 @@ def sql_global_init(db_file: Path, check_thread=False):
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
- import db.sql._all_models
+ import db.models._all_models
SqlAlchemyBase.metadata.create_all(engine)
diff --git a/mealie/db/sql/meal_models.py b/mealie/db/models/mealplan.py
similarity index 97%
rename from mealie/db/sql/meal_models.py
rename to mealie/db/models/mealplan.py
index 9001ded78..973227bd3 100644
--- a/mealie/db/sql/meal_models.py
+++ b/mealie/db/models/mealplan.py
@@ -3,7 +3,7 @@ from typing import List
import sqlalchemy as sa
import sqlalchemy.orm as orm
-from db.sql.model_base import BaseMixins, SqlAlchemyBase
+from db.models.model_base import BaseMixins, SqlAlchemyBase
class Meal(SqlAlchemyBase):
diff --git a/mealie/db/sql/model_base.py b/mealie/db/models/model_base.py
similarity index 100%
rename from mealie/db/sql/model_base.py
rename to mealie/db/models/model_base.py
diff --git a/mealie/db/sql/recipe_models.py b/mealie/db/models/recipe.py
similarity index 99%
rename from mealie/db/sql/recipe_models.py
rename to mealie/db/models/recipe.py
index cc877e767..a2d341c3b 100644
--- a/mealie/db/sql/recipe_models.py
+++ b/mealie/db/models/recipe.py
@@ -4,11 +4,11 @@ from typing import List
import sqlalchemy as sa
import sqlalchemy.orm as orm
-from db.sql.model_base import BaseMixins, SqlAlchemyBase
+from db.models.model_base import BaseMixins, SqlAlchemyBase
from slugify import slugify
from sqlalchemy.ext.orderinglist import ordering_list
from sqlalchemy.orm import validates
-from utils.logger import logger
+from fastapi.logger import logger
class ApiExtras(SqlAlchemyBase):
diff --git a/mealie/db/sql/settings_models.py b/mealie/db/models/settings.py
similarity index 96%
rename from mealie/db/sql/settings_models.py
rename to mealie/db/models/settings.py
index 0eaa67f20..1ba470bd5 100644
--- a/mealie/db/sql/settings_models.py
+++ b/mealie/db/models/settings.py
@@ -1,7 +1,6 @@
import sqlalchemy as sa
import sqlalchemy.orm as orm
-from db.sql.model_base import BaseMixins, SqlAlchemyBase
-from db.sql.recipe_models import Category
+from db.models.model_base import BaseMixins, SqlAlchemyBase
class SiteSettingsModel(SqlAlchemyBase, BaseMixins):
diff --git a/mealie/db/sql/theme_models.py b/mealie/db/models/theme.py
similarity index 96%
rename from mealie/db/sql/theme_models.py
rename to mealie/db/models/theme.py
index c6dca8019..f778545ce 100644
--- a/mealie/db/sql/theme_models.py
+++ b/mealie/db/models/theme.py
@@ -1,6 +1,6 @@
import sqlalchemy as sa
import sqlalchemy.orm as orm
-from db.sql.model_base import BaseMixins, SqlAlchemyBase
+from db.models.model_base import BaseMixins, SqlAlchemyBase
class SiteThemeModel(SqlAlchemyBase):
diff --git a/mealie/db/models/users.py b/mealie/db/models/users.py
new file mode 100644
index 000000000..ae03d15b9
--- /dev/null
+++ b/mealie/db/models/users.py
@@ -0,0 +1,44 @@
+from db.models.model_base import BaseMixins, SqlAlchemyBase
+from sqlalchemy import Boolean, Column, Integer, String
+
+
+class User(SqlAlchemyBase, BaseMixins):
+ __tablename__ = "users"
+ id = Column(Integer, primary_key=True)
+ full_name = Column(String, index=True)
+ email = Column(String, unique=True, index=True)
+ password = Column(String)
+ is_active = Column(Boolean(), default=True)
+ family = Column(String)
+ admin = Column(Boolean(), default=False)
+
+ def __init__(
+ self,
+ session,
+ full_name,
+ email,
+ password,
+ family="public",
+ admin=False,
+ ) -> None:
+ self.full_name = full_name
+ self.email = email
+ self.family = family
+ self.admin = admin
+ self.password = password
+
+ def dict(self):
+ return {
+ "id": self.id,
+ "full_name": self.full_name,
+ "email": self.email,
+ "admin": self.admin,
+ "family": self.family,
+ "password": self.password,
+ }
+
+ def update(self, full_name, email, family, admin, session=None):
+ self.full_name = full_name
+ self.email = email
+ self.family = family
+ self.admin = admin
diff --git a/mealie/db/sql/_all_models.py b/mealie/db/sql/_all_models.py
deleted file mode 100644
index c7df3b3be..000000000
--- a/mealie/db/sql/_all_models.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from db.sql.meal_models import *
-from db.sql.recipe_models import *
-from db.sql.settings_models import *
-from db.sql.theme_models import *
diff --git a/mealie/routes/__init__.py b/mealie/routes/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/mealie/routes/backup_routes.py b/mealie/routes/backup_routes.py
index df64c38f7..94f1d93ec 100644
--- a/mealie/routes/backup_routes.py
+++ b/mealie/routes/backup_routes.py
@@ -1,15 +1,15 @@
import operator
import shutil
-from app_config import BACKUP_DIR, TEMPLATE_DIR
+from core.config import BACKUP_DIR, TEMPLATE_DIR
from db.db_setup import generate_session
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
-from models.backup_models import BackupJob, ImportJob, Imports, LocalBackup
+from schema.backup import BackupJob, ImportJob, Imports, LocalBackup
from services.backups.exports import backup_all
from services.backups.imports import ImportDatabase
from sqlalchemy.orm.session import Session
from starlette.responses import FileResponse
-from utils.snackbar import SnackResponse
+from schema.snackbar import SnackResponse
router = APIRouter(prefix="/api/backups", tags=["Backups"])
diff --git a/mealie/routes/debug_routes.py b/mealie/routes/debug_routes.py
index bf7829047..5a3fedd3c 100644
--- a/mealie/routes/debug_routes.py
+++ b/mealie/routes/debug_routes.py
@@ -1,9 +1,7 @@
import json
-from app_config import APP_VERSION, DEBUG_DIR
+from core.config import APP_VERSION, DEBUG_DIR, LOGGER_FILE
from fastapi import APIRouter
-from fastapi.responses import HTMLResponse
-from utils.logger import LOGGER_FILE
router = APIRouter(prefix="/api/debug", tags=["Debug"])
diff --git a/mealie/routes/deps.py b/mealie/routes/deps.py
new file mode 100644
index 000000000..422efc17a
--- /dev/null
+++ b/mealie/routes/deps.py
@@ -0,0 +1,23 @@
+from core.config import SECRET
+from db.database import db
+from db.db_setup import create_session
+from fastapi_login import LoginManager
+from sqlalchemy.orm.session import Session
+
+from schema.user import UserInDB
+
+manager = LoginManager(SECRET, "/api/auth/token")
+
+
+@manager.user_loader
+def query_user(user_email: str, session: Session = None) -> UserInDB:
+ """
+ Get a user from the db
+ :param user_id: E-Mail of the user
+ :return: None or the UserInDB object
+ """
+
+ session = session if session else create_session()
+ user = db.users.get(session, user_email, "email")
+ session.close()
+ return UserInDB(**user)
\ No newline at end of file
diff --git a/mealie/routes/meal_routes.py b/mealie/routes/meal_routes.py
index 69a716d83..52f71f9dc 100644
--- a/mealie/routes/meal_routes.py
+++ b/mealie/routes/meal_routes.py
@@ -5,7 +5,7 @@ from db.db_setup import generate_session
from fastapi import APIRouter, Depends, HTTPException
from services.meal_services import MealPlan
from sqlalchemy.orm.session import Session
-from utils.snackbar import SnackResponse
+from schema.snackbar import SnackResponse
router = APIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
diff --git a/mealie/routes/migration_routes.py b/mealie/routes/migration_routes.py
index 19e2c7a5d..cd2b0963f 100644
--- a/mealie/routes/migration_routes.py
+++ b/mealie/routes/migration_routes.py
@@ -2,14 +2,14 @@ import operator
import shutil
from typing import List
-from app_config import MIGRATION_DIR
+from core.config import MIGRATION_DIR
from db.db_setup import generate_session
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
-from models.migration_models import MigrationFile, Migrations
+from schema.migration import MigrationFile, Migrations
from services.migrations.chowdown import chowdown_migrate as chowdow_migrate
from services.migrations.nextcloud import migrate as nextcloud_migrate
from sqlalchemy.orm.session import Session
-from utils.snackbar import SnackResponse
+from schema.snackbar import SnackResponse
router = APIRouter(prefix="/api/migrations", tags=["Migration"])
diff --git a/mealie/routes/recipe/all_recipe_routes.py b/mealie/routes/recipe/all_recipe_routes.py
index b0106b660..5e3c7ab87 100644
--- a/mealie/routes/recipe/all_recipe_routes.py
+++ b/mealie/routes/recipe/all_recipe_routes.py
@@ -3,7 +3,7 @@ from typing import List, Optional
from db.database import db
from db.db_setup import generate_session
from fastapi import APIRouter, Depends, Query
-from models.recipe_models import AllRecipeRequest
+from schema.recipe import AllRecipeRequest
from slugify import slugify
from sqlalchemy.orm.session import Session
diff --git a/mealie/routes/recipe/category_routes.py b/mealie/routes/recipe/category_routes.py
index a96eae07d..bbf5b36de 100644
--- a/mealie/routes/recipe/category_routes.py
+++ b/mealie/routes/recipe/category_routes.py
@@ -1,11 +1,11 @@
from db.database import db
from db.db_setup import generate_session
from fastapi import APIRouter, Depends
-from models.category_models import RecipeCategoryResponse
+from schema.category import RecipeCategoryResponse
from sqlalchemy.orm.session import Session
-from utils.snackbar import SnackResponse
+from schema.snackbar import SnackResponse
-from utils.snackbar import SnackResponse
+from schema.snackbar import SnackResponse
router = APIRouter(
prefix="/api/categories",
@@ -27,7 +27,6 @@ def get_all_recipes_by_category(
return db.categories.get(session, category)
-
@router.delete("/{category}")
async def delete_recipe_category(
category: str, session: Session = Depends(generate_session)
diff --git a/mealie/routes/recipe/recipe_crud_routes.py b/mealie/routes/recipe/recipe_crud_routes.py
index 142c8d3a4..58ace63a8 100644
--- a/mealie/routes/recipe/recipe_crud_routes.py
+++ b/mealie/routes/recipe/recipe_crud_routes.py
@@ -2,12 +2,12 @@ from db.db_setup import generate_session
from fastapi import APIRouter, Depends, File, Form, HTTPException
from fastapi.logger import logger
from fastapi.responses import FileResponse
-from models.recipe_models import RecipeURLIn
+from schema.recipe import RecipeURLIn
from services.image_services import read_image, write_image
from services.recipe_services import Recipe
from services.scraper.scraper import create_from_url
from sqlalchemy.orm.session import Session
-from utils.snackbar import SnackResponse
+from schema.snackbar import SnackResponse
router = APIRouter(
prefix="/api/recipes",
diff --git a/mealie/routes/recipe/tag_routes.py b/mealie/routes/recipe/tag_routes.py
index 193046031..f59545e8c 100644
--- a/mealie/routes/recipe/tag_routes.py
+++ b/mealie/routes/recipe/tag_routes.py
@@ -2,9 +2,9 @@ from db.database import db
from db.db_setup import generate_session
from fastapi import APIRouter, Depends
from sqlalchemy.orm.session import Session
-from utils.snackbar import SnackResponse
+from schema.snackbar import SnackResponse
-from utils.snackbar import SnackResponse
+from schema.snackbar import SnackResponse
router = APIRouter(tags=["Recipes"])
diff --git a/mealie/routes/setting_routes.py b/mealie/routes/setting_routes.py
index a0f6b01cd..b59cd6677 100644
--- a/mealie/routes/setting_routes.py
+++ b/mealie/routes/setting_routes.py
@@ -1,11 +1,10 @@
from db.database import db
from db.db_setup import generate_session
from fastapi import APIRouter, Depends
-from models.settings_models import SiteSettings
-from services.settings_services import default_settings_init
+from schema.settings import SiteSettings
from sqlalchemy.orm.session import Session
from utils.post_webhooks import post_webhooks
-from utils.snackbar import SnackResponse
+from schema.snackbar import SnackResponse
router = APIRouter(prefix="/api/site-settings", tags=["Settings"])
@@ -17,8 +16,7 @@ def get_main_settings(session: Session = Depends(generate_session)):
try:
data = db.settings.get(session, "main")
except:
- default_settings_init(session)
- data = db.settings.get(session, "main")
+ return
return data
diff --git a/mealie/routes/theme_routes.py b/mealie/routes/theme_routes.py
index 1d9646dd4..fa6a26361 100644
--- a/mealie/routes/theme_routes.py
+++ b/mealie/routes/theme_routes.py
@@ -1,8 +1,8 @@
from db.db_setup import generate_session
from fastapi import APIRouter, Depends
-from models.theme_models import SiteTheme
+from schema.theme import SiteTheme
from sqlalchemy.orm.session import Session
-from utils.snackbar import SnackResponse
+from schema.snackbar import SnackResponse
from db.database import db
router = APIRouter(prefix="/api", tags=["Themes"])
diff --git a/mealie/routes/users/__init__.py b/mealie/routes/users/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/mealie/routes/users/auth.py b/mealie/routes/users/auth.py
new file mode 100644
index 000000000..368f37036
--- /dev/null
+++ b/mealie/routes/users/auth.py
@@ -0,0 +1,32 @@
+from datetime import timedelta
+
+from core.security import verify_password
+from db.db_setup import generate_session
+from fastapi import APIRouter, Depends
+from fastapi.security import OAuth2PasswordRequestForm
+from fastapi_login.exceptions import InvalidCredentialsException
+from routes.deps import manager, query_user
+from schema.user import UserInDB
+from sqlalchemy.orm.session import Session
+
+router = APIRouter(prefix="/api/auth", tags=["Auth"])
+
+
+@router.post("/token")
+def token(
+ data: OAuth2PasswordRequestForm = Depends(),
+ session: Session = Depends(generate_session),
+):
+ email = data.username
+ password = data.password
+
+ user: UserInDB = query_user(email, session)
+ if not user:
+ raise InvalidCredentialsException # you can also use your own HTTPException
+ elif not verify_password(password, user.password):
+ raise InvalidCredentialsException
+
+ access_token = manager.create_access_token(
+ data=dict(sub=email), expires=timedelta(hours=2)
+ )
+ return {"access_token": access_token, "token_type": "bearer"}
diff --git a/mealie/routes/users/crud.py b/mealie/routes/users/crud.py
new file mode 100644
index 000000000..5eec6d796
--- /dev/null
+++ b/mealie/routes/users/crud.py
@@ -0,0 +1,85 @@
+from datetime import timedelta
+
+from core.security import get_password_hash
+from db.database import db
+from db.db_setup import generate_session
+from fastapi import APIRouter, Depends
+from routes.deps import manager, query_user
+from schema.user import UserBase, UserIn, UserInDB, UserOut
+from sqlalchemy.orm.session import Session
+
+router = APIRouter(prefix="/api/users", tags=["Users"])
+
+
+@router.post("", response_model=UserOut, status_code=201)
+async def create_user(
+ new_user: UserIn,
+ current_user=Depends(manager),
+ session: Session = Depends(generate_session),
+):
+ """ Returns a list of all user in the Database """
+
+ new_user.password = get_password_hash(new_user.password)
+
+ data = db.users.create(session, new_user.dict())
+ return data
+
+
+@router.get("", response_model=list[UserOut])
+async def get_all_users(
+ current_user: UserInDB = Depends(manager),
+ session: Session = Depends(generate_session),
+):
+
+ if current_user.admin:
+ return db.users.get_all(session)
+ else:
+ return {"details": "user not authorized"}
+
+
+@router.get("/self", response_model=UserOut)
+async def get_user_by_id(
+ current_user: UserInDB = Depends(manager),
+ session: Session = Depends(generate_session),
+):
+ return current_user.dict()
+
+
+@router.get("/{id}", response_model=UserOut)
+async def get_user_by_id(
+ id: int,
+ current_user: UserInDB = Depends(manager),
+ session: Session = Depends(generate_session),
+):
+ return db.users.get(session, id)
+
+
+@router.put("/{id}")
+async def update_user(
+ id: int,
+ new_data: UserBase,
+ current_user: UserInDB = Depends(manager),
+ session: Session = Depends(generate_session),
+):
+
+ if current_user.id == id or current_user.admin:
+ updated_user = db.users.update(session, id, new_data.dict())
+ email = updated_user.get("email")
+ if current_user.id == id:
+ access_token = manager.create_access_token(
+ data=dict(sub=email), expires=timedelta(hours=2)
+ )
+ return {"access_token": access_token, "token_type": "bearer"}
+ return
+
+
+@router.delete("/{id}")
+async def delete_user(
+ id: int,
+ current_user: UserInDB = Depends(manager),
+ session: Session = Depends(generate_session),
+):
+ """ Removes a user from the database. Must be the current user or a super user"""
+
+ if current_user.id == id or current_user.admin:
+ return db.users.delete(session, id)
diff --git a/mealie/routes/users/users.py b/mealie/routes/users/users.py
new file mode 100644
index 000000000..309f1d9d3
--- /dev/null
+++ b/mealie/routes/users/users.py
@@ -0,0 +1,7 @@
+from fastapi import APIRouter
+from routes.users import auth, crud
+
+router = APIRouter()
+
+router.include_router(auth.router)
+router.include_router(crud.router)
diff --git a/mealie/models/backup_models.py b/mealie/schema/backup.py
similarity index 100%
rename from mealie/models/backup_models.py
rename to mealie/schema/backup.py
diff --git a/mealie/models/category_models.py b/mealie/schema/category.py
similarity index 100%
rename from mealie/models/category_models.py
rename to mealie/schema/category.py
diff --git a/mealie/models/meal_models.py b/mealie/schema/meal.py
similarity index 100%
rename from mealie/models/meal_models.py
rename to mealie/schema/meal.py
diff --git a/mealie/models/migration_models.py b/mealie/schema/migration.py
similarity index 100%
rename from mealie/models/migration_models.py
rename to mealie/schema/migration.py
diff --git a/mealie/models/recipe_models.py b/mealie/schema/recipe.py
similarity index 100%
rename from mealie/models/recipe_models.py
rename to mealie/schema/recipe.py
diff --git a/mealie/models/import_models.py b/mealie/schema/restore.py
similarity index 100%
rename from mealie/models/import_models.py
rename to mealie/schema/restore.py
diff --git a/mealie/models/settings_models.py b/mealie/schema/settings.py
similarity index 100%
rename from mealie/models/settings_models.py
rename to mealie/schema/settings.py
diff --git a/mealie/utils/snackbar.py b/mealie/schema/snackbar.py
similarity index 100%
rename from mealie/utils/snackbar.py
rename to mealie/schema/snackbar.py
diff --git a/mealie/models/theme_models.py b/mealie/schema/theme.py
similarity index 100%
rename from mealie/models/theme_models.py
rename to mealie/schema/theme.py
diff --git a/mealie/schema/user.py b/mealie/schema/user.py
new file mode 100644
index 000000000..e80e9e4ad
--- /dev/null
+++ b/mealie/schema/user.py
@@ -0,0 +1,32 @@
+from typing import Optional
+
+from fastapi_camelcase import CamelModel
+
+# from pydantic import EmailStr
+
+
+class UserBase(CamelModel):
+ full_name: Optional[str] = None
+ email: str
+ family: str
+ admin: bool
+
+ class Config:
+ schema_extra = {
+ "fullName": "Change Me",
+ "email": "changeme@email.com",
+ "family": "public",
+ "admin": "false",
+ }
+
+
+class UserIn(UserBase):
+ password: str
+
+
+class UserOut(UserBase):
+ id: int
+
+
+class UserInDB(UserIn, UserOut):
+ pass
diff --git a/mealie/services/backups/exports.py b/mealie/services/backups/exports.py
index 8c53e3d70..9076ebced 100644
--- a/mealie/services/backups/exports.py
+++ b/mealie/services/backups/exports.py
@@ -3,13 +3,13 @@ import shutil
from datetime import datetime
from pathlib import Path
-from app_config import BACKUP_DIR, IMG_DIR, TEMP_DIR, TEMPLATE_DIR
+from core.config import BACKUP_DIR, IMG_DIR, TEMP_DIR, TEMPLATE_DIR
from db.database import db
from db.db_setup import create_session
from jinja2 import Template
from services.meal_services import MealPlan
from services.recipe_services import Recipe
-from utils.logger import logger
+from fastapi.logger import logger
class ExportDatabase:
diff --git a/mealie/services/backups/imports.py b/mealie/services/backups/imports.py
index 267cba715..2f0944179 100644
--- a/mealie/services/backups/imports.py
+++ b/mealie/services/backups/imports.py
@@ -4,13 +4,13 @@ import zipfile
from pathlib import Path
from typing import List
-from app_config import BACKUP_DIR, IMG_DIR, TEMP_DIR
+from core.config import BACKUP_DIR, IMG_DIR, TEMP_DIR
from db.database import db
-from models.import_models import RecipeImport, SettingsImport, ThemeImport
-from models.theme_models import SiteTheme
+from schema.restore import RecipeImport, SettingsImport, ThemeImport
+from schema.theme import SiteTheme
from services.recipe_services import Recipe
from sqlalchemy.orm.session import Session
-from utils.logger import logger
+from fastapi.logger import logger
class ImportDatabase:
diff --git a/mealie/services/image_services.py b/mealie/services/image_services.py
index 4e6d542e4..bd73cf301 100644
--- a/mealie/services/image_services.py
+++ b/mealie/services/image_services.py
@@ -2,8 +2,8 @@ import shutil
from pathlib import Path
import requests
-from app_config import IMG_DIR
-from utils.logger import logger
+from core.config import IMG_DIR
+from fastapi.logger import logger
def read_image(recipe_slug: str) -> Path:
diff --git a/mealie/services/migrations/chowdown.py b/mealie/services/migrations/chowdown.py
index a26215ab9..85bad0554 100644
--- a/mealie/services/migrations/chowdown.py
+++ b/mealie/services/migrations/chowdown.py
@@ -2,7 +2,7 @@ import shutil
from pathlib import Path
import yaml
-from app_config import IMG_DIR, TEMP_DIR
+from core.config import IMG_DIR, TEMP_DIR
from services.recipe_services import Recipe
from sqlalchemy.orm.session import Session
from utils.unzip import unpack_zip
diff --git a/mealie/services/migrations/nextcloud.py b/mealie/services/migrations/nextcloud.py
index 155d1d120..fe8547a52 100644
--- a/mealie/services/migrations/nextcloud.py
+++ b/mealie/services/migrations/nextcloud.py
@@ -4,10 +4,10 @@ import shutil
import zipfile
from pathlib import Path
-from app_config import IMG_DIR, MIGRATION_DIR, TEMP_DIR
+from core.config import IMG_DIR, MIGRATION_DIR, TEMP_DIR
from services.recipe_services import Recipe
from services.scraper.cleaner import Cleaner
-from app_config import IMG_DIR, TEMP_DIR
+from core.config import IMG_DIR, TEMP_DIR
def process_selection(selection: Path) -> Path:
@@ -81,7 +81,7 @@ def migrate(session, selection: str):
successful_imports.append(recipe.name)
except:
logging.error(f"Failed Nextcloud Import: {dir.name}")
- logging.exception('')
+ logging.exception("")
failed_imports.append(dir.name)
cleanup()
diff --git a/mealie/services/scheduler/scheduled_jobs.py b/mealie/services/scheduler/scheduled_jobs.py
index ea93fb202..37e4efeb9 100644
--- a/mealie/services/scheduler/scheduled_jobs.py
+++ b/mealie/services/scheduler/scheduled_jobs.py
@@ -3,8 +3,8 @@ from db.db_setup import create_session
from services.backups.exports import auto_backup_job
from services.scheduler.global_scheduler import scheduler
from services.scheduler.scheduler_utils import Cron, cron_parser
-from utils.logger import logger
-from models.settings_models import SiteSettings
+from fastapi.logger import logger
+from schema.settings import SiteSettings
from db.database import db
from utils.post_webhooks import post_webhooks
diff --git a/mealie/services/scraper/open_graph.py b/mealie/services/scraper/open_graph.py
index 49d1072ef..3b359bcc2 100644
--- a/mealie/services/scraper/open_graph.py
+++ b/mealie/services/scraper/open_graph.py
@@ -1,7 +1,7 @@
from typing import Tuple
import extruct
-from app_config import DEBUG_DIR
+from core.config import DEBUG_DIR
from slugify import slugify
from w3lib.html import get_base_url
diff --git a/mealie/services/scraper/scraper.py b/mealie/services/scraper/scraper.py
index a13ed79e2..516773403 100644
--- a/mealie/services/scraper/scraper.py
+++ b/mealie/services/scraper/scraper.py
@@ -3,12 +3,12 @@ from typing import List
import requests
import scrape_schema_recipe
-from app_config import DEBUG_DIR
+from core.config import DEBUG_DIR
+from fastapi.logger import logger
from services.image_services import scrape_image
from services.recipe_services import Recipe
-from services.scraper import open_graph
+from services.scraper import open_graph
from services.scraper.cleaner import Cleaner
-from utils.logger import logger
LAST_JSON = DEBUG_DIR.joinpath("last_recipe.json")
@@ -25,7 +25,7 @@ def create_from_url(url: str) -> Recipe:
"""
r = requests.get(url)
new_recipe = extract_recipe_from_html(r.text, url)
- new_recipe = Cleaner.clean(new_recipe)
+ new_recipe = Cleaner.clean(new_recipe, url)
new_recipe = download_image_for_recipe(new_recipe)
recipe = Recipe(**new_recipe)
diff --git a/mealie/services/settings_services.py b/mealie/services/settings_services.py
deleted file mode 100644
index 62e32bb3d..000000000
--- a/mealie/services/settings_services.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from db.database import db
-from db.db_setup import create_session
-from models.settings_models import SiteSettings, Webhooks
-from sqlalchemy.orm.session import Session
-from utils.logger import logger
-
-
-def default_settings_init(session: Session = None):
- if session == None:
- session = create_session()
- try:
- webhooks = Webhooks()
- default_entry = SiteSettings(name="main", webhooks=webhooks)
- document = db.settings.create(session, default_entry.dict())
- logger.info(f"Created Site Settings: \n {document}")
- except:
- pass
diff --git a/mealie/services/theme_services.py b/mealie/services/theme_services.py
deleted file mode 100644
index 12cd9c889..000000000
--- a/mealie/services/theme_services.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from db.database import db
-from db.db_setup import create_session, sql_exists
-from utils.logger import logger
-
-
-def default_theme_init():
- default_theme = {
- "name": "default",
- "colors": {
- "primary": "#E58325",
- "accent": "#00457A",
- "secondary": "#973542",
- "success": "#5AB1BB",
- "info": "#4990BA",
- "warning": "#FF4081",
- "error": "#EF5350",
- },
- }
- session = create_session()
- try:
- db.themes.create(session, default_theme)
- logger.info("Generating default theme...")
- except:
- logger.info("Default Theme Exists.. skipping generation")
-
-
-if not sql_exists:
- default_theme_init()
diff --git a/mealie/services/users/__init__.py b/mealie/services/users/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/mealie/tests/conftest.py b/mealie/tests/conftest.py
index 4a3c1c763..e73807528 100644
--- a/mealie/tests/conftest.py
+++ b/mealie/tests/conftest.py
@@ -1,27 +1,27 @@
-from pathlib import Path
+import json
+import requests
from app import app
-from app_config import SQLITE_DIR
+from core.config import SQLITE_DIR
from db.db_setup import generate_session, sql_global_init
+from db.init_db import init_db
from fastapi.testclient import TestClient
from pytest import fixture
-from services.settings_services import default_settings_init
-from services.theme_services import default_theme_init
from tests.test_config import TEST_DATA
SQLITE_FILE = SQLITE_DIR.joinpath("test.db")
SQLITE_FILE.unlink(missing_ok=True)
+TOKEN_URL = "/api/auth/token"
TestSessionLocal = sql_global_init(SQLITE_FILE, check_thread=False)
+init_db(TestSessionLocal())
def override_get_db():
try:
db = TestSessionLocal()
- default_theme_init()
- default_settings_init()
yield db
finally:
db.close()
@@ -31,11 +31,22 @@ def override_get_db():
def api_client():
app.dependency_overrides[generate_session] = override_get_db
+
yield TestClient(app)
- SQLITE_FILE.unlink()
+ # SQLITE_FILE.unlink()
@fixture(scope="session")
def test_image():
return TEST_DATA.joinpath("test_image.jpg")
+
+
+@fixture(scope="session")
+def token(api_client: requests):
+ form_data = {"username": "changeme@email.com", "password": "MyPassword"}
+ response = api_client.post(TOKEN_URL, form_data)
+
+ token = json.loads(response.text).get("access_token")
+
+ return {"Authorization": f"Bearer {token}"}
diff --git a/mealie/tests/test_migrations/test_nextcloud.py b/mealie/tests/test_migrations/test_nextcloud.py
index 52a251411..b6d363358 100644
--- a/mealie/tests/test_migrations/test_nextcloud.py
+++ b/mealie/tests/test_migrations/test_nextcloud.py
@@ -1,10 +1,14 @@
from pathlib import Path
-from app_config import TEMP_DIR
+from core.config import TEMP_DIR
import pytest
-from app_config import TEMP_DIR
+from core.config import TEMP_DIR
from services.image_services import IMG_DIR
-from services.migrations.nextcloud import (cleanup, import_recipes, prep,
- process_selection)
+from services.migrations.nextcloud import (
+ cleanup,
+ import_recipes,
+ prep,
+ process_selection,
+)
from services.recipe_services import Recipe
from tests.test_config import TEST_NEXTCLOUD_DIR
diff --git a/mealie/tests/test_routes/test_migration_routes.py b/mealie/tests/test_routes/test_migration_routes.py
index 2bb35ec93..4efa280b7 100644
--- a/mealie/tests/test_routes/test_migration_routes.py
+++ b/mealie/tests/test_routes/test_migration_routes.py
@@ -2,7 +2,7 @@ import json
import shutil
import pytest
-from app_config import MIGRATION_DIR
+from core.config import MIGRATION_DIR
from tests.test_config import TEST_CHOWDOWN_DIR, TEST_NEXTCLOUD_DIR
from tests.utils.routes import MIGRATIONS_PREFIX, RECIPES_PREFIX
diff --git a/mealie/tests/test_routes/test_settings_routes.py b/mealie/tests/test_routes/test_settings_routes.py
index 56ccae369..fe4bbdf65 100644
--- a/mealie/tests/test_routes/test_settings_routes.py
+++ b/mealie/tests/test_routes/test_settings_routes.py
@@ -34,8 +34,6 @@ def default_theme(api_client):
},
}
- api_client.post(THEMES_CREATE, json=default_theme)
-
return default_theme
diff --git a/mealie/tests/test_routes/test_user_routes.py b/mealie/tests/test_routes/test_user_routes.py
new file mode 100644
index 000000000..02a542d0e
--- /dev/null
+++ b/mealie/tests/test_routes/test_user_routes.py
@@ -0,0 +1,93 @@
+import json
+
+import requests
+from pytest import fixture
+
+BASE = "/api/users"
+TOKEN_URL = "/api/auth/token"
+
+
+
+
+
+@fixture(scope="session")
+def default_user():
+ return {
+ "id": 1,
+ "fullName": "Change Me",
+ "email": "changeme@email.com",
+ "family": "public",
+ "admin": True
+ }
+
+
+@fixture(scope="session")
+def new_user():
+ return {
+ "id": 2,
+ "fullName": "My New User",
+ "email": "newuser@email.com",
+ "family": "public",
+ "admin": False
+ }
+
+
+def test_superuser_login(api_client: requests):
+ form_data = {"username": "changeme@email.com", "password": "MyPassword"}
+ response = api_client.post(TOKEN_URL, form_data)
+
+ assert response.status_code == 200
+ token = json.loads(response.text).get("access_token")
+
+ return {"Authorization": f"Bearer {token}"}
+
+
+def test_init_superuser(api_client: requests, token, default_user):
+ response = api_client.get(f"{BASE}/1", headers=token)
+ assert response.status_code == 200
+
+ assert json.loads(response.text) == default_user
+
+
+def test_create_user(api_client: requests, token, new_user):
+ create_data = {
+ "fullName": "My New User",
+ "email": "newuser@email.com",
+ "password": "MyStrongPassword",
+ "family": "public",
+ "admin": False
+ }
+
+ response = api_client.post(f"{BASE}", json=create_data, headers=token)
+
+ assert response.status_code == 201
+ assert json.loads(response.text) == new_user
+ assert True
+
+
+def test_get_all_users(api_client: requests, token, new_user, default_user):
+ response = api_client.get(f"{BASE}", headers=token)
+
+ assert response.status_code == 200
+
+ assert json.loads(response.text) == [default_user, new_user]
+
+
+def test_update_user(api_client: requests, token):
+ update_data = {
+ "id": 1,
+ "fullName": "Updated Name",
+ "email": "updated@email.com",
+ "family": "public",
+ "admin": True
+ }
+ response = api_client.put(f"{BASE}/1", headers=token, json=update_data)
+
+ assert response.status_code == 200
+ assert json.loads(response.text).get("access_token")
+
+
+def test_delete_user(api_client: requests, token):
+ response = api_client.delete(f"{BASE}/2", headers=token)
+
+ assert response.status_code == 200
diff --git a/mealie/tests/test_services/test_migrations/test_nextcloud.py b/mealie/tests/test_services/test_migrations/test_nextcloud.py
index 52a251411..b6d363358 100644
--- a/mealie/tests/test_services/test_migrations/test_nextcloud.py
+++ b/mealie/tests/test_services/test_migrations/test_nextcloud.py
@@ -1,10 +1,14 @@
from pathlib import Path
-from app_config import TEMP_DIR
+from core.config import TEMP_DIR
import pytest
-from app_config import TEMP_DIR
+from core.config import TEMP_DIR
from services.image_services import IMG_DIR
-from services.migrations.nextcloud import (cleanup, import_recipes, prep,
- process_selection)
+from services.migrations.nextcloud import (
+ cleanup,
+ import_recipes,
+ prep,
+ process_selection,
+)
from services.recipe_services import Recipe
from tests.test_config import TEST_NEXTCLOUD_DIR
diff --git a/mealie/utils/api_docs.py b/mealie/utils/api_docs.py
index 356789789..d0abc39aa 100644
--- a/mealie/utils/api_docs.py
+++ b/mealie/utils/api_docs.py
@@ -1,6 +1,6 @@
import json
-from app_config import DATA_DIR
+from core.config import DATA_DIR
"""Script to export the ReDoc documentation page into a standalone HTML file."""
@@ -37,6 +37,3 @@ HTML_PATH = DATA_DIR.parent.joinpath("docs/docs/html/api.html")
def generate_api_docs(app):
with open(HTML_PATH, "w") as fd:
print(HTML_TEMPLATE % json.dumps(app.openapi()), file=fd)
-
-
-
diff --git a/mealie/utils/global_scheduler.py b/mealie/utils/global_scheduler.py
deleted file mode 100644
index 77d574dd0..000000000
--- a/mealie/utils/global_scheduler.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from services.scheduler_services import Scheduler
-
-
-def start_scheduler():
- global scheduler
- scheduler = Scheduler()
- scheduler.startup_scheduler()
- return scheduler
-
-
-scheduler = start_scheduler()
diff --git a/mealie/utils/logger.py b/mealie/utils/logger.py
deleted file mode 100644
index 03fa87933..000000000
--- a/mealie/utils/logger.py
+++ /dev/null
@@ -1,25 +0,0 @@
-import logging
-from pathlib import Path
-
-from app_config import DATA_DIR
-
-LOGGER_LEVEL = "INFO"
-CWD = Path(__file__).parent
-LOGGER_FILE = DATA_DIR.joinpath("mealie.log")
-
-
-logging.basicConfig(
- level=LOGGER_LEVEL,
- format="%(asctime)s %(levelname)s: %(message)s",
- datefmt="%d-%b-%y %H:%M:%S",
-)
-
-logger = logging.getLogger(__name__)
-
-""" Logging Cheat Sheet
-logger.debug("this is a debugging message")
-logger.info("this is an informational message")
-logger.warning("this is a warning message")
-logger.error("this is an error message")
-logger.critical("this is a critical message")
-"""
diff --git a/mealie/utils/post_webhooks.py b/mealie/utils/post_webhooks.py
index 90520c51a..9bc475039 100644
--- a/mealie/utils/post_webhooks.py
+++ b/mealie/utils/post_webhooks.py
@@ -3,7 +3,7 @@ import json
import requests
from db.database import db
from db.db_setup import create_session
-from models.settings_models import SiteSettings
+from schema.settings import SiteSettings
from services.meal_services import MealPlan
from services.recipe_services import Recipe
diff --git a/mealie/utils/startup.py b/mealie/utils/startup.py
deleted file mode 100644
index b97811693..000000000
--- a/mealie/utils/startup.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from pathlib import Path
-
-from services.settings_services import default_theme_init
-
-CWD = Path(__file__).parent
-
-
-def post_start():
- default_theme_init()
-
-
-if __name__ == "__main__":
- pass
diff --git a/mealie/utils/unzip.py b/mealie/utils/unzip.py
index b8e53767b..930da7365 100644
--- a/mealie/utils/unzip.py
+++ b/mealie/utils/unzip.py
@@ -2,7 +2,7 @@ import tempfile
import zipfile
from pathlib import Path
-from app_config import TEMP_DIR
+from core.config import TEMP_DIR
def unpack_zip(selection: Path) -> tempfile.TemporaryDirectory:
diff --git a/poetry.lock b/poetry.lock
index 3b5e65cb4..0e8fef8cf 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -83,6 +83,22 @@ docs = ["furo", "sphinx", "zope.interface"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"]
+[[package]]
+name = "bcrypt"
+version = "3.2.0"
+description = "Modern password hashing for your software and your servers"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+cffi = ">=1.1"
+six = ">=1.4.1"
+
+[package.extras]
+tests = ["pytest (>=3.2.1,!=3.3.0)"]
+typecheck = ["mypy"]
+
[[package]]
name = "beautifulsoup4"
version = "4.9.3"
@@ -128,6 +144,17 @@ category = "main"
optional = false
python-versions = "*"
+[[package]]
+name = "cffi"
+version = "1.14.5"
+description = "Foreign Function Interface for Python calling C code."
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+pycparser = "*"
+
[[package]]
name = "chardet"
version = "4.0.0"
@@ -211,6 +238,31 @@ dev = ["python-jose[cryptography] (>=3.1.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<
doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=6.1.4,<7.0.0)", "markdown-include (>=0.5.1,<0.6.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.2.0)", "typer-cli (>=0.0.9,<0.0.10)", "pyyaml (>=5.3.1,<6.0.0)"]
test = ["pytest (==5.4.3)", "pytest-cov (==2.10.0)", "pytest-asyncio (>=0.14.0,<0.15.0)", "mypy (==0.790)", "flake8 (>=3.8.3,<4.0.0)", "black (==20.8b1)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.15.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.4.0)", "orjson (>=3.2.1,<4.0.0)", "async_exit_stack (>=1.0.1,<2.0.0)", "async_generator (>=1.10,<2.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "aiofiles (>=0.5.0,<0.6.0)", "flask (>=1.1.2,<2.0.0)"]
+[[package]]
+name = "fastapi-camelcase"
+version = "1.0.2"
+description = "Package provides an easy way to have camelcase request/response bodies for Pydantic"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+pydantic = "*"
+pyhumps = "*"
+
+[[package]]
+name = "fastapi-login"
+version = "1.5.3"
+description = ""
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+fastapi = "*"
+passlib = "*"
+pyjwt = "*"
+
[[package]]
name = "h11"
version = "0.12.0"
@@ -267,18 +319,6 @@ category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-[[package]]
-name = "importlib-resources"
-version = "5.1.0"
-description = "Read resources from Python packages"
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
-[package.extras]
-docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
-testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "pytest-black (>=0.3.7)", "pytest-mypy"]
-
[[package]]
name = "iniconfig"
version = "1.1.1"
@@ -403,6 +443,20 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies]
pyparsing = ">=2.0.2"
+[[package]]
+name = "passlib"
+version = "1.7.4"
+description = "comprehensive password hashing framework supporting over 30 schemes"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.extras]
+argon2 = ["argon2-cffi (>=18.2.0)"]
+bcrypt = ["bcrypt (>=3.1.0)"]
+build_docs = ["sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)", "cloud-sptheme (>=1.10.1)"]
+totp = ["cryptography"]
+
[[package]]
name = "pathspec"
version = "0.8.1"
@@ -430,6 +484,14 @@ category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+[[package]]
+name = "pycparser"
+version = "2.20"
+description = "C parser in Python"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
[[package]]
name = "pydantic"
version = "1.7.3"
@@ -443,6 +505,28 @@ dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"]
typing_extensions = ["typing-extensions (>=3.7.2)"]
+[[package]]
+name = "pyhumps"
+version = "1.6.1"
+description = "🐫 Convert strings (and dictionary keys) between snake case, camel case and pascal case in Python. Inspired by Humps for Node"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pyjwt"
+version = "2.0.1"
+description = "JSON Web Token implementation in Python"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+crypto = ["cryptography (>=3.3.1,<4.0.0)"]
+dev = ["sphinx", "sphinx-rtd-theme", "zope.interface", "cryptography (>=3.3.1,<4.0.0)", "pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)", "mypy", "pre-commit"]
+docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
+tests = ["pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)"]
+
[[package]]
name = "pylint"
version = "2.6.0"
@@ -632,7 +716,6 @@ python-versions = "*"
[package.dependencies]
extruct = "*"
-importlib-resources = {version = "*", markers = "python_version < \"3.9\""}
isodate = ">=0.5.1"
requests = "*"
validators = ">=0.12.4"
@@ -742,7 +825,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
name = "uvicorn"
-version = "0.13.3"
+version = "0.13.4"
description = "The lightning-fast ASGI server."
category = "main"
optional = false
@@ -755,12 +838,12 @@ h11 = ">=0.8"
httptools = {version = ">=0.1.0,<0.2.0", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""}
python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""}
PyYAML = {version = ">=5.1", optional = true, markers = "extra == \"standard\""}
-uvloop = {version = ">=0.14.0", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""}
-watchgod = {version = ">=0.6,<0.7", optional = true, markers = "extra == \"standard\""}
+uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""}
+watchgod = {version = ">=0.6", optional = true, markers = "extra == \"standard\""}
websockets = {version = ">=8.0.0,<9.0.0", optional = true, markers = "extra == \"standard\""}
[package.extras]
-standard = ["websockets (>=8.0.0,<9.0.0)", "watchgod (>=0.6,<0.7)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "httptools (>=0.1.0,<0.2.0)", "uvloop (>=0.14.0)", "colorama (>=0.4)"]
+standard = ["websockets (>=8.0.0,<9.0.0)", "watchgod (>=0.6)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "httptools (>=0.1.0,<0.2.0)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"]
[[package]]
name = "uvloop"
@@ -830,8 +913,8 @@ python-versions = "*"
[metadata]
lock-version = "1.1"
-python-versions = "^3.8"
-content-hash = "fbe2a3d2885fcc24abe5a285a961f968ea32aa22182f88f44a7c9dd624c968b5"
+python-versions = "^3.9"
+content-hash = "182496243ce59b60506b646a0a521d0186a7ef36009c5212276807e13e20d8a3"
[metadata.files]
aiofiles = [
@@ -862,6 +945,15 @@ attrs = [
{file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"},
{file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"},
]
+bcrypt = [
+ {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"},
+ {file = "bcrypt-3.2.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7"},
+ {file = "bcrypt-3.2.0-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1"},
+ {file = "bcrypt-3.2.0-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"},
+ {file = "bcrypt-3.2.0-cp36-abi3-win32.whl", hash = "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55"},
+ {file = "bcrypt-3.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34"},
+ {file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"},
+]
beautifulsoup4 = [
{file = "beautifulsoup4-4.9.3-py2-none-any.whl", hash = "sha256:4c98143716ef1cb40bf7f39a8e3eec8f8b009509e74904ba3a7b315431577e35"},
{file = "beautifulsoup4-4.9.3-py3-none-any.whl", hash = "sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666"},
@@ -874,6 +966,45 @@ certifi = [
{file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"},
{file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"},
]
+cffi = [
+ {file = "cffi-1.14.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991"},
+ {file = "cffi-1.14.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1"},
+ {file = "cffi-1.14.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa"},
+ {file = "cffi-1.14.5-cp27-cp27m-win32.whl", hash = "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3"},
+ {file = "cffi-1.14.5-cp27-cp27m-win_amd64.whl", hash = "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5"},
+ {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482"},
+ {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6"},
+ {file = "cffi-1.14.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045"},
+ {file = "cffi-1.14.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa"},
+ {file = "cffi-1.14.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406"},
+ {file = "cffi-1.14.5-cp35-cp35m-win32.whl", hash = "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369"},
+ {file = "cffi-1.14.5-cp35-cp35m-win_amd64.whl", hash = "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315"},
+ {file = "cffi-1.14.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892"},
+ {file = "cffi-1.14.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058"},
+ {file = "cffi-1.14.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5"},
+ {file = "cffi-1.14.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132"},
+ {file = "cffi-1.14.5-cp36-cp36m-win32.whl", hash = "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53"},
+ {file = "cffi-1.14.5-cp36-cp36m-win_amd64.whl", hash = "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813"},
+ {file = "cffi-1.14.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73"},
+ {file = "cffi-1.14.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06"},
+ {file = "cffi-1.14.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1"},
+ {file = "cffi-1.14.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49"},
+ {file = "cffi-1.14.5-cp37-cp37m-win32.whl", hash = "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62"},
+ {file = "cffi-1.14.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4"},
+ {file = "cffi-1.14.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053"},
+ {file = "cffi-1.14.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0"},
+ {file = "cffi-1.14.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e"},
+ {file = "cffi-1.14.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827"},
+ {file = "cffi-1.14.5-cp38-cp38-win32.whl", hash = "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e"},
+ {file = "cffi-1.14.5-cp38-cp38-win_amd64.whl", hash = "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396"},
+ {file = "cffi-1.14.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea"},
+ {file = "cffi-1.14.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322"},
+ {file = "cffi-1.14.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c"},
+ {file = "cffi-1.14.5-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee"},
+ {file = "cffi-1.14.5-cp39-cp39-win32.whl", hash = "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396"},
+ {file = "cffi-1.14.5-cp39-cp39-win_amd64.whl", hash = "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d"},
+ {file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"},
+]
chardet = [
{file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
{file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
@@ -949,6 +1080,13 @@ fastapi = [
{file = "fastapi-0.63.0-py3-none-any.whl", hash = "sha256:98d8ea9591d8512fdadf255d2a8fa56515cdd8624dca4af369da73727409508e"},
{file = "fastapi-0.63.0.tar.gz", hash = "sha256:63c4592f5ef3edf30afa9a44fa7c6b7ccb20e0d3f68cd9eba07b44d552058dcb"},
]
+fastapi-camelcase = [
+ {file = "fastapi_camelcase-1.0.2.tar.gz", hash = "sha256:1d852149f6c9e5bb8002839a1e024050af917f1944b9d108d56468d64c6da279"},
+]
+fastapi-login = [
+ {file = "fastapi-login-1.5.3.tar.gz", hash = "sha256:8e8ef710f1b7107e81d00e205779e73e17be35d5a91d11685ff72f323898e93b"},
+ {file = "fastapi_login-1.5.3-py3-none-any.whl", hash = "sha256:6c83b74bdb45c34ec0aab22000a7951df96c5d011f02a99a46ca4b2be6b1263c"},
+]
h11 = [
{file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"},
{file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"},
@@ -979,10 +1117,6 @@ idna = [
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
]
-importlib-resources = [
- {file = "importlib_resources-5.1.0-py3-none-any.whl", hash = "sha256:885b8eae589179f661c909d699a546cf10d83692553e34dca1bf5eb06f7f6217"},
- {file = "importlib_resources-5.1.0.tar.gz", hash = "sha256:bfdad047bce441405a49cf8eb48ddce5e56c696e185f59147a8b79e75e9e6380"},
-]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
@@ -1083,20 +1217,39 @@ markupsafe = [
{file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"},
+ {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"},
+ {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"},
+ {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"},
+ {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"},
+ {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"},
+ {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"},
+ {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"},
+ {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"},
{file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"},
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"},
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"},
+ {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"},
+ {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"},
+ {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"},
{file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"},
{file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"},
+ {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"},
+ {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"},
+ {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"},
+ {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"},
+ {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"},
+ {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"},
+ {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"},
+ {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"},
{file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"},
]
mccabe = [
@@ -1114,6 +1267,10 @@ packaging = [
{file = "packaging-20.8-py2.py3-none-any.whl", hash = "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858"},
{file = "packaging-20.8.tar.gz", hash = "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"},
]
+passlib = [
+ {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"},
+ {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"},
+]
pathspec = [
{file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"},
{file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},
@@ -1126,6 +1283,10 @@ py = [
{file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
{file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
]
+pycparser = [
+ {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
+ {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
+]
pydantic = [
{file = "pydantic-1.7.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c59ea046aea25be14dc22d69c97bee629e6d48d2b2ecb724d7fe8806bf5f61cd"},
{file = "pydantic-1.7.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a4143c8d0c456a093387b96e0f5ee941a950992904d88bc816b4f0e72c9a0009"},
@@ -1150,6 +1311,14 @@ pydantic = [
{file = "pydantic-1.7.3-py3-none-any.whl", hash = "sha256:38be427ea01a78206bcaf9a56f835784afcba9e5b88fbdce33bbbfbcd7841229"},
{file = "pydantic-1.7.3.tar.gz", hash = "sha256:213125b7e9e64713d16d988d10997dabc6a1f73f3991e1ff8e35ebb1409c7dc9"},
]
+pyhumps = [
+ {file = "pyhumps-1.6.1-py3-none-any.whl", hash = "sha256:58b367b73c57b64e32d211dc769addabd68ff6db07ce64b2e6565f7d5a12291f"},
+ {file = "pyhumps-1.6.1.tar.gz", hash = "sha256:01612603c5ad73a407299d806d30708a3935052276fdd93776953bccc0724e0a"},
+]
+pyjwt = [
+ {file = "PyJWT-2.0.1-py3-none-any.whl", hash = "sha256:b70b15f89dc69b993d8a8d32c299032d5355c82f9b5b7e851d1a6d706dffe847"},
+ {file = "PyJWT-2.0.1.tar.gz", hash = "sha256:a5c70a06e1f33d81ef25eecd50d50bd30e34de1ca8b2b9fa3fe0daaabcf69bf7"},
+]
pylint = [
{file = "pylint-2.6.0-py3-none-any.whl", hash = "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f"},
{file = "pylint-2.6.0.tar.gz", hash = "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210"},
@@ -1371,8 +1540,8 @@ urllib3 = [
{file = "urllib3-1.26.2.tar.gz", hash = "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08"},
]
uvicorn = [
- {file = "uvicorn-0.13.3-py3-none-any.whl", hash = "sha256:1079c50a06f6338095b4f203e7861dbff318dde5f22f3a324fc6e94c7654164c"},
- {file = "uvicorn-0.13.3.tar.gz", hash = "sha256:ef1e0bb5f7941c6fe324e06443ddac0331e1632a776175f87891c7bd02694355"},
+ {file = "uvicorn-0.13.4-py3-none-any.whl", hash = "sha256:7587f7b08bd1efd2b9bad809a3d333e972f1d11af8a5e52a9371ee3a5de71524"},
+ {file = "uvicorn-0.13.4.tar.gz", hash = "sha256:3292251b3c7978e8e4a7868f4baf7f7f7bb7e40c759ecc125c37e99cdea34202"},
]
uvloop = [
{file = "uvloop-0.14.0-cp35-cp35m-macosx_10_11_x86_64.whl", hash = "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd"},
diff --git a/pyproject.toml b/pyproject.toml
index 9be84a044..de3809ac0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -9,7 +9,7 @@ license = "MIT"
start = "app:app"
[tool.poetry.dependencies]
-python = "^3.8"
+python = "^3.9"
aiofiles = "0.5.0"
aniso8601 = "7.0.0"
appdirs = "1.4.4"
@@ -25,6 +25,9 @@ PyYAML = "^5.3.1"
extruct = "^0.12.0"
scrape-schema-recipe = "^0.1.3"
python-multipart = "^0.0.5"
+fastapi-login = "^1.5.3"
+bcrypt = "^3.2.0"
+fastapi-camelcase = "^1.0.2"
[tool.poetry.dev-dependencies]
pylint = "^2.6.0"