diff --git a/backend/.env b/backend/.env index 6d4e283c..accb7936 100644 --- a/backend/.env +++ b/backend/.env @@ -112,7 +112,11 @@ LOG_LEVEL="INFO" DEFAULT_RATE_LIMIT="100/minute" LOGIN_RATE_LIMIT="5/minute" -REGISTER_RATE_LIMIT="3/minute" +REGISTER_RATE_LIMIT="3/hour" +MESSAGE_RATE_LIMIT="30/minute" +SEARCH_RATE_LIMIT="60/minute" +PROJECT_RATE_LIMIT="100/minute" +PASSWORD_RESET_RATE_LIMIT="3/15minutes" # ---------------------------------------------------------- # Security Headers @@ -155,4 +159,5 @@ ENABLE_NOTIFICATIONS=true ENABLE_CHAT=true ENABLE_BUILDER_FLARE=true ENABLE_PROJECTS=true -ENABLE_APPLICATIONS=true \ No newline at end of file +ENABLE_APPLICATIONS=true + diff --git a/backend/.env.example b/backend/.env.example index e81dff0b..c717dadf 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -122,7 +122,15 @@ DEFAULT_RATE_LIMIT=100/minute LOGIN_RATE_LIMIT=5/minute -REGISTER_RATE_LIMIT=3/minute +REGISTER_RATE_LIMIT=3/hour + +MESSAGE_RATE_LIMIT=30/minute + +SEARCH_RATE_LIMIT=60/minute + +PROJECT_RATE_LIMIT=100/minute + +PASSWORD_RESET_RATE_LIMIT=3/15minutes # ---------------------------------------------------------- # Security Headers @@ -178,4 +186,5 @@ ENABLE_BUILDER_FLARE=True ENABLE_PROJECTS=True -ENABLE_APPLICATIONS=True \ No newline at end of file +ENABLE_APPLICATIONS=True + diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 09b8d451..4c97ed91 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -117,7 +117,11 @@ class Settings(BaseSettings): DEFAULT_RATE_LIMIT: str = "100/minute" LOGIN_RATE_LIMIT: str = "5/minute" - REGISTER_RATE_LIMIT: str = "3/minute" + REGISTER_RATE_LIMIT: str = "3/hour" + MESSAGE_RATE_LIMIT: str = "30/minute" + SEARCH_RATE_LIMIT: str = "60/minute" + PROJECT_RATE_LIMIT: str = "100/minute" + PASSWORD_RESET_RATE_LIMIT: str = "3/15minutes" # ========================================================== # Security Headers diff --git a/backend/app/middleware/rate_limit.py b/backend/app/middleware/rate_limit.py index 3cb162b2..b565304b 100644 --- a/backend/app/middleware/rate_limit.py +++ b/backend/app/middleware/rate_limit.py @@ -15,21 +15,17 @@ ) # ------------------------------------------------------------------ -# Common Limits +# Common Limits (all configurable via settings) # ------------------------------------------------------------------ LOGIN_LIMIT = settings.LOGIN_RATE_LIMIT REGISTER_LIMIT = settings.REGISTER_RATE_LIMIT -PASSWORD_RESET_LIMIT = "3/15minutes" +MESSAGE_LIMIT = settings.MESSAGE_RATE_LIMIT -UPLOAD_LIMIT = "10/hour" +SEARCH_LIMIT = settings.SEARCH_RATE_LIMIT -MESSAGE_LIMIT = "60/minute" +PROJECT_LIMIT = settings.PROJECT_RATE_LIMIT -SEARCH_LIMIT = "120/minute" - -PROJECT_LIMIT = "20/hour" - -FLARE_LIMIT = "10/hour" +PASSWORD_RESET_LIMIT = settings.PASSWORD_RESET_RATE_LIMIT diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index bc88e8a6..c6aeaa33 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -4,11 +4,18 @@ APIRouter, Depends, HTTPException, + Request, status, ) from sqlalchemy.orm import Session from app.database.session import get_db +from app.middleware.rate_limit import ( + limiter, + LOGIN_LIMIT, + PASSWORD_RESET_LIMIT, + REGISTER_LIMIT, +) from app.schemas.auth import ( AuthResponse, LoginRequest, @@ -34,7 +41,9 @@ status_code=status.HTTP_201_CREATED, summary="Register a new user", ) +@limiter.limit(REGISTER_LIMIT) def register( + request: Request, payload: RegisterRequest, db: Session = Depends(get_db), ): @@ -59,7 +68,9 @@ def register( response_model=AuthResponse, summary="Login", ) +@limiter.limit(LOGIN_LIMIT) def login( + request: Request, payload: LoginRequest, db: Session = Depends(get_db), ): @@ -121,7 +132,9 @@ def get_current_user_id( response_model=CurrentUserResponse, summary="Current authenticated user", ) +@limiter.limit("30/minute") def me( + request: Request, user_id: str = Depends(get_current_user_id), db: Session = Depends(get_db), ): @@ -141,7 +154,9 @@ def me( response_model=AuthResponse, summary="Refresh JWT", ) +@limiter.limit("10/minute") def refresh( + request: Request, payload: RefreshTokenRequest, db: Session = Depends(get_db), ): @@ -176,7 +191,9 @@ def refresh( response_model=LogoutResponse, summary="Logout", ) +@limiter.limit("10/minute") def logout( + request: Request, user_id: str = Depends(get_current_user_id), db: Session = Depends(get_db), ): @@ -207,7 +224,9 @@ def logout( response_model=SuccessResponse, summary="Change Password", ) +@limiter.limit("5/minute") def change_password( + request: Request, payload: ChangePasswordRequest, user_id: str = Depends(get_current_user_id), db: Session = Depends(get_db), @@ -232,7 +251,9 @@ def change_password( response_model=ForgotPasswordResponse, summary="Forgot Password", ) +@limiter.limit(PASSWORD_RESET_LIMIT) def forgot_password( + request: Request, payload: ForgotPasswordRequest, db: Session = Depends(get_db), ): @@ -254,7 +275,9 @@ def forgot_password( response_model=SuccessResponse, summary="Reset Password", ) +@limiter.limit(PASSWORD_RESET_LIMIT) def reset_password( + request: Request, payload: ResetPasswordRequest, db: Session = Depends(get_db), ): @@ -295,7 +318,9 @@ def reset_password( response_model=VerifyEmailResponse, summary="Verify Email", ) +@limiter.limit("5/minute") def verify_email( + request: Request, payload: VerifyEmailRequest, db: Session = Depends(get_db), ): @@ -326,7 +351,9 @@ def verify_email( response_model=SuccessResponse, summary="Resend Verification Email", ) +@limiter.limit("3/hour") def resend_verification( + request: Request, payload: ResendVerificationEmailRequest, db: Session = Depends(get_db), ): diff --git a/backend/app/routers/messages.py b/backend/app/routers/messages.py index 963cfdea..3f556bb5 100644 --- a/backend/app/routers/messages.py +++ b/backend/app/routers/messages.py @@ -2,11 +2,12 @@ import uuid -from fastapi import APIRouter, Depends, HTTPException, Query, status +from fastapi import APIRouter, Depends, HTTPException, Query, Request, status from sqlalchemy.orm import Session from app.database.session import get_db from app.dependencies import get_current_user +from app.middleware.rate_limit import limiter, MESSAGE_LIMIT, SEARCH_LIMIT from app.models.user import User from app.schemas.message import ( MessageCreate, @@ -26,7 +27,9 @@ response_model=MessageResponse, status_code=status.HTTP_201_CREATED, ) +@limiter.limit(MESSAGE_LIMIT) def send_message( + request: Request, message: MessageCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), @@ -99,7 +102,9 @@ def my_messages( "/search/{conversation_id}", response_model=list[MessageResponse], ) +@limiter.limit(SEARCH_LIMIT) def search_messages( + request: Request, conversation_id: uuid.UUID, keyword: str = Query(...), db: Session = Depends(get_db), @@ -116,7 +121,9 @@ def search_messages( "/{message_id}", response_model=MessageResponse, ) +@limiter.limit("20/minute") def update_message( + request: Request, message_id: uuid.UUID, message: MessageUpdate, db: Session = Depends(get_db), @@ -144,7 +151,9 @@ def update_message( "/{message_id}/restore", response_model=MessageResponse, ) +@limiter.limit("10/minute") def restore_message( + request: Request, message_id: uuid.UUID, db: Session = Depends(get_db), ): @@ -170,7 +179,9 @@ def restore_message( "/{message_id}", response_model=MessageResponse, ) +@limiter.limit("10/minute") def delete_message( + request: Request, message_id: uuid.UUID, db: Session = Depends(get_db), ): diff --git a/backend/app/routers/organizations.py b/backend/app/routers/organizations.py index 788ea212..1a04e1fa 100644 --- a/backend/app/routers/organizations.py +++ b/backend/app/routers/organizations.py @@ -2,11 +2,12 @@ import uuid -from fastapi import APIRouter, Depends, HTTPException, Query, status +from fastapi import APIRouter, Depends, HTTPException, Query, Request, status from sqlalchemy.orm import Session from app.database.session import get_db from app.dependencies import get_current_user +from app.middleware.rate_limit import limiter, SEARCH_LIMIT from app.models.user import User from app.schemas.organization import ( OrganizationCreate, @@ -95,7 +96,9 @@ def get_organization_by_slug( "/", response_model=list[OrganizationResponse], ) +@limiter.limit(SEARCH_LIMIT) def list_organizations( + request: Request, skip: int = Query(0, ge=0), limit: int = Query(20, ge=1, le=100), db: Session = Depends(get_db), @@ -112,7 +115,9 @@ def list_organizations( "/me", response_model=list[OrganizationResponse], ) +@limiter.limit(SEARCH_LIMIT) def my_organizations( + request: Request, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): @@ -127,7 +132,9 @@ def my_organizations( "/search/{keyword}", response_model=list[OrganizationResponse], ) +@limiter.limit(SEARCH_LIMIT) def search_organizations( + request: Request, keyword: str, db: Session = Depends(get_db), ): diff --git a/backend/app/routers/projects.py b/backend/app/routers/projects.py index ff6a4f87..b36dfbfe 100644 --- a/backend/app/routers/projects.py +++ b/backend/app/routers/projects.py @@ -2,11 +2,12 @@ import uuid -from fastapi import APIRouter, Depends, HTTPException, Query, status +from fastapi import APIRouter, Depends, HTTPException, Query, Request, status from sqlalchemy.orm import Session from app.database.session import get_db from app.dependencies import get_current_user +from app.middleware.rate_limit import limiter, PROJECT_LIMIT from app.models.user import User from app.schemas.project import ( ProjectCreate, @@ -26,7 +27,9 @@ response_model=ProjectResponse, status_code=status.HTTP_201_CREATED, ) +@limiter.limit(PROJECT_LIMIT) def create_project( + request: Request, project: ProjectCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), @@ -132,7 +135,9 @@ def my_projects( "/{project_id}", response_model=ProjectResponse, ) +@limiter.limit("30/minute") def update_project( + request: Request, project_id: uuid.UUID, project: ProjectUpdate, db: Session = Depends(get_db), @@ -167,7 +172,9 @@ def update_project( "/{project_id}/archive", response_model=ProjectResponse, ) +@limiter.limit("20/minute") def archive_project( + request: Request, project_id: uuid.UUID, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), @@ -200,7 +207,9 @@ def archive_project( "/{project_id}/restore", response_model=ProjectResponse, ) +@limiter.limit("20/minute") def restore_project( + request: Request, project_id: uuid.UUID, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), @@ -233,7 +242,9 @@ def restore_project( "/{project_id}/feature", response_model=ProjectResponse, ) +@limiter.limit("20/minute") def feature_project( + request: Request, project_id: uuid.UUID, db: Session = Depends(get_db), ): @@ -258,7 +269,9 @@ def feature_project( @router.post( "/{project_id}/star", ) +@limiter.limit("30/minute") def star_project( + request: Request, project_id: uuid.UUID, db: Session = Depends(get_db), ): @@ -287,7 +300,9 @@ def star_project( @router.delete( "/{project_id}/star", ) +@limiter.limit("30/minute") def unstar_project( + request: Request, project_id: uuid.UUID, db: Session = Depends(get_db), ): @@ -317,7 +332,9 @@ def unstar_project( "/{project_id}", status_code=status.HTTP_204_NO_CONTENT, ) +@limiter.limit("20/minute") def delete_project( + request: Request, project_id: uuid.UUID, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), diff --git a/backend/app/routers/skills.py b/backend/app/routers/skills.py index c4912c21..0ab9bd8b 100644 --- a/backend/app/routers/skills.py +++ b/backend/app/routers/skills.py @@ -2,11 +2,12 @@ import uuid -from fastapi import APIRouter, Depends, HTTPException, Query, status +from fastapi import APIRouter, Depends, HTTPException, Query, Request, status from sqlalchemy.orm import Session from app.database.session import get_db from app.dependencies import get_current_user +from app.middleware.rate_limit import limiter, SEARCH_LIMIT from app.models.user import User from app.schemas.skill import ( SkillCreate, @@ -94,7 +95,9 @@ def get_skill_by_slug( "/", response_model=list[SkillResponse], ) +@limiter.limit(SEARCH_LIMIT) def list_skills( + request: Request, skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=500), db: Session = Depends(get_db), @@ -111,7 +114,9 @@ def list_skills( "/search/{keyword}", response_model=list[SkillResponse], ) +@limiter.limit(SEARCH_LIMIT) def search_skills( + request: Request, keyword: str, db: Session = Depends(get_db), ): diff --git a/backend/app/routers/users.py b/backend/app/routers/users.py index bf074c01..922e0634 100644 --- a/backend/app/routers/users.py +++ b/backend/app/routers/users.py @@ -2,11 +2,12 @@ import uuid -from fastapi import APIRouter, Depends, HTTPException, Query, status +from fastapi import APIRouter, Depends, HTTPException, Query, Request, status from sqlalchemy.orm import Session from app.database.session import get_db from app.dependencies import get_current_user +from app.middleware.rate_limit import limiter, SEARCH_LIMIT from app.models.user import User from app.schemas.user import ( UserCreate, @@ -93,7 +94,9 @@ def get_user( "/", response_model=list[UserResponse], ) +@limiter.limit(SEARCH_LIMIT) def list_users( + request: Request, skip: int = Query(0, ge=0), limit: int = Query(20, ge=1, le=100), db: Session = Depends(get_db),