Dama12 commited on
Commit
41d7760
·
1 Parent(s): b3b5fcf

feat: enforce strict CORS and SlowAPI rate limiting

Browse files
app/api/routes/analysis.py CHANGED
@@ -1,7 +1,7 @@
1
  """
2
  Routes analyses K2 Think
3
  """
4
- from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks, Body
5
  from fastapi.responses import Response, FileResponse
6
  from sqlalchemy.orm import Session
7
  from app.dependencies import get_db, get_current_user
@@ -24,6 +24,7 @@ from app.services.export_service import ExportService
24
  from app.services.arxiv_service import ArXivService
25
  from app.services.openalex_service import OpenAlexService
26
  from app.core.logging import logger
 
27
 
28
  router = APIRouter()
29
 
@@ -55,9 +56,11 @@ async def get_user_analysis_history(
55
 
56
 
57
  @router.post("/{project_id}", response_model=dict, status_code=status.HTTP_202_ACCEPTED)
 
58
  async def start_project_analysis(
 
59
  project_id: str,
60
- request: AnalysisRequest,
61
  background_tasks: BackgroundTasks,
62
  current_user = Depends(get_current_user),
63
  db: Session = Depends(get_db)
@@ -81,8 +84,8 @@ async def start_project_analysis(
81
  if not current_user.id.hex.startswith("0000"):
82
  user_repo.deduct_credits(current_user, 50)
83
 
84
- request.user_id = str(current_user.id)
85
- request.user_profile = current_user.research_profile
86
  target_project_id = project_id
87
 
88
  # If project_id is the "nil" UUID from frontend, use/create a real project for history
@@ -94,14 +97,14 @@ async def start_project_analysis(
94
 
95
  analysis = service.create_analysis_run(
96
  project_id=target_project_id,
97
- model_used=request.model or "k2-think-pro"
98
  )
99
 
100
  # START THE ACTUAL PROCESSING
101
  background_tasks.add_task(
102
  service.process_analysis,
103
  str(analysis.id),
104
- request
105
  )
106
 
107
  return {
 
1
  """
2
  Routes analyses K2 Think
3
  """
4
+ from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks, Body, Request
5
  from fastapi.responses import Response, FileResponse
6
  from sqlalchemy.orm import Session
7
  from app.dependencies import get_db, get_current_user
 
24
  from app.services.arxiv_service import ArXivService
25
  from app.services.openalex_service import OpenAlexService
26
  from app.core.logging import logger
27
+ from app.main import limiter
28
 
29
  router = APIRouter()
30
 
 
56
 
57
 
58
  @router.post("/{project_id}", response_model=dict, status_code=status.HTTP_202_ACCEPTED)
59
+ @limiter.limit("2/minute")
60
  async def start_project_analysis(
61
+ request: Request,
62
  project_id: str,
63
+ analysis_request: AnalysisRequest,
64
  background_tasks: BackgroundTasks,
65
  current_user = Depends(get_current_user),
66
  db: Session = Depends(get_db)
 
84
  if not current_user.id.hex.startswith("0000"):
85
  user_repo.deduct_credits(current_user, 50)
86
 
87
+ analysis_request.user_id = str(current_user.id)
88
+ analysis_request.user_profile = current_user.research_profile
89
  target_project_id = project_id
90
 
91
  # If project_id is the "nil" UUID from frontend, use/create a real project for history
 
97
 
98
  analysis = service.create_analysis_run(
99
  project_id=target_project_id,
100
+ model_used=analysis_request.model or "k2-think-pro"
101
  )
102
 
103
  # START THE ACTUAL PROCESSING
104
  background_tasks.add_task(
105
  service.process_analysis,
106
  str(analysis.id),
107
+ analysis_request
108
  )
109
 
110
  return {
app/api/routes/users.py CHANGED
@@ -1,7 +1,7 @@
1
  """
2
  Routes utilisateur
3
  """
4
- from fastapi import APIRouter, Depends, HTTPException, status
5
  from sqlalchemy.orm import Session
6
  from app.dependencies import get_db
7
  from app.services.user_service import UserService
@@ -13,6 +13,7 @@ from datetime import timedelta
13
  from app.core.settings import settings
14
  from app.core.logging import logger
15
  import uuid
 
16
 
17
  router = APIRouter()
18
 
@@ -36,7 +37,8 @@ async def create_user(user: UserCreate, db: Session = Depends(get_db)):
36
  )
37
 
38
  @router.post("/register", response_model=dict, status_code=status.HTTP_201_CREATED)
39
- async def register(user: UserCreate, db: Session = Depends(get_db)):
 
40
  """Enregistre un nouvel utilisateur avec mot de passe"""
41
  logger.info(f"Registering new user: {user.email}")
42
  try:
@@ -72,7 +74,8 @@ async def register(user: UserCreate, db: Session = Depends(get_db)):
72
 
73
 
74
  @router.post("/login", response_model=dict)
75
- async def login(user_login: UserLogin, db: Session = Depends(get_db)):
 
76
  """Connecte un utilisateur par email/password et retourne un JWT"""
77
  logger.info(f"Login attempt for: {user_login.email}")
78
  try:
 
1
  """
2
  Routes utilisateur
3
  """
4
+ from fastapi import APIRouter, Depends, HTTPException, status, Request
5
  from sqlalchemy.orm import Session
6
  from app.dependencies import get_db
7
  from app.services.user_service import UserService
 
13
  from app.core.settings import settings
14
  from app.core.logging import logger
15
  import uuid
16
+ from app.main import limiter
17
 
18
  router = APIRouter()
19
 
 
37
  )
38
 
39
  @router.post("/register", response_model=dict, status_code=status.HTTP_201_CREATED)
40
+ @limiter.limit("3/minute")
41
+ async def register(request: Request, user: UserCreate, db: Session = Depends(get_db)):
42
  """Enregistre un nouvel utilisateur avec mot de passe"""
43
  logger.info(f"Registering new user: {user.email}")
44
  try:
 
74
 
75
 
76
  @router.post("/login", response_model=dict)
77
+ @limiter.limit("5/minute")
78
+ async def login(request: Request, user_login: UserLogin, db: Session = Depends(get_db)):
79
  """Connecte un utilisateur par email/password et retourne un JWT"""
80
  logger.info(f"Login attempt for: {user_login.email}")
81
  try:
app/core/settings.py CHANGED
@@ -39,7 +39,12 @@ class Settings(BaseSettings):
39
  FRONTEND_URL: str = "https://ai-scientific-coinvestigator-ui.vercel.app"
40
 
41
  # CORS — array or comma-separated list of allowed origins
42
- ALLOWED_ORIGINS: List[str] = ["http://localhost:3000", "https://ai-scientific-coinvestigator-ui.vercel.app"]
 
 
 
 
 
43
 
44
  @field_validator("ALLOWED_ORIGINS", mode="before")
45
  @classmethod
 
39
  FRONTEND_URL: str = "https://ai-scientific-coinvestigator-ui.vercel.app"
40
 
41
  # CORS — array or comma-separated list of allowed origins
42
+ ALLOWED_ORIGINS: List[str] = [
43
+ "http://localhost:3000",
44
+ "https://ai-scientific-coinvestigator-ui.vercel.app",
45
+ "https://www.scoinvestigator.com",
46
+ "https://scoinvestigator.com"
47
+ ]
48
 
49
  @field_validator("ALLOWED_ORIGINS", mode="before")
50
  @classmethod
app/main.py CHANGED
@@ -133,6 +133,14 @@ from fastapi.middleware.cors import CORSMiddleware
133
  from starlette.middleware.sessions import SessionMiddleware
134
  from starlette.responses import Response
135
 
 
 
 
 
 
 
 
 
136
  # Create FastAPI app
137
  app = FastAPI(
138
  title=settings.API_TITLE,
@@ -141,6 +149,11 @@ app = FastAPI(
141
  lifespan=lifespan,
142
  )
143
 
 
 
 
 
 
144
  # --- PROXY FIX MIDDLEWARE (Required for Hugging Face / OAuth) ---
145
  @app.middleware("http")
146
  async def fix_proxy_headers(request: Request, call_next):
 
133
  from starlette.middleware.sessions import SessionMiddleware
134
  from starlette.responses import Response
135
 
136
+ from slowapi import Limiter, _rate_limit_exceeded_handler
137
+ from slowapi.util import get_remote_address
138
+ from slowapi.errors import RateLimitExceeded
139
+ from slowapi.middleware import SlowAPIMiddleware
140
+
141
+ # Initialize Rate Limiter based on client IP address
142
+ limiter = Limiter(key_func=get_remote_address)
143
+
144
  # Create FastAPI app
145
  app = FastAPI(
146
  title=settings.API_TITLE,
 
149
  lifespan=lifespan,
150
  )
151
 
152
+ # Attach the Limiter to the app
153
+ app.state.limiter = limiter
154
+ app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
155
+ app.add_middleware(SlowAPIMiddleware)
156
+
157
  # --- PROXY FIX MIDDLEWARE (Required for Hugging Face / OAuth) ---
158
  @app.middleware("http")
159
  async def fix_proxy_headers(request: Request, call_next):
requirements.txt CHANGED
@@ -7,6 +7,7 @@ uvicorn[standard]==0.24.0
7
  python-multipart==0.0.6
8
  pydantic==2.5.0
9
  pydantic-settings==2.1.0
 
10
 
11
  # Database
12
  sqlalchemy==2.0.23
 
7
  python-multipart==0.0.6
8
  pydantic==2.5.0
9
  pydantic-settings==2.1.0
10
+ slowapi==0.1.9
11
 
12
  # Database
13
  sqlalchemy==2.0.23