diff --git a/backend/app/alembic/env.py b/backend/app/alembic/env.py index 7f29c04..e63bb45 100755 --- a/backend/app/alembic/env.py +++ b/backend/app/alembic/env.py @@ -18,9 +18,10 @@ # target_metadata = mymodel.Base.metadata # target_metadata = None -from app.models import SQLModel # noqa +from sqlmodel import SQLModel # Direkter Import aus dem sqlmodel Paket from app.core.config import settings # noqa + target_metadata = SQLModel.metadata # other values from the config, defined by the needs of env.py, diff --git a/backend/app/api/deps.py b/backend/app/api/deps.py index c2b83c8..625f3c3 100644 --- a/backend/app/api/deps.py +++ b/backend/app/api/deps.py @@ -11,7 +11,7 @@ from app.core import security from app.core.config import settings from app.core.db import engine -from app.models import TokenPayload, User +from app.models.user import User, TokenPayload reusable_oauth2 = OAuth2PasswordBearer( tokenUrl=f"{settings.API_V1_STR}/login/access-token" diff --git a/backend/app/api/routes/items.py b/backend/app/api/routes/items.py index d8a0b14..dab74d2 100644 --- a/backend/app/api/routes/items.py +++ b/backend/app/api/routes/items.py @@ -5,7 +5,8 @@ from sqlmodel import func, select from app.api.deps import CurrentUser, SessionDep -from app.models import Item, ItemCreate, ItemPublic, ItemsPublic, ItemUpdate, Message +from app.models.user import Message +from app.models.item import Item, ItemCreate, ItemPublic, ItemsPublic, ItemUpdate router = APIRouter() diff --git a/backend/app/api/routes/login.py b/backend/app/api/routes/login.py index fe7e94d..2bfdf32 100644 --- a/backend/app/api/routes/login.py +++ b/backend/app/api/routes/login.py @@ -5,12 +5,12 @@ from fastapi.responses import HTMLResponse from fastapi.security import OAuth2PasswordRequestForm -from app import crud +from app.crud import authenticate, get_user_by_email from app.api.deps import CurrentUser, SessionDep, get_current_active_superuser from app.core import security from app.core.config import settings from app.core.security import get_password_hash -from app.models import Message, NewPassword, Token, UserPublic +from app.models.user import Message, NewPassword, Token, UserPublic from app.utils import ( generate_password_reset_token, generate_reset_password_email, @@ -28,7 +28,7 @@ def login_access_token( """ OAuth2 compatible token login, get an access token for future requests """ - user = crud.authenticate( + user = authenticate( session=session, email=form_data.username, password=form_data.password ) if not user: @@ -56,7 +56,7 @@ def recover_password(email: str, session: SessionDep) -> Message: """ Password Recovery """ - user = crud.get_user_by_email(session=session, email=email) + user = get_user_by_email(session=session, email=email) if not user: raise HTTPException( @@ -83,7 +83,7 @@ def reset_password(session: SessionDep, body: NewPassword) -> Message: email = verify_password_reset_token(token=body.token) if not email: raise HTTPException(status_code=400, detail="Invalid token") - user = crud.get_user_by_email(session=session, email=email) + user = get_user_by_email(session=session, email=email) if not user: raise HTTPException( status_code=404, @@ -107,7 +107,7 @@ def recover_password_html_content(email: str, session: SessionDep) -> Any: """ HTML Content for Password Recovery """ - user = crud.get_user_by_email(session=session, email=email) + user = get_user_by_email(session=session, email=email) if not user: raise HTTPException( diff --git a/backend/app/api/routes/users.py b/backend/app/api/routes/users.py index c636b09..861e6fa 100644 --- a/backend/app/api/routes/users.py +++ b/backend/app/api/routes/users.py @@ -4,7 +4,8 @@ from fastapi import APIRouter, Depends, HTTPException from sqlmodel import col, delete, func, select -from app import crud +from app.crud import get_user_by_email, create_user, update_user + from app.api.deps import ( CurrentUser, SessionDep, diff --git a/backend/app/api/routes/utils.py b/backend/app/api/routes/utils.py index a73b80d..2ab0e64 100644 --- a/backend/app/api/routes/utils.py +++ b/backend/app/api/routes/utils.py @@ -2,7 +2,7 @@ from pydantic.networks import EmailStr from app.api.deps import get_current_active_superuser -from app.models import Message +from app.models.user import Message from app.utils import generate_test_email, send_email router = APIRouter() diff --git a/backend/app/core/db.py b/backend/app/core/db.py index ba991fb..69c84da 100644 --- a/backend/app/core/db.py +++ b/backend/app/core/db.py @@ -1,6 +1,6 @@ from sqlmodel import Session, create_engine, select -from app import crud +from app.crud import create_user # Direkt importiert from app.core.config import settings from app.models import User, UserCreate @@ -30,4 +30,4 @@ def init_db(session: Session) -> None: password=settings.FIRST_SUPERUSER_PASSWORD, is_superuser=True, ) - user = crud.create_user(session=session, user_create=user_in) + user = create_user(session=session, user_create=user_in) # Direkter Aufruf von create_user diff --git a/backend/app/crud/__init__.py b/backend/app/crud/__init__.py new file mode 100644 index 0000000..f7377ef --- /dev/null +++ b/backend/app/crud/__init__.py @@ -0,0 +1,12 @@ +from .crud_user import ( + create_user, + update_user, + get_user_by_email, + authenticate, +) + +from .crud_item import ( + create_item, + get_item, + get_items, +) diff --git a/backend/app/crud/crud_item.py b/backend/app/crud/crud_item.py new file mode 100644 index 0000000..4ce814e --- /dev/null +++ b/backend/app/crud/crud_item.py @@ -0,0 +1,16 @@ +from sqlmodel import Session, select +from app.models.item import Item, ItemCreate + +def create_item(*, session: Session, item_create: ItemCreate) -> Item: + db_item = Item(**item_create.dict()) + session.add(db_item) + session.commit() + session.refresh(db_item) + return db_item + +def get_item(*, session: Session, item_id: int) -> Item | None: + return session.get(Item, item_id) + +def get_items(*, session: Session, skip: int = 0, limit: int = 10): + statement = select(Item).offset(skip).limit(limit) + return session.exec(statement).all() diff --git a/backend/app/crud.py b/backend/app/crud/crud_user.py similarity index 80% rename from backend/app/crud.py rename to backend/app/crud/crud_user.py index 905bf48..7e96b36 100644 --- a/backend/app/crud.py +++ b/backend/app/crud/crud_user.py @@ -1,10 +1,7 @@ -import uuid -from typing import Any - from sqlmodel import Session, select - from app.core.security import get_password_hash, verify_password -from app.models import Item, ItemCreate, User, UserCreate, UserUpdate +from app.models.user import User, UserCreate, UserUpdate +from typing import Any def create_user(*, session: Session, user_create: UserCreate) -> User: @@ -16,7 +13,6 @@ def create_user(*, session: Session, user_create: UserCreate) -> User: session.refresh(db_obj) return db_obj - def update_user(*, session: Session, db_user: User, user_in: UserUpdate) -> Any: user_data = user_in.model_dump(exclude_unset=True) extra_data = {} @@ -30,13 +26,11 @@ def update_user(*, session: Session, db_user: User, user_in: UserUpdate) -> Any: session.refresh(db_user) return db_user - def get_user_by_email(*, session: Session, email: str) -> User | None: statement = select(User).where(User.email == email) session_user = session.exec(statement).first() return session_user - def authenticate(*, session: Session, email: str, password: str) -> User | None: db_user = get_user_by_email(session=session, email=email) if not db_user: @@ -44,11 +38,3 @@ def authenticate(*, session: Session, email: str, password: str) -> User | None: if not verify_password(password, db_user.hashed_password): return None return db_user - - -def create_item(*, session: Session, item_in: ItemCreate, owner_id: uuid.UUID) -> Item: - db_item = Item.model_validate(item_in, update={"owner_id": owner_id}) - session.add(db_item) - session.commit() - session.refresh(db_item) - return db_item diff --git a/backend/app/models.py b/backend/app/models.py deleted file mode 100644 index efafb1d..0000000 --- a/backend/app/models.py +++ /dev/null @@ -1,133 +0,0 @@ -import uuid - -from pydantic import EmailStr -from sqlmodel import Field, Relationship, SQLModel -from typing import Optional -from datetime import date - - -# Shared properties -class UserBase(SQLModel): - email: EmailStr = Field(unique=True, index=True, max_length=255) - is_active: bool = True - is_superuser: bool = False - full_name: str | None = Field(default=None, max_length=255) - - -# Properties to receive via API on creation -class UserCreate(UserBase): - password: str = Field(min_length=8, max_length=40) - - -class UserRegister(SQLModel): - email: EmailStr = Field(max_length=255) - password: str = Field(min_length=8, max_length=40) - full_name: str | None = Field(default=None, max_length=255) - - -# Properties to receive via API on update, all are optional -class UserUpdate(UserBase): - email: EmailStr | None = Field(default=None, max_length=255) # type: ignore - password: str | None = Field(default=None, min_length=8, max_length=40) - - -class UserUpdateMe(SQLModel): - full_name: str | None = Field(default=None, max_length=255) - email: EmailStr | None = Field(default=None, max_length=255) - - -class UpdatePassword(SQLModel): - current_password: str = Field(min_length=8, max_length=40) - new_password: str = Field(min_length=8, max_length=40) - - -# Database model, database table inferred from class name -class User(UserBase, table=True): - id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) - hashed_password: str - items: list["Item"] = Relationship(back_populates="owner", cascade_delete=True) - - -# Properties to return via API, id is always required -class UserPublic(UserBase): - id: uuid.UUID - - -class UsersPublic(SQLModel): - data: list[UserPublic] - count: int - - -# Shared properties -class ItemBase(SQLModel): - title: str = Field(min_length=1, max_length=255) - description: str | None = Field(default=None, max_length=255) - aiwscode: str = Field(min_length=1, max_length=50) - name: str = Field(min_length=1, max_length=255) - location: str = Field(min_length=1, max_length=255) - expiry: Optional[date] = Field(default=None) - stk: Optional[date] = Field(default=None) - mtk: Optional[date] = Field(default=None) - lot: Optional[str] = Field(default=None, max_length=255) - serial: Optional[str] = Field(default=None, max_length=255) - notes: Optional[str] = Field(default=None, max_length=1000) - - -# Properties to receive on item creation -class ItemCreate(ItemBase): - pass - - -# Properties to receive on item update -class ItemUpdate(ItemBase): - pass - - -# Database model, database table inferred from class name -class Item(ItemBase, table=True): - id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) - owner_id: uuid.UUID = Field( - foreign_key="user.id", nullable=False, ondelete="CASCADE" - ) - owner: User | None = Relationship(back_populates="items") - aiwscode: str = Field(min_length=1, max_length=50) - name: str = Field(min_length=1, max_length=255) - location: str = Field(min_length=1, max_length=255) - expiry: Optional[date] = Field(default=None) - stk: Optional[date] = Field(default=None) - mtk: Optional[date] = Field(default=None) - lot: Optional[str] = Field(default=None, max_length=255) - serial: Optional[str] = Field(default=None, max_length=255) - notes: Optional[str] = Field(default=None, max_length=1000) - - -# Properties to return via API, id is always required -class ItemPublic(ItemBase): - id: uuid.UUID - owner_id: uuid.UUID - - -class ItemsPublic(SQLModel): - data: list[ItemPublic] - count: int - - -# Generic message -class Message(SQLModel): - message: str - - -# JSON payload containing access token -class Token(SQLModel): - access_token: str - token_type: str = "bearer" - - -# Contents of JWT token -class TokenPayload(SQLModel): - sub: str | None = None - - -class NewPassword(SQLModel): - token: str - new_password: str = Field(min_length=8, max_length=40) diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..3183b0c --- /dev/null +++ b/backend/app/models/__init__.py @@ -0,0 +1,13 @@ +from .user import ( + User, + UserCreate, + UserUpdate, + Message, + UpdatePassword, + UserPublic, + UserRegister, + UsersPublic, + UserUpdateMe +) + +from .item import Item diff --git a/backend/app/models/item.py b/backend/app/models/item.py new file mode 100644 index 0000000..26db47e --- /dev/null +++ b/backend/app/models/item.py @@ -0,0 +1,44 @@ + +import uuid +from sqlmodel import Field, SQLModel, Relationship +from typing import Optional +from datetime import date + +# Shared properties +class ItemBase(SQLModel): + title: str = Field(min_length=1, max_length=255) + description: str | None = Field(default=None, max_length=255) + aiwscode: str = Field(min_length=1, max_length=50) + name: str = Field(min_length=1, max_length=255) + location: str = Field(min_length=1, max_length=255) + expiry: Optional[date] = Field(default=None) + stk: Optional[date] = Field(default=None) + mtk: Optional[date] = Field(default=None) + lot: Optional[str] = Field(default=None, max_length=255) + serial: Optional[str] = Field(default=None, max_length=255) + notes: Optional[str] = Field(default=None, max_length=1000) + +# Properties to receive on item creation +class ItemCreate(ItemBase): + pass + +# Properties to receive on item update +class ItemUpdate(ItemBase): + pass + +# Database model, database table inferred from class name +class Item(ItemBase, table=True): + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) + owner_id: uuid.UUID = Field( + foreign_key="user.id", nullable=False, ondelete="CASCADE" + ) + owner: Optional['User'] = Relationship(back_populates="items") # Lazy import for User using a string + +# Properties to return via API, id is always required +class ItemPublic(ItemBase): + id: uuid.UUID + owner_id: uuid.UUID + +class ItemsPublic(SQLModel): + data: list[ItemPublic] + count: int diff --git a/backend/app/models/user.py b/backend/app/models/user.py new file mode 100644 index 0000000..28f4687 --- /dev/null +++ b/backend/app/models/user.py @@ -0,0 +1,63 @@ +import uuid +from pydantic import EmailStr +from sqlmodel import Field, Relationship, SQLModel + +# Shared properties +class UserBase(SQLModel): + email: EmailStr = Field(unique=True, index=True, max_length=255) + is_active: bool = True + is_superuser: bool = False + full_name: str | None = Field(default=None, max_length=255) + +# Properties to receive via API on creation +class UserCreate(UserBase): + password: str = Field(min_length=8, max_length=40) + +class UserRegister(SQLModel): + email: EmailStr = Field(max_length=255) + password: str = Field(min_length=8, max_length=40) + full_name: str | None = Field(default=None, max_length=255) + +# Properties to receive via API on update, all are optional +class UserUpdate(UserBase): + email: EmailStr | None = Field(default=None, max_length=255) # type: ignore + password: str | None = Field(default=None, min_length=8, max_length=40) + +class UserUpdateMe(SQLModel): + full_name: str | None = Field(default=None, max_length=255) + email: EmailStr | None = Field(default=None, max_length=255) + +class UpdatePassword(SQLModel): + current_password: str = Field(min_length=8, max_length=40) + new_password: str = Field(min_length=8, max_length=40) + +# Database model +class User(UserBase, table=True): + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) + hashed_password: str + items: list["Item"] = Relationship(back_populates="owner", cascade_delete=True) # Reference to Item as a string + +# Properties to return via API, id is always required +class UserPublic(UserBase): + id: uuid.UUID + +class UsersPublic(SQLModel): + data: list[UserPublic] + count: int + +# Generic message +class Message(SQLModel): + message: str + +# JSON payload containing access token +class Token(SQLModel): + access_token: str + token_type: str = "bearer" + +# Contents of JWT token +class TokenPayload(SQLModel): + sub: str | None = None + +class NewPassword(SQLModel): + token: str + new_password: str = Field(min_length=8, max_length=40) diff --git a/backend/app/tests/api/routes/test_users.py b/backend/app/tests/api/routes/test_users.py index ba9be65..667d798 100644 --- a/backend/app/tests/api/routes/test_users.py +++ b/backend/app/tests/api/routes/test_users.py @@ -4,7 +4,8 @@ from fastapi.testclient import TestClient from sqlmodel import Session, select -from app import crud +from app.crud import get_user_by_email, create_user + from app.core.config import settings from app.core.security import verify_password from app.models import User, UserCreate diff --git a/backend/app/tests/crud/test_user.py b/backend/app/tests/crud/test_user.py index e9eb4a0..af89fc9 100644 --- a/backend/app/tests/crud/test_user.py +++ b/backend/app/tests/crud/test_user.py @@ -1,9 +1,9 @@ from fastapi.encoders import jsonable_encoder from sqlmodel import Session -from app import crud +from app.crud import create_user, authenticate, update_user from app.core.security import verify_password -from app.models import User, UserCreate, UserUpdate +from app.models.user import User, UserCreate, UserUpdate from app.tests.utils.utils import random_email, random_lower_string diff --git a/backend/app/tests/utils/item.py b/backend/app/tests/utils/item.py index 6e32b3a..e5b4b0b 100644 --- a/backend/app/tests/utils/item.py +++ b/backend/app/tests/utils/item.py @@ -1,7 +1,7 @@ from sqlmodel import Session -from app import crud -from app.models import Item, ItemCreate +from app.crud import create_item +from app.models.item import Item, ItemCreate from app.tests.utils.user import create_random_user from app.tests.utils.utils import random_lower_string @@ -13,4 +13,4 @@ def create_random_item(db: Session) -> Item: title = random_lower_string() description = random_lower_string() item_in = ItemCreate(title=title, description=description) - return crud.create_item(session=db, item_in=item_in, owner_id=owner_id) + return create_item(session=db, item_in=item_in, owner_id=owner_id) diff --git a/backend/app/tests/utils/user.py b/backend/app/tests/utils/user.py index 9c1b073..6bd41e6 100644 --- a/backend/app/tests/utils/user.py +++ b/backend/app/tests/utils/user.py @@ -1,9 +1,9 @@ from fastapi.testclient import TestClient from sqlmodel import Session -from app import crud +from app.crud import create_user, get_user_by_email, update_user from app.core.config import settings -from app.models import User, UserCreate, UserUpdate +from app.models.user import User, UserCreate, UserUpdate from app.tests.utils.utils import random_email, random_lower_string @@ -23,7 +23,7 @@ def create_random_user(db: Session) -> User: email = random_email() password = random_lower_string() user_in = UserCreate(email=email, password=password) - user = crud.create_user(session=db, user_create=user_in) + user = create_user(session=db, user_create=user_in) return user @@ -36,14 +36,14 @@ def authentication_token_from_email( If the user doesn't exist it is created first. """ password = random_lower_string() - user = crud.get_user_by_email(session=db, email=email) + user = get_user_by_email(session=db, email=email) if not user: user_in_create = UserCreate(email=email, password=password) - user = crud.create_user(session=db, user_create=user_in_create) + user = create_user(session=db, user_create=user_in_create) else: user_in_update = UserUpdate(password=password) if not user.id: raise Exception("User id not set") - user = crud.update_user(session=db, db_user=user, user_in=user_in_update) + user = update_user(session=db, db_user=user, user_in=user_in_update) return user_authentication_headers(client=client, email=email, password=password)