Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ STACK_NAME=full-stack-fastapi-project

# Backend
BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,https://localhost,https://localhost:5173,http://localhost.tiangolo.com"
SECRET_KEY=changethis
SECRET_KEY=changethis_changethis_changethis
[email protected]
FIRST_SUPERUSER_PASSWORD=changethis

Expand Down
6 changes: 6 additions & 0 deletions backend/.coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[run]
omit =
*/models/*
*/test_*
*/tests/*
*/__init__.py
4 changes: 1 addition & 3 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
FROM python:3.10

ENV PYTHONUNBUFFERED=1

WORKDIR /app/

# Install uv
Expand Down Expand Up @@ -31,7 +29,7 @@ ENV PYTHONPATH=/app

COPY ./scripts /app/scripts

COPY ./pyproject.toml ./uv.lock ./alembic.ini /app/
COPY ./pyproject.toml ./uv.lock ./alembic.ini ./pytest.ini ./.coveragerc /app/

COPY ./app /app/app

Expand Down
2 changes: 1 addition & 1 deletion backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ $ source .venv/bin/activate

Make sure your editor is using the correct Python virtual environment, with the interpreter at `backend/.venv/bin/python`.

Modify or add SQLModel models for data and SQL tables in `./backend/app/models.py`, API endpoints in `./backend/app/api/`, CRUD (Create, Read, Update, Delete) utils in `./backend/app/crud.py`.
Modify or add SQLModel models for data and SQL tables in `./backend/app/models/*`, API endpoints in `./backend/app/api/`, CRUD (Create, Read, Update, Delete) utils in `./backend/app/crud/*`.

## VS Code

Expand Down
4 changes: 2 additions & 2 deletions backend/app/alembic/env.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import os
from logging.config import fileConfig

from alembic import context
Expand All @@ -18,7 +17,8 @@
# target_metadata = mymodel.Base.metadata
# target_metadata = None

from app.models import SQLModel # noqa
from sqlmodel import SQLModel # noqa
from app.models import * # noqa
from app.core.config import settings # noqa

target_metadata = SQLModel.metadata
Expand Down
70 changes: 70 additions & 0 deletions backend/app/api/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,44 +13,114 @@
from app.core.db import engine
from app.models import TokenPayload, User

# Define OAuth2 scheme for token authentication
reusable_oauth2 = OAuth2PasswordBearer(
tokenUrl=f"{settings.API_V1_STR}/login/access-token"
)


def get_db() -> Generator[Session, None, None]:
"""
Get a database session.

This function creates a new session for the database and yields it.
The session is automatically closed after the function is executed.
It's not protected and can be used by any part of the application.

Args:
None

Returns:
Generator[Session, None, None]: A generator that yields the database session.

Raises:
None

Notes:
This function uses a context manager to ensure proper closure of the session.
"""
with Session(engine) as session:
yield session


# Define dependencies for database session and token
SessionDep = Annotated[Session, Depends(get_db)]
TokenDep = Annotated[str, Depends(reusable_oauth2)]


# Function to get the current user from the token
def get_current_user(session: SessionDep, token: TokenDep) -> User:
"""
Get the current user from the token.

This function decodes the JWT token and retrieves the corresponding user from the database.
It's protected and used as a dependency in routes that require authentication.

Args:
session (SessionDep): The database session.
token (TokenDep): The JWT token.

Returns:
User: The current authenticated user.

Raises:
HTTPException:
- 403: If the token is invalid or expired.
- 404: If the user is not found in the database.
- 400: If the user account is inactive.

Notes:
This function performs token validation and user authentication.
"""
try:
# Decode the JWT token
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
)
token_data = TokenPayload(**payload)
except (InvalidTokenError, ValidationError):
# Raise exception if token is invalid
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Could not validate credentials",
)
# Get the user from the database
user = session.get(User, token_data.sub)
if not user:
# Raise exception if user not found
raise HTTPException(status_code=404, detail="User not found")
if not user.is_active:
# Raise exception if user is inactive
raise HTTPException(status_code=400, detail="Inactive user")
return user


# Define dependency for getting the current user
CurrentUser = Annotated[User, Depends(get_current_user)]


def get_current_active_superuser(current_user: CurrentUser) -> User:
"""
Get the current active superuser.

This function checks if the current user is a superuser.
It's protected and used as a dependency in routes that require superuser privileges.

Args:
current_user (CurrentUser): The current authenticated user.

Returns:
User: The current active superuser.

Raises:
HTTPException:
- 403: If the user is not a superuser.

Notes:
This function is typically used to restrict access to administrative endpoints.
"""
if not current_user.is_superuser:
# Raise exception if user is not a superuser
raise HTTPException(
status_code=403, detail="The user doesn't have enough privileges"
)
Expand Down
8 changes: 8 additions & 0 deletions backend/app/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@

from app.api.routes import items, login, users, utils

# Create a main APIRouter instance
api_router = APIRouter()

# Include the login router without a prefix, tagged as "login"
api_router.include_router(login.router, tags=["login"])

# Include the users router with "/users" prefix, tagged as "users"
api_router.include_router(users.router, prefix="/users", tags=["users"])

# Include the utils router with "/utils" prefix, tagged as "utils"
api_router.include_router(utils.router, prefix="/utils", tags=["utils"])

api_router.include_router(items.router, prefix="/items", tags=["items"])
45 changes: 13 additions & 32 deletions backend/app/api/routes/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
from typing import Any

from fastapi import APIRouter, HTTPException
from sqlmodel import func, select

from app import crud
from app.api.deps import CurrentUser, SessionDep
from app.models import Item, ItemCreate, ItemPublic, ItemsPublic, ItemUpdate, Message
from app.models import ItemCreate, ItemPublic, ItemsPublic, ItemUpdate, Message

router = APIRouter()

Expand All @@ -19,24 +19,13 @@ def read_items(
"""

if current_user.is_superuser:
count_statement = select(func.count()).select_from(Item)
count = session.exec(count_statement).one()
statement = select(Item).offset(skip).limit(limit)
items = session.exec(statement).all()
count = crud.get_item_count(session=session)
items = crud.get_items(session=session, skip=skip, limit=limit)
else:
count_statement = (
select(func.count())
.select_from(Item)
.where(Item.owner_id == current_user.id)
count = crud.get_item_count_by_owner(session=session, owner_id=current_user.id)
items = crud.get_items_by_owner(
session=session, owner_id=current_user.id, skip=skip, limit=limit
)
count = session.exec(count_statement).one()
statement = (
select(Item)
.where(Item.owner_id == current_user.id)
.offset(skip)
.limit(limit)
)
items = session.exec(statement).all()

return ItemsPublic(data=items, count=count)

Expand All @@ -46,7 +35,7 @@ def read_item(session: SessionDep, current_user: CurrentUser, id: uuid.UUID) ->
"""
Get item by ID.
"""
item = session.get(Item, id)
item = crud.get_item(session=session, item_id=id)
if not item:
raise HTTPException(status_code=404, detail="Item not found")
if not current_user.is_superuser and (item.owner_id != current_user.id):
Expand All @@ -61,10 +50,7 @@ def create_item(
"""
Create new item.
"""
item = Item.model_validate(item_in, update={"owner_id": current_user.id})
session.add(item)
session.commit()
session.refresh(item)
item = crud.create_item(session=session, item_in=item_in, owner_id=current_user.id)
return item


Expand All @@ -79,16 +65,12 @@ def update_item(
"""
Update an item.
"""
item = session.get(Item, id)
item = crud.get_item(session=session, item_id=id)
if not item:
raise HTTPException(status_code=404, detail="Item not found")
if not current_user.is_superuser and (item.owner_id != current_user.id):
raise HTTPException(status_code=400, detail="Not enough permissions")
update_dict = item_in.model_dump(exclude_unset=True)
item.sqlmodel_update(update_dict)
session.add(item)
session.commit()
session.refresh(item)
item = crud.update_item(session=session, db_item=item, item_in=item_in)
return item


Expand All @@ -99,11 +81,10 @@ def delete_item(
"""
Delete an item.
"""
item = session.get(Item, id)
item = crud.get_item(session=session, item_id=id)
if not item:
raise HTTPException(status_code=404, detail="Item not found")
if not current_user.is_superuser and (item.owner_id != current_user.id):
raise HTTPException(status_code=400, detail="Not enough permissions")
session.delete(item)
session.commit()
crud.delete_item(session=session, item_id=id)
return Message(message="Item deleted successfully")
Loading
Loading