diff --git a/backend/app/api/routes/login.py b/backend/app/api/routes/login.py index 980c66f..964f5fe 100644 --- a/backend/app/api/routes/login.py +++ b/backend/app/api/routes/login.py @@ -11,7 +11,7 @@ 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.utils import ( +from app.utils.common import ( generate_password_reset_token, generate_reset_password_email, send_email, diff --git a/backend/app/api/routes/users.py b/backend/app/api/routes/users.py index 6429818..71fd85d 100644 --- a/backend/app/api/routes/users.py +++ b/backend/app/api/routes/users.py @@ -24,7 +24,7 @@ from app.models import ( UserUpdate, UserUpdateMe, ) -from app.utils import generate_new_account_email, send_email +from app.utils.common import generate_new_account_email, send_email router = APIRouter(prefix="/users", tags=["users"]) diff --git a/backend/app/api/routes/utils.py b/backend/app/api/routes/utils.py index fc09341..9977c03 100644 --- a/backend/app/api/routes/utils.py +++ b/backend/app/api/routes/utils.py @@ -3,7 +3,7 @@ from pydantic.networks import EmailStr from app.api.deps import get_current_active_superuser from app.models import Message -from app.utils import generate_test_email, send_email +from app.utils.common import generate_test_email, send_email router = APIRouter(prefix="/utils", tags=["utils"]) diff --git a/backend/app/api/v1/api.py b/backend/app/api/v1/api.py deleted file mode 100644 index 7b6461d..0000000 --- a/backend/app/api/v1/api.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi import APIRouter - -from app.api.v1.endpoints import auth, users, utils, todos - -api_router = APIRouter() -api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) -api_router.include_router(users.router, prefix="/users", tags=["users"]) -api_router.include_router(utils.router, prefix="/utils", tags=["utils"]) -api_router.include_router(todos.router, prefix="/todos", tags=["todos"]) \ No newline at end of file diff --git a/backend/app/api/v1/endpoints/todos.py b/backend/app/api/v1/endpoints/todos.py deleted file mode 100644 index 01423ff..0000000 --- a/backend/app/api/v1/endpoints/todos.py +++ /dev/null @@ -1,75 +0,0 @@ -from typing import List -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.orm import Session - -from app.api import deps -from app.schemas.todo import Todo, TodoCreate, TodoUpdate -from app.models.todo import Todo as TodoModel -from app.crud.todo import todo - -router = APIRouter() - -@router.get("/", response_model=List[Todo]) -def read_todos( - db: Session = Depends(deps.get_db), - skip: int = 0, - limit: int = 100, -): - """ - Retrieve todos. - """ - return todo.get_multi(db, skip=skip, limit=limit) - -@router.post("/", response_model=Todo) -def create_todo( - *, - db: Session = Depends(deps.get_db), - todo_in: TodoCreate, -): - """ - Create new todo. - """ - return todo.create(db=db, obj_in=todo_in) - -@router.put("/{id}", response_model=Todo) -def update_todo( - *, - db: Session = Depends(deps.get_db), - id: int, - todo_in: TodoUpdate, -): - """ - Update a todo. - """ - todo_current = todo.get(db=db, id=id) - if not todo_current: - raise HTTPException(status_code=404, detail="Todo not found") - return todo.update(db=db, db_obj=todo_current, obj_in=todo_in) - -@router.get("/{id}", response_model=Todo) -def read_todo( - *, - db: Session = Depends(deps.get_db), - id: int, -): - """ - Get todo by ID. - """ - todo_current = todo.get(db=db, id=id) - if not todo_current: - raise HTTPException(status_code=404, detail="Todo not found") - return todo_current - -@router.delete("/{id}", response_model=Todo) -def delete_todo( - *, - db: Session = Depends(deps.get_db), - id: int, -): - """ - Delete todo. - """ - todo_current = todo.get(db=db, id=id) - if not todo_current: - raise HTTPException(status_code=404, detail="Todo not found") - return todo.remove(db=db, id=id) \ No newline at end of file diff --git a/backend/app/backend_pre_start.py b/backend/app/core/backend_pre_start.py similarity index 100% rename from backend/app/backend_pre_start.py rename to backend/app/core/backend_pre_start.py diff --git a/backend/app/initial_data.py b/backend/app/core/initial_data.py similarity index 100% rename from backend/app/initial_data.py rename to backend/app/core/initial_data.py diff --git a/backend/app/tests_pre_start.py b/backend/app/core/tests_pre_start.py similarity index 100% rename from backend/app/tests_pre_start.py rename to backend/app/core/tests_pre_start.py diff --git a/backend/app/crud/todo.py b/backend/app/crud/todo.py deleted file mode 100644 index 76ef7d1..0000000 --- a/backend/app/crud/todo.py +++ /dev/null @@ -1,23 +0,0 @@ -from typing import List -from fastapi.encoders import jsonable_encoder -from sqlalchemy.orm import Session - -from app.crud.base import CRUDBase -from app.models.todo import Todo -from app.schemas.todo import TodoCreate, TodoUpdate - -class CRUDTodo(CRUDBase[Todo, TodoCreate, TodoUpdate]): - def create(self, db: Session, *, obj_in: TodoCreate) -> Todo: - obj_in_data = jsonable_encoder(obj_in) - db_obj = self.model(**obj_in_data) - db.add(db_obj) - db.commit() - db.refresh(db_obj) - return db_obj - - def get_multi( - self, db: Session, *, skip: int = 0, limit: int = 100 - ) -> List[Todo]: - return db.query(self.model).offset(skip).limit(limit).all() - -todo = CRUDTodo(Todo) \ No newline at end of file diff --git a/backend/app/email-templates/build/new_account.html b/backend/app/email-templates/build/new_account.html deleted file mode 100644 index 3445050..0000000 --- a/backend/app/email-templates/build/new_account.html +++ /dev/null @@ -1,25 +0,0 @@ -
{{ project_name }} - New Account
Welcome to your new account!
Here are your account details:
Username: {{ username }}
Password: {{ password }}
Go to Dashboard

\ No newline at end of file diff --git a/backend/app/email-templates/build/reset_password.html b/backend/app/email-templates/build/reset_password.html deleted file mode 100644 index 4148a5b..0000000 --- a/backend/app/email-templates/build/reset_password.html +++ /dev/null @@ -1,25 +0,0 @@ -
{{ project_name }} - Password Recovery
Hello {{ username }}
We've received a request to reset your password. You can do it by clicking the button below:
Reset password
Or copy and paste the following link into your browser:
This password will expire in {{ valid_hours }} hours.

If you didn't request a password recovery you can disregard this email.
\ No newline at end of file diff --git a/backend/app/email-templates/build/test_email.html b/backend/app/email-templates/build/test_email.html deleted file mode 100644 index 04d0d85..0000000 --- a/backend/app/email-templates/build/test_email.html +++ /dev/null @@ -1,25 +0,0 @@ -
{{ project_name }}
Test email for: {{ email }}

\ No newline at end of file diff --git a/backend/app/email-templates/src/new_account.mjml b/backend/app/email-templates/src/new_account.mjml deleted file mode 100644 index f41a3e3..0000000 --- a/backend/app/email-templates/src/new_account.mjml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - {{ project_name }} - New Account - Welcome to your new account! - Here are your account details: - Username: {{ username }} - Password: {{ password }} - Go to Dashboard - - - - - diff --git a/backend/app/email-templates/src/reset_password.mjml b/backend/app/email-templates/src/reset_password.mjml deleted file mode 100644 index 743f5d7..0000000 --- a/backend/app/email-templates/src/reset_password.mjml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - {{ project_name }} - Password Recovery - Hello {{ username }} - We've received a request to reset your password. You can do it by clicking the button below: - Reset password - Or copy and paste the following link into your browser: - {{ link }} - This password will expire in {{ valid_hours }} hours. - - If you didn't request a password recovery you can disregard this email. - - - - diff --git a/backend/app/email-templates/src/test_email.mjml b/backend/app/email-templates/src/test_email.mjml deleted file mode 100644 index 45d58d6..0000000 --- a/backend/app/email-templates/src/test_email.mjml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - {{ project_name }} - Test email for: {{ email }} - - - - - diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/backend/app/models/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/backend/app/models/auth.py b/backend/app/models/auth.py new file mode 100644 index 0000000..0f12523 --- /dev/null +++ b/backend/app/models/auth.py @@ -0,0 +1,17 @@ +from sqlmodel import SQLModel, Field + + +# 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) \ No newline at end of file diff --git a/backend/app/models/item.py b/backend/app/models/item.py new file mode 100644 index 0000000..68a55d0 --- /dev/null +++ b/backend/app/models/item.py @@ -0,0 +1,40 @@ +import uuid +from sqlmodel import Field, Relationship, SQLModel + +from app.models.user import User + + +# Shared properties +class ItemBase(SQLModel): + title: str = Field(min_length=1, max_length=255) + description: str | None = Field(default=None, max_length=255) + + +# Properties to receive on item creation +class ItemCreate(ItemBase): + pass + + +# Properties to receive on item update +class ItemUpdate(ItemBase): + title: str | None = Field(default=None, min_length=1, max_length=255) # type: ignore + + +# 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") + + +# 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 \ No newline at end of file diff --git a/backend/app/models/todo.py b/backend/app/models/todo.py deleted file mode 100644 index 6e53672..0000000 --- a/backend/app/models/todo.py +++ /dev/null @@ -1,13 +0,0 @@ -from sqlalchemy import Column, Integer, String, Boolean, DateTime -from sqlalchemy.sql import func -from app.db.base_class import Base - -class Todo(Base): - __tablename__ = "todos" - - id = Column(Integer, primary_key=True, index=True) - title = Column(String, index=True) - description = Column(String, nullable=True) - completed = Column(Boolean, default=False) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), onupdate=func.now()) \ No newline at end of file diff --git a/backend/app/models.py b/backend/app/models/user.py similarity index 52% rename from backend/app/models.py rename to backend/app/models/user.py index 2389b4a..33b1412 100644 --- a/backend/app/models.py +++ b/backend/app/models/user.py @@ -1,8 +1,9 @@ import uuid - from pydantic import EmailStr from sqlmodel import Field, Relationship, SQLModel +from app.models.item import Item + # Shared properties class UserBase(SQLModel): @@ -43,7 +44,7 @@ class UpdatePassword(SQLModel): 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) + items: list[Item] = Relationship(back_populates="owner", cascade_delete=True) # Properties to return via API, id is always required @@ -53,61 +54,4 @@ class UserPublic(UserBase): 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) - - -# Properties to receive on item creation -class ItemCreate(ItemBase): - pass - - -# Properties to receive on item update -class ItemUpdate(ItemBase): - title: str | None = Field(default=None, min_length=1, max_length=255) # type: ignore - - -# 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") - - -# 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) + count: int \ No newline at end of file diff --git a/backend/app/repositories/item.py b/backend/app/repositories/item.py new file mode 100644 index 0000000..a28e359 --- /dev/null +++ b/backend/app/repositories/item.py @@ -0,0 +1,12 @@ +import uuid +from sqlmodel import Session + +from app.models import Item, ItemCreate + + +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 \ No newline at end of file diff --git a/backend/app/crud.py b/backend/app/repositories/user.py similarity index 58% rename from backend/app/crud.py rename to backend/app/repositories/user.py index 905bf48..ad9cd52 100644 --- a/backend/app/crud.py +++ b/backend/app/repositories/user.py @@ -1,10 +1,9 @@ -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.core.security import get_password_hash +from app.models import User, UserCreate, UserUpdate def create_user(*, session: Session, user_create: UserCreate) -> User: @@ -34,21 +33,4 @@ def update_user(*, session: Session, db_user: User, user_in: UserUpdate) -> Any: 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: - return 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 + return session_user \ No newline at end of file diff --git a/backend/app/schemas/todo.py b/backend/app/schemas/todo.py deleted file mode 100644 index 2332745..0000000 --- a/backend/app/schemas/todo.py +++ /dev/null @@ -1,22 +0,0 @@ -from pydantic import BaseModel -from datetime import datetime -from typing import Optional - -class TodoBase(BaseModel): - title: str - description: Optional[str] = None - completed: bool = False - -class TodoCreate(TodoBase): - pass - -class TodoUpdate(TodoBase): - pass - -class Todo(TodoBase): - id: int - created_at: datetime - updated_at: Optional[datetime] = None - - class Config: - from_attributes = True \ No newline at end of file diff --git a/backend/app/services/auth.py b/backend/app/services/auth.py new file mode 100644 index 0000000..5c3c52a --- /dev/null +++ b/backend/app/services/auth.py @@ -0,0 +1,13 @@ +from sqlmodel import Session + +from app.core.security import verify_password +from app.repositories.user import get_user_by_email + + +def authenticate(*, session: Session, email: str, password: str) -> User | None: + db_user = get_user_by_email(session=session, email=email) + if not db_user: + return None + if not verify_password(password, db_user.hashed_password): + return None + return db_user \ No newline at end of file diff --git a/backend/app/tests/api/routes/test_login.py b/backend/app/tests/api/routes/test_login.py index 80fa787..a814205 100644 --- a/backend/app/tests/api/routes/test_login.py +++ b/backend/app/tests/api/routes/test_login.py @@ -9,7 +9,7 @@ from app.crud import create_user from app.models import UserCreate from app.tests.utils.user import user_authentication_headers from app.tests.utils.utils import random_email, random_lower_string -from app.utils import generate_password_reset_token +from app.utils.common import generate_password_reset_token def test_get_access_token(client: TestClient) -> None: diff --git a/backend/app/utils.py b/backend/app/utils/common.py similarity index 100% rename from backend/app/utils.py rename to backend/app/utils/common.py diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..7bae48a --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,6 @@ +pydantic>=2.8.2 +sqlmodel>=0.0.14 +python-jose[cryptography]>=3.3.0 +passlib[bcrypt]>=1.7.4 +python-multipart>=0.0.6 +email-validator>=2.1.0.post1 \ No newline at end of file diff --git a/scripts/generate-client.sh b/scripts/generate-client.sh index 1e76864..f688c18 100644 --- a/scripts/generate-client.sh +++ b/scripts/generate-client.sh @@ -9,4 +9,4 @@ cd .. mv openapi.json frontend/ cd frontend npm run generate-client -npx biome format --write ./src/client +npx biome format --write ./src/client \ No newline at end of file