Skip to content

rate limiting #30

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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 master_backend_api/app/applications/auth/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from dependencies.security import get_current_user
from services.redis_service import redis_service

router_auth = APIRouter()
router_auth = APIRouter(prefix="/v1")


@router_auth.post("/login")
Expand Down
59 changes: 16 additions & 43 deletions master_backend_api/app/applications/products/routers.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,29 @@
import uuid
from typing import Annotated

from fastapi import (APIRouter, Body, Depends, HTTPException, Path, UploadFile,
status)
from fastapi import APIRouter, Body, Depends, HTTPException, Path, UploadFile, status
from sqlalchemy.ext.asyncio import AsyncSession

from applications.base_queries import SearchParams
from applications.base_schemas import StatusSuccess
from applications.products.crud import category_manager, product_manager
from applications.products.models import Category, Product
from applications.products.schemas import (NewCategory,
PaginationSavedCategoriesResponse,
SavedCategory, SavedProduct)
from applications.products.schemas import NewCategory, PaginationSavedCategoriesResponse, SavedCategory, SavedProduct
from constants.messages import HelpTexts
from constants.permissions import UserPermissionsEnum
from dependencies.database import get_async_session
from dependencies.file_storage import validate_image, validate_images
from dependencies.security import require_permissions
from storage.s3 import s3_storage

router_categories = APIRouter()
router_products = APIRouter()
router_categories = APIRouter(prefix="/v1")
router_products = APIRouter(prefix="/v1")


@router_categories.post(
"/create",
status_code=status.HTTP_201_CREATED,
dependencies=[
Depends(require_permissions([UserPermissionsEnum.CAN_CREATE_PRODUCT_CATEGORY]))
],
dependencies=[Depends(require_permissions([UserPermissionsEnum.CAN_CREATE_PRODUCT_CATEGORY]))],
)
async def create_category(
new_category: NewCategory,
Expand All @@ -43,23 +38,17 @@ async def create_category(
status_code=status.HTTP_403_FORBIDDEN,
)

saved_category = await category_manager.create_instance(
name=new_category.name, session=session
)
saved_category = await category_manager.create_instance(name=new_category.name, session=session)

return SavedCategory.from_orm(saved_category)


@router_categories.get("/{id}")
async def get_user(
category_id: int = Path(
..., description=HelpTexts.ITEM_PATH_ID_PARAM, ge=1, alias="id"
),
category_id: int = Path(..., description=HelpTexts.ITEM_PATH_ID_PARAM, ge=1, alias="id"),
session: AsyncSession = Depends(get_async_session),
) -> SavedCategory:
user = await category_manager.get_item(
field=Category.id, field_value=category_id, session=session
)
user = await category_manager.get_item(field=Category.id, field_value=category_id, session=session)
if not user:
raise HTTPException(
detail=f"Category with id #{category_id} was not found",
Expand All @@ -85,33 +74,23 @@ async def get_categories(

@router_categories.patch(
"/{id}",
dependencies=[
Depends(require_permissions([UserPermissionsEnum.CAN_CREATE_PRODUCT_CATEGORY]))
],
dependencies=[Depends(require_permissions([UserPermissionsEnum.CAN_CREATE_PRODUCT_CATEGORY]))],
)
async def update_category(
category_data: NewCategory,
category_id: int = Path(
..., description=HelpTexts.ITEM_PATH_ID_PARAM, ge=1, alias="id"
),
category_id: int = Path(..., description=HelpTexts.ITEM_PATH_ID_PARAM, ge=1, alias="id"),
session: AsyncSession = Depends(get_async_session),
) -> SavedCategory:
category_updated = await category_manager.patch_item(
category_id, data_to_patch=category_data, session=session
)
category_updated = await category_manager.patch_item(category_id, data_to_patch=category_data, session=session)
return SavedCategory.from_orm(category_updated)


@router_categories.delete(
"/{id}",
dependencies=[
Depends(require_permissions([UserPermissionsEnum.CAN_CREATE_PRODUCT_CATEGORY]))
],
dependencies=[Depends(require_permissions([UserPermissionsEnum.CAN_CREATE_PRODUCT_CATEGORY]))],
)
async def delete_category(
category_id: int = Path(
..., description=HelpTexts.ITEM_PATH_ID_PARAM, ge=1, alias="id"
),
category_id: int = Path(..., description=HelpTexts.ITEM_PATH_ID_PARAM, ge=1, alias="id"),
session: AsyncSession = Depends(get_async_session),
) -> StatusSuccess:
# todo check deleting with products in it
Expand All @@ -136,18 +115,14 @@ async def create_product(
session: AsyncSession = Depends(get_async_session),
) -> SavedProduct:
# base validation part
category = await category_manager.get_item(
field=Category.id, field_value=categoryId, session=session
)
category = await category_manager.get_item(field=Category.id, field_value=categoryId, session=session)
if not category:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Category #{categoryId} does not exist",
)

product = await product_manager.get_item(
field=Product.title, field_value=title.strip(), session=session
)
product = await product_manager.get_item(field=Product.title, field_value=title.strip(), session=session)
if product:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
Expand All @@ -156,9 +131,7 @@ async def create_product(
# processing
product_uuid = uuid.uuid4().hex
try:
main_image_url = await s3_storage.upload_image(
file=main_image, uuid_id=product_uuid
)
main_image_url = await s3_storage.upload_image(file=main_image, uuid_id=product_uuid)
images_urls = []
for image in images:
image_url = await s3_storage.upload_image(file=image, uuid_id=product_uuid)
Expand Down
44 changes: 16 additions & 28 deletions master_backend_api/app/applications/users/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,21 @@
from applications.base_schemas import StatusSuccess
from applications.users.crud import user_manager
from applications.users.models import User
from applications.users.schemas import (PaginationSavedUserResponse,
PatchDetailedUser, RegisterUserRequest,
SavedUser, UserRegistrationMessage)
from applications.users.schemas import (
PaginationSavedUserResponse,
PatchDetailedUser,
RegisterUserRequest,
SavedUser,
UserRegistrationMessage,
)
from constants.messages import HelpTexts
from constants.permissions import UserPermissionsEnum
from dependencies.database import get_async_session
from dependencies.security import require_permissions
from services.rabbit.constants import SupportedQueues
from services.rabbit.rabbitmq_service import rabbitmq_producer

router_users = APIRouter()
router_users = APIRouter(prefix="/v1")


@router_users.post("/create", status_code=status.HTTP_201_CREATED)
Expand All @@ -27,9 +31,7 @@ async def create_user(
new_user: RegisterUserRequest,
session: AsyncSession = Depends(get_async_session),
) -> SavedUser:
maybe_user: User | None = await user_manager.get_item(
field=User.email, field_value=new_user.email, session=session
)
maybe_user: User | None = await user_manager.get_item(field=User.email, field_value=new_user.email, session=session)
if maybe_user:
raise HTTPException(
detail=f"User {maybe_user.name} with email {maybe_user.email} already exists",
Expand All @@ -48,33 +50,25 @@ async def create_user(
lang="uk",
email=saved_user.email,
base_url=str(request.base_url),
redirect_url=str(
request.url_for("verify_user", user_uuid=saved_user.uuid_data)
),
redirect_url=str(request.url_for("verify_user", user_uuid=saved_user.uuid_data)),
).dict(),
queue_name=SupportedQueues.USER_REGISTRATION,
)
return SavedUser.from_orm(saved_user)


@router_users.get("/verify/{user_uuid}", description="verification via email expected")
async def verify_user(
user_uuid: uuid.UUID, session: AsyncSession = Depends(get_async_session)
) -> StatusSuccess:
async def verify_user(user_uuid: uuid.UUID, session: AsyncSession = Depends(get_async_session)) -> StatusSuccess:
await user_manager.activate_user_account(user_uuid, session)
return StatusSuccess()


@router_users.get("/{id}")
async def get_user(
user_id: int = Path(
..., description=HelpTexts.ITEM_PATH_ID_PARAM, ge=1, alias="id"
),
user_id: int = Path(..., description=HelpTexts.ITEM_PATH_ID_PARAM, ge=1, alias="id"),
session: AsyncSession = Depends(get_async_session),
) -> SavedUser:
user = await user_manager.get_item(
field=User.id, field_value=user_id, session=session
)
user = await user_manager.get_item(field=User.id, field_value=user_id, session=session)
if not user:
raise HTTPException(
detail=f"User with id #{user_id} was not found",
Expand All @@ -101,14 +95,10 @@ async def get_users(
@router_users.patch("/{id}")
async def update_user(
user_data: PatchDetailedUser,
user_id: int = Path(
..., description=HelpTexts.ITEM_PATH_ID_PARAM, ge=1, alias="id"
),
user_id: int = Path(..., description=HelpTexts.ITEM_PATH_ID_PARAM, ge=1, alias="id"),
session: AsyncSession = Depends(get_async_session),
) -> SavedUser:
user_updated = await user_manager.patch_item(
user_id, data_to_patch=user_data, session=session
)
user_updated = await user_manager.patch_item(user_id, data_to_patch=user_data, session=session)
return SavedUser.from_orm(user_updated)


Expand All @@ -117,9 +107,7 @@ async def update_user(
dependencies=[Depends(require_permissions([UserPermissionsEnum.CAN_DELETE_USER]))],
)
async def delete_user(
user_id: int = Path(
..., description=HelpTexts.ITEM_PATH_ID_PARAM, ge=1, alias="id"
),
user_id: int = Path(..., description=HelpTexts.ITEM_PATH_ID_PARAM, ge=1, alias="id"),
session: AsyncSession = Depends(get_async_session),
) -> StatusSuccess:
await user_manager.delete_item(user_id, session=session)
Expand Down
2 changes: 1 addition & 1 deletion master_backend_api/app/dependencies/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@


class SecurityHandler:
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")


async def get_current_user(
Expand Down
21 changes: 16 additions & 5 deletions master_backend_api/app/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from contextlib import asynccontextmanager

from fastapi import FastAPI
from fastapi import FastAPI, Depends
from fastapi.middleware.cors import CORSMiddleware

from applications.admin.admin_handler import add_sqladmin_interface
Expand All @@ -9,6 +9,9 @@
from applications.users.router import router_users
from services.redis_service import redis_service
from settings import settings
import redis.asyncio as redis
from fastapi_limiter import FastAPILimiter
from fastapi_limiter.depends import RateLimiter


@asynccontextmanager
Expand All @@ -19,6 +22,16 @@ async def lifespan(app: FastAPI):
session_gen = get_async_session()
session = await anext(session_gen)
await user_manager.create_admin(session=session)

redis_connection = redis.Redis(
host=settings.REDIS_HOST,
port=settings.REDIS_PORT,
# password=settings.REDIS_PASSWORD,
# username=settings.RE
db=settings.REDIS_DATABASES,
decode_responses=True,
)
await FastAPILimiter.init(redis_connection)
yield


Expand All @@ -44,9 +57,7 @@ def get_application() -> FastAPI:
_app.include_router(router_auth, prefix="/auth", tags=["Users", "Auth"])
_app.include_router(router_users, prefix="/users", tags=["Users"])
_app.include_router(router_products, prefix="/products", tags=["Products"])
_app.include_router(
router_categories, prefix="/categories", tags=["Products", "Categories"]
)
_app.include_router(router_categories, prefix="/categories", tags=["Products", "Categories"])

add_sqladmin_interface(_app)
return _app
Expand All @@ -55,7 +66,7 @@ def get_application() -> FastAPI:
app = get_application()


@app.get("/")
@app.get("/", dependencies=[Depends(RateLimiter(times=5, seconds=100))])
async def index():

await redis_service.set_cache("hjhjhjhjhh55555555555555551111111", 45)
Expand Down
17 changes: 16 additions & 1 deletion master_backend_api/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions master_backend_api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ aio-pika = "^9.5.4"
boto3 = "^1.36.21"
pyhumps = "^3.8.0"
sqladmin = {extras = ["full"], version = "^0.20.1"}
fastapi-limiter = "^0.1.6"

[tool.poetry.group.dev.dependencies]
flake8 = "^7.1.1"
Expand Down