feature/profile-cards (#391)

* unify format

* pass variables

* remove namespace

* rename

* group-card init

* shuffle + icons

* remove console.logs

* token CRUD

* update changelog

* add profile link

* consolidate mealplan to profile dashboard

* update docs

* add query parameter to search page

* update test routes

* update python depts

* basic token tests

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden
2021-05-06 21:08:27 -08:00
committed by GitHub
parent f4384167f6
commit 95ec13161f
41 changed files with 977 additions and 449 deletions

View File

@@ -8,7 +8,7 @@ from mealie.db.models.recipe.recipe import Category, RecipeModel, Tag
from mealie.db.models.settings import CustomPage, SiteSettings
from mealie.db.models.sign_up import SignUp
from mealie.db.models.theme import SiteThemeModel
from mealie.db.models.users import User
from mealie.db.models.users import LongLiveToken, User
from mealie.schema.category import RecipeCategoryResponse, RecipeTagResponse
from mealie.schema.events import Event as EventSchema
from mealie.schema.meal import MealPlanInDB
@@ -17,7 +17,7 @@ from mealie.schema.settings import CustomPageOut
from mealie.schema.settings import SiteSettings as SiteSettingsSchema
from mealie.schema.sign_up import SignUpOut
from mealie.schema.theme import SiteTheme
from mealie.schema.user import GroupInDB, UserInDB
from mealie.schema.user import GroupInDB, LongLiveTokenInDB, UserInDB
from sqlalchemy.orm.session import Session
logger = getLogger()
@@ -38,12 +38,16 @@ class _Recipes(BaseDocument):
def count_uncategorized(self, session: Session, count=True, override_schema=None) -> int:
return self._countr_attribute(
session, attribute_name=RecipeModel.recipe_category, attr_match=None, count=True, override_schema=None
session,
attribute_name=RecipeModel.recipe_category,
attr_match=None,
count=count,
override_schema=override_schema,
)
def count_untagged(self, session: Session, count=True, override_schema=None) -> int:
return self._countr_attribute(
session, attribute_name=RecipeModel.tags, attr_match=None, count=True, override_schema=None
session, attribute_name=RecipeModel.tags, attr_match=None, count=count, override_schema=override_schema
)
@@ -102,6 +106,13 @@ class _Users(BaseDocument):
return self.schema.from_orm(entry)
class _LongLiveToken(BaseDocument):
def __init__(self) -> None:
self.primary_key = "id"
self.sql_model = LongLiveToken
self.schema = LongLiveTokenInDB
class _Groups(BaseDocument):
def __init__(self) -> None:
self.primary_key = "id"
@@ -154,6 +165,7 @@ class Database:
self.categories = _Categories()
self.tags = _Tags()
self.users = _Users()
self.api_tokens = _LongLiveToken()
self.sign_ups = _SignUps()
self.groups = _Groups()
self.custom_pages = _CustomPages()

View File

@@ -1,13 +1,14 @@
import sqlalchemy as sa
from mealie.db.models.model_base import SqlAlchemyBase
from sqlalchemy import Column, ForeignKey, Integer, String
class RecipeIngredient(SqlAlchemyBase):
__tablename__ = "recipes_ingredients"
id = sa.Column(sa.Integer, primary_key=True)
position = sa.Column(sa.Integer)
parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id"))
ingredient = sa.Column(sa.String)
id = Column(Integer, primary_key=True)
position = Column(Integer)
parent_id = Column(Integer, ForeignKey("recipes.id"))
# title = Column(String)
ingredient = Column(String)
def update(self, ingredient):
self.ingredient = ingredient

View File

@@ -1,12 +1,12 @@
import sqlalchemy as sa
from mealie.db.models.model_base import SqlAlchemyBase
from sqlalchemy import Column, ForeignKey, Integer, String
class RecipeInstruction(SqlAlchemyBase):
__tablename__ = "recipe_instructions"
id = sa.Column(sa.Integer, primary_key=True)
parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id"))
position = sa.Column(sa.Integer)
type = sa.Column(sa.String, default="")
text = sa.Column(sa.String)
title = sa.Column(sa.String)
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey("recipes.id"))
position = Column(Integer)
type = Column(String, default="")
title = Column(String)
text = Column(String)

View File

@@ -93,7 +93,6 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
recipe_category: list[str] = None,
tags: list[str] = None,
date_added: datetime.date = None,
date_updated: datetime.datetime = None,
notes: list[dict] = None,
rating: int = None,
org_url: str = None,

View File

@@ -3,11 +3,19 @@ from mealie.db.models.group import Group
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm
# I'm not sure this is necessasry, browser based settings may be sufficient
# class UserSettings(SqlAlchemyBase, BaseMixins):
# __tablename__ = "user_settings"
# id = Column(Integer, primary_key=True, index=True)
# parent_id = Column(String, ForeignKey("users.id"))
class LongLiveToken(SqlAlchemyBase, BaseMixins):
__tablename__ = "long_live_tokens"
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey("users.id"))
name = Column(String, nullable=False)
token = Column(String, nullable=False)
user = orm.relationship("User")
def __init__(self, session, name, token, parent_id) -> None:
self.name = name
self.token = token
self.user = User.get_ref(session, parent_id)
class User(SqlAlchemyBase, BaseMixins):
@@ -19,6 +27,9 @@ class User(SqlAlchemyBase, BaseMixins):
group_id = Column(Integer, ForeignKey("groups.id"))
group = orm.relationship("Group", back_populates="users")
admin = Column(Boolean, default=False)
tokens: list[LongLiveToken] = orm.relationship(
LongLiveToken, back_populates="user", cascade="all, delete, delete-orphan", single_parent=True
)
def __init__(
self,
@@ -29,6 +40,8 @@ class User(SqlAlchemyBase, BaseMixins):
group: str = settings.DEFAULT_GROUP,
admin=False,
id=None,
*args,
**kwargs
) -> None:
group = group or settings.DEFAULT_GROUP
@@ -38,7 +51,7 @@ class User(SqlAlchemyBase, BaseMixins):
self.admin = admin
self.password = password
def update(self, full_name, email, group, admin, session=None, id=None, password=None):
def update(self, full_name, email, group, admin, session=None, id=None, password=None, *args, **kwargs):
self.full_name = full_name
self.email = email
self.group = Group.get_ref(session, group)
@@ -49,3 +62,7 @@ class User(SqlAlchemyBase, BaseMixins):
def update_password(self, password):
self.password = password
@staticmethod
def get_ref(session, id: str):
return session.query(User).filter(User.id == id).one()

View File

@@ -8,7 +8,8 @@ from mealie.core.config import settings
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.schema.auth import TokenData
from mealie.schema.user import UserInDB
from mealie.schema.user import LongLiveTokenInDB, UserInDB
from sqlalchemy.orm.session import Session
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
ALGORITHM = "HS256"
@@ -23,8 +24,14 @@ async def get_current_user(token: str = Depends(oauth2_scheme), session=Depends(
try:
payload = jwt.decode(token, settings.SECRET, algorithms=[ALGORITHM])
username: str = payload.get("sub")
long_token: str = payload.get("long_token")
if long_token is not None:
return validate_long_live_token(session, token, payload.get("id"))
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
raise credentials_exception
@@ -35,6 +42,16 @@ async def get_current_user(token: str = Depends(oauth2_scheme), session=Depends(
return user
def validate_long_live_token(session: Session, client_token: str, id: int) -> UserInDB:
tokens: list[LongLiveTokenInDB] = db.api_tokens.get(session, id, "parent_id", limit=9999)
for token in tokens:
token: LongLiveTokenInDB
if token.token == client_token:
return token.user
async def validate_file_token(token: Optional[str] = None) -> Path:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,

View File

@@ -75,7 +75,7 @@ def get_today(session: Session = Depends(generate_session), current_user: UserIn
group_in_db: GroupInDB = db.groups.get(session, current_user.group, "name")
recipe = get_todays_meal(session, group_in_db)
if recipe:
return recipe.slug
return recipe
@router.get("/today/image", tags=["Meal Plan"])

View File

@@ -1,9 +1,10 @@
from fastapi import APIRouter
from . import auth, crud, sign_up
from . import api_tokens, auth, crud, sign_up
user_router = APIRouter()
user_router.include_router(auth.router)
user_router.include_router(sign_up.router)
user_router.include_router(crud.router)
user_router.include_router(api_tokens.router)

View File

@@ -0,0 +1,56 @@
from datetime import timedelta
from fastapi import APIRouter, HTTPException, status
from fastapi.param_functions import Depends
from mealie.core.security import create_access_token
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.schema.user import CreateToken, LoingLiveTokenIn, LongLiveTokenInDB, UserInDB
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/users", tags=["User API Tokens"])
@router.post("/api-tokens", status_code=status.HTTP_201_CREATED)
async def create_api_token(
token_name: LoingLiveTokenIn,
current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session),
):
""" Create api_token in the Database """
token_data = {"long_token": True, "id": current_user.id}
five_years = timedelta(1825)
token = create_access_token(token_data, five_years)
token_model = CreateToken(
name=token_name.name,
token=token,
parent_id=current_user.id,
)
new_token_in_db = db.api_tokens.create(session, token_model)
if new_token_in_db:
return {"token": token}
@router.delete("/api-tokens/{token_id}")
async def delete_api_token(
token_id: int,
current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session),
):
""" Delete api_token from the Database """
token: LongLiveTokenInDB = db.api_tokens.get(session, token_id)
if not token:
raise HTTPException(status.HTTP_404_NOT_FOUND, f"Could not locate token with id '{token_id}' in database")
if token.user.email == current_user.email:
deleted_token = db.api_tokens.delete(session, token_id)
return {"token_delete": deleted_token.name}
else:
raise HTTPException(status.HTTP_401_UNAUTHORIZED)

View File

@@ -1,7 +1,7 @@
from typing import Optional
from fastapi_camelcase import CamelModel
from mealie.schema.category import CategoryBase
from mealie.schema.category import CategoryBase, RecipeCategoryResponse
from pydantic import validator
from slugify import slugify
@@ -34,7 +34,7 @@ class CustomPageBase(CamelModel):
name: str
slug: Optional[str]
position: int
categories: list[CategoryBase] = []
categories: list[RecipeCategoryResponse] = []
class Config:
orm_mode = True

View File

@@ -10,6 +10,25 @@ from pydantic.types import constr
from pydantic.utils import GetterDict
class LoingLiveTokenIn(CamelModel):
name: str
class LongLiveTokenOut(LoingLiveTokenIn):
id: int
class Config:
orm_mode = True
class CreateToken(LoingLiveTokenIn):
parent_id: int
token: str
class Config:
orm_mode = True
class ChangePassword(CamelModel):
current_password: str
new_password: str
@@ -53,6 +72,7 @@ class UserIn(UserBase):
class UserOut(UserBase):
id: int
group: str
tokens: Optional[list[LongLiveTokenOut]]
class Config:
orm_mode = True
@@ -96,3 +116,11 @@ class GroupInDB(UpdateGroup):
**GetterDict(orm_model),
"webhook_urls": [x.url for x in orm_model.webhook_urls if x],
}
class LongLiveTokenInDB(CreateToken):
id: int
user: UserInDB
class Config:
orm_mode = True