Spaces:
Sleeping
Sleeping
feat: enforce strict CORS and SlowAPI rate limiting
Browse files- app/api/routes/analysis.py +9 -6
- app/api/routes/users.py +6 -3
- app/core/settings.py +6 -1
- app/main.py +13 -0
- requirements.txt +1 -0
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 |
-
|
| 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 |
-
|
| 85 |
-
|
| 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=
|
| 98 |
)
|
| 99 |
|
| 100 |
# START THE ACTUAL PROCESSING
|
| 101 |
background_tasks.add_task(
|
| 102 |
service.process_analysis,
|
| 103 |
str(analysis.id),
|
| 104 |
-
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 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] = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|