mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 14:33:33 -07:00
Drop FastAPI-Login, add JWT management
* basic crud NOT SECURE * refactor/database init on startup * added scratch.py * tests/user CRUD routes * password hashing * change app_config location * bump python version * formatting * login ui starter * change import from url design * move components * remove old snackbar * refactor/Componenet folder structure rework * refactor/remove old code * refactor/rename componenets/js files * remove console.logs * refactor/ models to schema and sql to models * new header styling for imports * token request * fix url scrapper * refactor/rename schema files * split routes file * redesigned admin page * enable relative imports for vue components * refactor/switch to pages view * add CamelCase package * majors settings rework * user management second pass * super user CRUD * refactor/consistent models names * refactor/consistent model names * password reset * store refactor * dependency update * abstract button props * profile page refactor * basic password validation * login form refactor/split v-container * remo unused code * hide editor buttons when not logged in * mkdocs dev dependency * v0.4.0 docs update * profile image upload * additional token routes * Smaller recipe cards for smaller viewports * fix admin sidebar * add users * change to outlined * theme card starter * code cleanup * signups * signup pages * fix #194 * fix #193 * clarify mealie_port * fix #184 * fixes #178 * fix blank card error on meal-plan creator * admin signup * formatting * improved search bar * improved search bar * refresh token on page refresh * allow mealplan with no categories * fix card layout * remove cdn dependencies * start on groups * Fixes #196 * recipe databse refactor * changelog draft * database refactoring * refactor recipe schema/model * site settings refactor * continued model refactor * merge docs changes from master * site-settings work * cleanup + tag models * notes * typo * user table * sign up data validation * package updates * group store init * Fix home page settings * group admin init * group dashboard init * update deps * formatting * bug / added libffi-dev * pages refactor * fix mealplan * docs update * multi group supporot for job scheduler * formatting * formatting * home-page redesign * set background for docs darkmode * code cleanup * docs refactor * v0.4.0 image * mkdocs port change * formatting * Fix Meal-Plan Today * fix webhook bug * fix meal plan this week * export users * 📦 Proper Package + Black Config * formatting * delete old files * fix ci * fix failing builds * package/makefile docs update * add docs server to tasks * uncomment docker-compose * reload in dev env * move developer data * fix upload issue * run init_db before startup * import groups and users * fix themes * fix theme * potentially fixes #216 * unlink test db * potentially fix #217 * localization * fix import errors on no group * fix hacky lxml error * fix import error * more import errors * test failing tests * fix/test that never really passed Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
parent
6bb1e42026
commit
4a5b0fcfe5
20 changed files with 222 additions and 150 deletions
3
.github/workflows/pytest.yml
vendored
3
.github/workflows/pytest.yml
vendored
|
@ -50,5 +50,4 @@ jobs:
|
|||
#----------------------------------------------
|
||||
- name: Run tests
|
||||
run: |
|
||||
source .venv/bin/activate
|
||||
pytest
|
||||
poetry run pytest
|
||||
|
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -3,8 +3,6 @@
|
|||
"python.pythonPath": ".venv/bin/python3.9",
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.linting.enabled": true,
|
||||
"python.autoComplete.extraPaths": ["mealie", "mealie/mealie"],
|
||||
"python.analysis.extraPaths": ["mealie", "mealie/mealie"],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.nosetestsEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
|
|
0
mealie/core/__init__.py
Normal file
0
mealie/core/__init__.py
Normal file
|
@ -19,7 +19,7 @@ ENV = CWD.joinpath(".env") #! I'm Broken Fix Me!
|
|||
dotenv.load_dotenv(ENV)
|
||||
|
||||
|
||||
SECRET = "super-secret-key"
|
||||
SECRET = "test-secret-shhh"
|
||||
|
||||
# General
|
||||
PRODUCTION = os.environ.get("ENV")
|
||||
|
|
|
@ -1,6 +1,33 @@
|
|||
from datetime import datetime, timedelta
|
||||
from mealie.schema.user import UserInDB
|
||||
|
||||
from jose import jwt
|
||||
from mealie.core.config import SECRET
|
||||
from mealie.db.database import db
|
||||
from passlib.context import CryptContext
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
ALGORITHM = "HS256"
|
||||
|
||||
|
||||
def create_access_token(data: dict(), expires_delta: timedelta = None) -> str:
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=120)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, SECRET, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
def authenticate_user(session, email: str, password: str) -> UserInDB:
|
||||
user: UserInDB = db.users.get(session, email, "email")
|
||||
if not user:
|
||||
return False
|
||||
if not verify_password(password, user.password):
|
||||
return False
|
||||
return user
|
||||
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
|
|
|
@ -1,23 +1,35 @@
|
|||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from jose import JWTError, jwt
|
||||
from mealie.core.config import SECRET
|
||||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import create_session
|
||||
from fastapi_login import LoginManager
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie.db.db_setup import create_session, generate_session
|
||||
from mealie.schema.auth import Token, TokenData
|
||||
from mealie.schema.user import UserInDB
|
||||
|
||||
manager = LoginManager(SECRET, "/api/auth/token")
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
|
||||
ALGORITHM = "HS256"
|
||||
|
||||
|
||||
@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()
|
||||
async def get_current_user(token: str = Depends(oauth2_scheme), session=Depends(generate_session)) -> UserInDB:
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET, algorithms=[ALGORITHM])
|
||||
username: str = payload.get("sub")
|
||||
if username is None:
|
||||
raise credentials_exception
|
||||
token_data = TokenData(username=username)
|
||||
print("Login Payload", token_data)
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
user = db.users.get(session, token_data.username, "email")
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
return user
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
from fastapi import APIRouter, Depends
|
||||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends
|
||||
from mealie.routes.deps import manager
|
||||
from mealie.routes.deps import get_current_user
|
||||
from mealie.schema.snackbar import SnackResponse
|
||||
from mealie.schema.user import GroupBase, GroupInDB, UpdateGroup, UserInDB
|
||||
from mealie.schema.user import GroupBase, GroupInDB, UpdateGroup, UserIn, UserInDB
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/groups", tags=["Groups"])
|
||||
|
@ -11,7 +11,7 @@ router = APIRouter(prefix="/api/groups", tags=["Groups"])
|
|||
|
||||
@router.get("", response_model=list[GroupInDB])
|
||||
async def get_all_groups(
|
||||
current_user=Depends(manager),
|
||||
current_user=Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Returns a list of all groups in the database """
|
||||
|
@ -21,7 +21,7 @@ async def get_all_groups(
|
|||
|
||||
@router.get("/self", response_model=GroupInDB)
|
||||
async def get_current_user_group(
|
||||
current_user=Depends(manager),
|
||||
current_user: UserInDB =Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Returns the Group Data for the Current User """
|
||||
|
@ -33,7 +33,7 @@ async def get_current_user_group(
|
|||
@router.post("")
|
||||
async def create_group(
|
||||
group_data: GroupBase,
|
||||
current_user=Depends(manager),
|
||||
current_user=Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Creates a Group in the Database """
|
||||
|
@ -49,7 +49,7 @@ async def create_group(
|
|||
async def update_group_data(
|
||||
id: int,
|
||||
group_data: UpdateGroup,
|
||||
current_user=Depends(manager),
|
||||
current_user=Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Updates a User Group """
|
||||
|
@ -59,7 +59,9 @@ async def update_group_data(
|
|||
|
||||
|
||||
@router.delete("/{id}")
|
||||
async def delete_user_group(id: int, current_user=Depends(manager), session: Session = Depends(generate_session)):
|
||||
async def delete_user_group(
|
||||
id: int, current_user=Depends(get_current_user), session: Session = Depends(generate_session)
|
||||
):
|
||||
""" Removes a user group from the database """
|
||||
|
||||
if id == 1:
|
||||
|
|
|
@ -3,7 +3,7 @@ import datetime
|
|||
from fastapi import APIRouter, Depends
|
||||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.routes.deps import manager
|
||||
from mealie.routes.deps import get_current_user
|
||||
from mealie.schema.meal import MealPlanIn, MealPlanInDB
|
||||
from mealie.schema.snackbar import SnackResponse
|
||||
from mealie.schema.user import GroupInDB, UserInDB
|
||||
|
@ -15,7 +15,7 @@ router = APIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
|
|||
|
||||
@router.get("/all", response_model=list[MealPlanInDB])
|
||||
def get_all_meals(
|
||||
current_user: UserInDB = Depends(manager),
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Returns a list of all available Meal Plan """
|
||||
|
@ -27,7 +27,7 @@ def get_all_meals(
|
|||
def create_meal_plan(
|
||||
data: MealPlanIn,
|
||||
session: Session = Depends(generate_session),
|
||||
current_user=Depends(manager),
|
||||
current_user=Depends(get_current_user),
|
||||
):
|
||||
""" Creates a meal plan database entry """
|
||||
processed_plan = process_meals(session, data)
|
||||
|
@ -58,7 +58,7 @@ def delete_meal_plan(plan_id, session: Session = Depends(generate_session)):
|
|||
@router.get("/this-week", response_model=MealPlanInDB)
|
||||
def get_this_week(
|
||||
session: Session = Depends(generate_session),
|
||||
current_user: UserInDB = Depends(manager),
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
):
|
||||
""" Returns the meal plan data for this week """
|
||||
|
||||
|
@ -68,7 +68,7 @@ def get_this_week(
|
|||
@router.get("/today", tags=["Meal Plan"])
|
||||
def get_today(
|
||||
session: Session = Depends(generate_session),
|
||||
current_user: UserInDB = Depends(manager),
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Returns the recipe slug for the meal scheduled for today.
|
||||
|
|
|
@ -7,7 +7,7 @@ from mealie.schema.user import GroupInDB, UserInDB
|
|||
from sqlalchemy.orm.session import Session
|
||||
from mealie.utils.post_webhooks import post_webhooks
|
||||
|
||||
from mealie.routes.deps import manager
|
||||
from mealie.routes.deps import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/api/site-settings", tags=["Settings"])
|
||||
|
||||
|
@ -31,7 +31,7 @@ def update_settings(data: SiteSettings, session: Session = Depends(generate_sess
|
|||
|
||||
@router.post("/webhooks/test")
|
||||
def test_webhooks(
|
||||
current_user: UserInDB = Depends(manager),
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Run the function to test your webhooks """
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
from datetime import timedelta
|
||||
|
||||
from mealie.core.security import verify_password
|
||||
from mealie.db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from fastapi_login.exceptions import InvalidCredentialsException
|
||||
from mealie.routes.deps import manager, query_user
|
||||
from mealie.core import security
|
||||
from mealie.core.security import authenticate_user, verify_password
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.routes.deps import get_current_user
|
||||
from mealie.schema.snackbar import SnackResponse
|
||||
from mealie.schema.user import UserInDB
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
@ -13,6 +14,7 @@ from sqlalchemy.orm.session import Session
|
|||
router = APIRouter(prefix="/api/auth", tags=["Authentication"])
|
||||
|
||||
|
||||
@router.post("/token/long")
|
||||
@router.post("/token")
|
||||
def get_token(
|
||||
data: OAuth2PasswordRequestForm = Depends(),
|
||||
|
@ -21,35 +23,16 @@ def get_token(
|
|||
email = data.username
|
||||
password = data.password
|
||||
|
||||
user: UserInDB = query_user(email, session)
|
||||
user = authenticate_user(session, email, password)
|
||||
|
||||
if not user:
|
||||
raise InvalidCredentialsException # you can also use your own HTTPException
|
||||
elif not verify_password(password, user.password):
|
||||
raise InvalidCredentialsException
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
access_token = manager.create_access_token(data=dict(sub=email), expires=timedelta(hours=2))
|
||||
return SnackResponse.success(
|
||||
"User Successfully Logged In",
|
||||
{"access_token": access_token, "token_type": "bearer"},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/token/long")
|
||||
def get_long_token(
|
||||
data: OAuth2PasswordRequestForm = Depends(),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
"""Get an Access Token for 1 day"""
|
||||
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(days=1))
|
||||
access_token = security.create_access_token(dict(sub=email), timedelta(hours=2))
|
||||
return SnackResponse.success(
|
||||
"User Successfully Logged In",
|
||||
{"access_token": access_token, "token_type": "bearer"},
|
||||
|
@ -57,7 +40,7 @@ def get_long_token(
|
|||
|
||||
|
||||
@router.get("/refresh")
|
||||
async def refresh_token(current_user: UserInDB = Depends(manager)):
|
||||
async def refresh_token(current_user: UserInDB = Depends(get_current_user)):
|
||||
""" Use a valid token to get another token"""
|
||||
access_token = manager.create_access_token(data=dict(sub=current_user.email), expires=timedelta(hours=1))
|
||||
access_token = security.create_access_token(data=dict(sub=current_user.email), expires_delta=timedelta(hours=1))
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import shutil
|
||||
from datetime import timedelta
|
||||
from os import access
|
||||
|
||||
from fastapi import APIRouter, Depends, File, UploadFile
|
||||
from fastapi.responses import FileResponse
|
||||
from mealie.core import security
|
||||
from mealie.core.config import USER_DIR
|
||||
from mealie.core.security import get_password_hash, verify_password
|
||||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends, File, UploadFile
|
||||
from fastapi.responses import FileResponse
|
||||
from mealie.routes.deps import manager
|
||||
from mealie.routes.deps import get_current_user
|
||||
from mealie.schema.snackbar import SnackResponse
|
||||
from mealie.schema.user import ChangePassword, UserBase, UserIn, UserInDB, UserOut
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
@ -19,7 +19,7 @@ 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),
|
||||
current_user=Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
|
||||
|
@ -31,7 +31,7 @@ async def create_user(
|
|||
|
||||
@router.get("", response_model=list[UserOut])
|
||||
async def get_all_users(
|
||||
current_user: UserInDB = Depends(manager),
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
|
||||
|
@ -43,7 +43,7 @@ async def get_all_users(
|
|||
|
||||
@router.get("/self", response_model=UserOut)
|
||||
async def get_logged_in_user(
|
||||
current_user: UserInDB = Depends(manager),
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
return current_user.dict()
|
||||
|
@ -52,7 +52,7 @@ async def get_logged_in_user(
|
|||
@router.get("/{id}", response_model=UserOut)
|
||||
async def get_user_by_id(
|
||||
id: int,
|
||||
current_user: UserInDB = Depends(manager),
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
return db.users.get(session, id)
|
||||
|
@ -62,19 +62,21 @@ async def get_user_by_id(
|
|||
async def update_user(
|
||||
id: int,
|
||||
new_data: UserBase,
|
||||
current_user: UserInDB = Depends(manager),
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
|
||||
access_token = None
|
||||
token = None
|
||||
if current_user.id == id or current_user.admin:
|
||||
updated_user: UserInDB = db.users.update(session, id, new_data.dict())
|
||||
email = updated_user.email
|
||||
print("Current User")
|
||||
db.users.update(session, id, new_data.dict())
|
||||
if current_user.id == id:
|
||||
access_token = manager.create_access_token(data=dict(sub=email), expires=timedelta(hours=2))
|
||||
access_token = {"access_token": access_token, "token_type": "bearer"}
|
||||
print(new_data.email)
|
||||
access_token = security.create_access_token(data=dict(sub=new_data.email), expires_delta=timedelta(hours=2))
|
||||
token = {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
return SnackResponse.success("User Updated", access_token)
|
||||
print(SnackResponse.success("User Updated", token))
|
||||
return SnackResponse.success("User Updated", token)
|
||||
|
||||
|
||||
@router.get("/{id}/image")
|
||||
|
@ -91,7 +93,7 @@ async def get_user_image(id: str):
|
|||
async def update_user_image(
|
||||
id: str,
|
||||
profile_image: UploadFile = File(...),
|
||||
current_user: UserInDB = Depends(manager),
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
):
|
||||
""" Updates a User Image """
|
||||
|
||||
|
@ -119,7 +121,7 @@ async def update_user_image(
|
|||
async def update_password(
|
||||
id: int,
|
||||
password_change: ChangePassword,
|
||||
current_user: UserInDB = Depends(manager),
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Resets the User Password"""
|
||||
|
@ -138,7 +140,7 @@ async def update_password(
|
|||
@router.delete("/{id}")
|
||||
async def delete_user(
|
||||
id: int,
|
||||
current_user: UserInDB = Depends(manager),
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Removes a user from the database. Must be the current user or a super user"""
|
||||
|
|
|
@ -4,7 +4,7 @@ from mealie.core.security import get_password_hash
|
|||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends
|
||||
from mealie.routes.deps import manager
|
||||
from mealie.routes.deps import get_current_user
|
||||
from mealie.schema.sign_up import SignUpIn, SignUpOut, SignUpToken
|
||||
from mealie.schema.snackbar import SnackResponse
|
||||
from mealie.schema.user import UserIn, UserInDB
|
||||
|
@ -15,7 +15,7 @@ router = APIRouter(prefix="/api/users/sign-ups", tags=["User Signup"])
|
|||
|
||||
@router.get("", response_model=list[SignUpOut])
|
||||
async def get_all_open_sign_ups(
|
||||
current_user=Depends(manager),
|
||||
current_user=Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Returns a list of open sign up links """
|
||||
|
@ -28,7 +28,7 @@ async def get_all_open_sign_ups(
|
|||
@router.post("", response_model=SignUpToken)
|
||||
async def create_user_sign_up_key(
|
||||
key_data: SignUpIn,
|
||||
current_user: UserInDB = Depends(manager),
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Generates a Random Token that a new user can sign up with """
|
||||
|
@ -75,7 +75,7 @@ async def create_user_with_token(
|
|||
@router.delete("/{token}")
|
||||
async def delete_token(
|
||||
token: str,
|
||||
current_user: UserInDB = Depends(manager),
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Removed a token from the database """
|
||||
|
|
10
mealie/schema/auth.py
Normal file
10
mealie/schema/auth.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
|
||||
class TokenData(BaseModel):
|
||||
username: Optional[str] = None
|
|
@ -1,13 +1,12 @@
|
|||
from typing import Any, Optional
|
||||
from typing import Optional
|
||||
|
||||
from fastapi_camelcase import CamelModel
|
||||
from mealie.core.config import DEFAULT_GROUP
|
||||
from mealie.db.models.group import Group
|
||||
from mealie.db.models.users import User
|
||||
from fastapi_camelcase import CamelModel
|
||||
from pydantic.utils import GetterDict
|
||||
|
||||
from mealie.schema.category import CategoryBase
|
||||
from mealie.schema.meal import MealPlanInDB
|
||||
from pydantic.utils import GetterDict
|
||||
|
||||
|
||||
class ChangePassword(CamelModel):
|
||||
|
|
113
poetry.lock
generated
113
poetry.lock
generated
|
@ -197,6 +197,17 @@ category = "main"
|
|||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*"
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
version = "0.14.1"
|
||||
description = "ECDSA cryptographic signature library (pure python)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[package.dependencies]
|
||||
six = "*"
|
||||
|
||||
[[package]]
|
||||
name = "extruct"
|
||||
version = "0.12.0"
|
||||
|
@ -249,19 +260,6 @@ python-versions = ">=3.6"
|
|||
pydantic = "*"
|
||||
pyhumps = "*"
|
||||
|
||||
[[package]]
|
||||
name = "fastapi-login"
|
||||
version = "1.5.3"
|
||||
description = ""
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
fastapi = "*"
|
||||
passlib = "*"
|
||||
pyjwt = "*"
|
||||
|
||||
[[package]]
|
||||
name = "future"
|
||||
version = "0.18.2"
|
||||
|
@ -614,6 +612,14 @@ category = "dev"
|
|||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "pyasn1"
|
||||
version = "0.4.8"
|
||||
description = "ASN.1 types and codecs"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.20"
|
||||
|
@ -653,20 +659,6 @@ 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.7.2"
|
||||
|
@ -763,6 +755,25 @@ python-versions = "*"
|
|||
[package.extras]
|
||||
cli = ["click (>=5.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "python-jose"
|
||||
version = "3.2.0"
|
||||
description = "JOSE implementation in Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
ecdsa = "<0.15"
|
||||
pyasn1 = "*"
|
||||
rsa = "*"
|
||||
six = "<2.0"
|
||||
|
||||
[package.extras]
|
||||
cryptography = ["cryptography"]
|
||||
pycrypto = ["pycrypto (>=2.6.0,<2.7.0)", "pyasn1"]
|
||||
pycryptodome = ["pycryptodome (>=3.3.1,<4.0.0)", "pyasn1"]
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.5"
|
||||
|
@ -860,6 +871,17 @@ urllib3 = ">=1.21.1,<1.27"
|
|||
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "4.7.2"
|
||||
description = "Pure-Python RSA implementation"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5, <4"
|
||||
|
||||
[package.dependencies]
|
||||
pyasn1 = ">=0.1.3"
|
||||
|
||||
[[package]]
|
||||
name = "scrape-schema-recipe"
|
||||
version = "0.1.3"
|
||||
|
@ -1103,7 +1125,7 @@ python-versions = "*"
|
|||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.9"
|
||||
content-hash = "adf8ad8e07d1af5c231c936276b57be83dd534e0cf706042ddb26f6ff51c86ca"
|
||||
content-hash = "688326ef0f3bf3b2d2d515b941dbca379f26b08ae83afab66fa0ec95dc2c57ce"
|
||||
|
||||
[metadata.files]
|
||||
aiofiles = [
|
||||
|
@ -1264,6 +1286,10 @@ decorator = [
|
|||
{file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"},
|
||||
{file = "decorator-4.4.2.tar.gz", hash = "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"},
|
||||
]
|
||||
ecdsa = [
|
||||
{file = "ecdsa-0.14.1-py2.py3-none-any.whl", hash = "sha256:e108a5fe92c67639abae3260e43561af914e7fd0d27bae6d2ec1312ae7934dfe"},
|
||||
{file = "ecdsa-0.14.1.tar.gz", hash = "sha256:64c613005f13efec6541bb0a33290d0d03c27abab5f15fbab20fb0ee162bdd8e"},
|
||||
]
|
||||
extruct = [
|
||||
{file = "extruct-0.12.0-py2.py3-none-any.whl", hash = "sha256:42c6c9f50b00aa6c17b5c26b5f1b3a337ebc27b427fafc3714f34ce3bbb16c2f"},
|
||||
{file = "extruct-0.12.0.tar.gz", hash = "sha256:d4a68bb79d1b85ff36d603a42c2666888bb480191a399a659d9daaf735358276"},
|
||||
|
@ -1275,10 +1301,6 @@ fastapi = [
|
|||
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"},
|
||||
]
|
||||
future = [
|
||||
{file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
|
||||
]
|
||||
|
@ -1552,6 +1574,21 @@ py = [
|
|||
{file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
|
||||
{file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
|
||||
]
|
||||
pyasn1 = [
|
||||
{file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"},
|
||||
{file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"},
|
||||
{file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"},
|
||||
{file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"},
|
||||
{file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"},
|
||||
{file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"},
|
||||
{file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"},
|
||||
{file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"},
|
||||
{file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"},
|
||||
{file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"},
|
||||
{file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"},
|
||||
{file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"},
|
||||
{file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
|
||||
]
|
||||
pycparser = [
|
||||
{file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
|
||||
{file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
|
||||
|
@ -1588,10 +1625,6 @@ 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.7.2-py3-none-any.whl", hash = "sha256:d09b0b07ba06bcdff463958f53f23df25e740ecd81895f7d2699ec04bbd8dc3b"},
|
||||
{file = "pylint-2.7.2.tar.gz", hash = "sha256:0e21d3b80b96740909d77206d741aa3ce0b06b41be375d92e1f3244a274c1f8a"},
|
||||
|
@ -1620,6 +1653,10 @@ python-dotenv = [
|
|||
{file = "python-dotenv-0.15.0.tar.gz", hash = "sha256:587825ed60b1711daea4832cf37524dfd404325b7db5e25ebe88c495c9f807a0"},
|
||||
{file = "python_dotenv-0.15.0-py2.py3-none-any.whl", hash = "sha256:0c8d1b80d1a1e91717ea7d526178e3882732420b03f08afea0406db6402e220e"},
|
||||
]
|
||||
python-jose = [
|
||||
{file = "python-jose-3.2.0.tar.gz", hash = "sha256:4e4192402e100b5fb09de5a8ea6bcc39c36ad4526341c123d401e2561720335b"},
|
||||
{file = "python_jose-3.2.0-py2.py3-none-any.whl", hash = "sha256:67d7dfff599df676b04a996520d9be90d6cdb7e6dd10b4c7cacc0c3e2e92f2be"},
|
||||
]
|
||||
python-multipart = [
|
||||
{file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"},
|
||||
]
|
||||
|
@ -1707,6 +1744,10 @@ requests = [
|
|||
{file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
|
||||
{file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
|
||||
]
|
||||
rsa = [
|
||||
{file = "rsa-4.7.2-py3-none-any.whl", hash = "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2"},
|
||||
{file = "rsa-4.7.2.tar.gz", hash = "sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9"},
|
||||
]
|
||||
scrape-schema-recipe = [
|
||||
{file = "scrape-schema-recipe-0.1.3.tar.gz", hash = "sha256:f5c9bdbdb254ac4ca008e4233afd38308cf9877fc9399643d03087df0d950aea"},
|
||||
{file = "scrape_schema_recipe-0.1.3-py2.py3-none-any.whl", hash = "sha256:7a505d7cd94091ffdfcbac0fad21dd583cceee2d9c7ea12366e8fefac8b4da82"},
|
||||
|
|
|
@ -25,9 +25,10 @@ PyYAML = "^5.3.1"
|
|||
extruct = "^0.12.0"
|
||||
scrape-schema-recipe = "^0.1.3"
|
||||
python-multipart = "^0.0.5"
|
||||
fastapi-login = "^1.5.3"
|
||||
fastapi-camelcase = "^1.0.2"
|
||||
bcrypt = "^3.2.0"
|
||||
python-jose = "^3.2.0"
|
||||
passlib = "^1.7.4"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pylint = "^2.6.0"
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import json
|
||||
|
||||
import requests
|
||||
from fastapi.testclient import TestClient
|
||||
from mealie.app import app
|
||||
from mealie.core.config import SQLITE_DIR
|
||||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import generate_session, sql_global_init
|
||||
from mealie.db.init_db import init_db
|
||||
from fastapi.testclient import TestClient
|
||||
from mealie.routes.deps import get_current_user
|
||||
from mealie.schema.user import UserInDB
|
||||
from pytest import fixture
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from tests.test_config import TEST_DATA
|
||||
|
||||
|
|
|
@ -23,9 +23,6 @@ def get_meal_plan_template(first=None, second=None):
|
|||
}
|
||||
|
||||
|
||||
## Meal Routes
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def slug_1(api_client):
|
||||
# Slug 1
|
||||
|
@ -50,9 +47,7 @@ def slug_2(api_client):
|
|||
|
||||
|
||||
def test_create_mealplan(api_client, slug_1, slug_2, token):
|
||||
meal_plan = get_meal_plan_template()
|
||||
meal_plan["meals"][0]["slug"] = slug_1
|
||||
meal_plan["meals"][1]["slug"] = slug_2
|
||||
meal_plan = get_meal_plan_template(slug_1, slug_2)
|
||||
|
||||
response = api_client.post(MEALPLAN_CREATE, json=meal_plan, headers=token)
|
||||
assert response.status_code == 200
|
||||
|
|
|
@ -17,14 +17,17 @@ def new_user():
|
|||
return {"id": 2, "fullName": "My New User", "email": "newuser@email.com", "group": "Home", "admin": False}
|
||||
|
||||
|
||||
def test_superuser_login(api_client: requests):
|
||||
def test_superuser_login(api_client: requests, token):
|
||||
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")
|
||||
new_token = json.loads(response.text).get("access_token")
|
||||
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
response = api_client.get("/api/users/self", headers=token)
|
||||
assert response.status_code == 200
|
||||
|
||||
return {"Authorization": f"Bearer {new_token}"}
|
||||
|
||||
|
||||
def test_init_superuser(api_client: requests, token, default_user):
|
||||
|
@ -59,10 +62,11 @@ def test_get_all_users(api_client: requests, token, new_user, default_user):
|
|||
|
||||
|
||||
def test_update_user(api_client: requests, token):
|
||||
update_data = {"id": 1, "fullName": "Updated Name", "email": "updated@email.com", "group": "Home", "admin": True}
|
||||
update_data = {"id": 1, "fullName": "Updated Name", "email": "changeme@email.com", "group": "Home", "admin": True}
|
||||
response = api_client.put(f"{BASE}/1", headers=token, json=update_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
print(response.text)
|
||||
assert json.loads(response.text).get("access_token")
|
||||
|
||||
|
||||
|
|
|
@ -4,12 +4,7 @@ import pytest
|
|||
from mealie.core.config import TEMP_DIR
|
||||
from mealie.schema.recipe import Recipe
|
||||
from mealie.services.image_services import IMG_DIR
|
||||
from mealie.services.migrations.nextcloud import (
|
||||
cleanup,
|
||||
import_recipes,
|
||||
prep,
|
||||
process_selection,
|
||||
)
|
||||
from mealie.services.migrations.nextcloud import cleanup, import_recipes, prep, process_selection
|
||||
from tests.test_config import TEST_NEXTCLOUD_DIR
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue