Hydra-Bolt commited on
Commit
bbfff2e
·
1 Parent(s): 15c5d41
.env.example CHANGED
@@ -20,17 +20,6 @@ SUPABASE_URL="https://your-project.supabase.co"
20
  SUPABASE_SERVICE_KEY="your-supabase-service-role-key"
21
  SUPABASE_ANON_KEY="your-supabase-anon-key"
22
 
23
- # Redis Configuration (for rate limiting)
24
- REDIS_URL="redis://localhost:6379"
25
- REDIS_HOST="localhost"
26
- REDIS_PORT=6379
27
- REDIS_DB=0
28
- REDIS_PASSWORD=""
29
-
30
- # Rate Limiting
31
- RATE_LIMIT_REQUESTS_PER_MINUTE=60
32
- RATE_LIMIT_BURST=10
33
-
34
  # LLM Configuration
35
  GOOGLE_API_KEY="your-google-api-key"
36
  GROQ_API_KEY="your-groq-api-key"
 
20
  SUPABASE_SERVICE_KEY="your-supabase-service-role-key"
21
  SUPABASE_ANON_KEY="your-supabase-anon-key"
22
 
 
 
 
 
 
 
 
 
 
 
 
23
  # LLM Configuration
24
  GOOGLE_API_KEY="your-google-api-key"
25
  GROQ_API_KEY="your-groq-api-key"
AUTHENTICATION.md CHANGED
@@ -162,20 +162,6 @@ curl -X POST "http://localhost:8000/api/v1/extract-narrators" \
162
  - Service role has full access for admin operations
163
  - Automatic user profile creation on signup
164
 
165
- ## Rate Limiting
166
-
167
- ### Default Limits
168
- - **Anonymous users**: 60 requests/minute
169
- - **Authenticated users**: 120 requests/minute
170
- - **Admin users**: 300 requests/minute
171
- - **Burst protection**: 10 requests/second
172
-
173
- ### Rate Limit Headers
174
- Responses include rate limit information:
175
- - `X-RateLimit-Limit`: Request limit
176
- - `X-RateLimit-Remaining`: Remaining requests
177
- - `X-RateLimit-Reset`: Reset time
178
-
179
  ## Analytics and Monitoring
180
 
181
  ### User Analytics
 
162
  - Service role has full access for admin operations
163
  - Automatic user profile creation on signup
164
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  ## Analytics and Monitoring
166
 
167
  ### User Analytics
NEXTJS_INTEGRATION_GUIDE.md CHANGED
@@ -46,8 +46,6 @@ Core Hadith Analysis (all protected except `/api/v1/health` + some analytics):
46
  - `GET /api/v1/analytics/popular-narrators` (public)
47
  - `GET /api/v1/health` (public)
48
 
49
- Rate limit errors will return 429 and headers: `X-RateLimit-*`.
50
-
51
  ---
52
  ## 3. Environment Variables (Next.js)
53
 
@@ -194,11 +192,6 @@ export async function sanadFetch<T>(path: string, init: RequestInit = {}): Promi
194
  headers.set('Content-Type', 'application/json');
195
 
196
  const res = await fetch(`${BASE}${path}`, { ...init, headers, cache: 'no-store' });
197
- if (res.status === 429) {
198
- const limit = res.headers.get('X-RateLimit-Limit');
199
- const reset = res.headers.get('X-RateLimit-Reset');
200
- throw new Error(`Rate limited. Limit=${limit} resets at=${reset}`);
201
- }
202
  if (!res.ok) {
203
  const body = await res.text();
204
  throw new Error(`Sanad API error ${res.status}: ${body}`);
@@ -354,19 +347,17 @@ export function LoginForm() {
354
  ```
355
 
356
  ---
357
- ## 12. Handling Rate Limits & Errors
358
 
359
  Pattern:
360
  ```ts
361
  try {
362
  const res = await sanadFetch('/api/v1/analyze-narrator', { method: 'POST', body: JSON.stringify({ narrator_name }) });
363
  } catch (e: any) {
364
- if (e.message.includes('429')) {
365
- // show friendly message / retry after header
366
- }
367
  }
368
  ```
369
- Consider exponential backoff for bursts and surface `X-RateLimit-Remaining` to show usage.
370
 
371
  ---
372
  ## 13. Logout Flow
@@ -442,7 +433,6 @@ export const config = { matcher: ['/dashboard/:path*', '/analysis/:path*'] };
442
  |---------|-------|-----|
443
  | 401 on every request | Missing Authorization header | Ensure proxy sets header after refresh |
444
  | 401 after refresh | Refresh token expired/blacklisted | Force logout & re-login |
445
- | 429 errors | Rate limit exceeded | Slow down / show user retry time |
446
  | CORS errors (if bypassing proxy) | Direct browser → FastAPI without proper CORS | Always use Next.js proxy or configure CORS on backend |
447
  | Cookie not set in prod | Missing `secure` or domain mismatch | Set correct domain & HTTPS |
448
 
 
46
  - `GET /api/v1/analytics/popular-narrators` (public)
47
  - `GET /api/v1/health` (public)
48
 
 
 
49
  ---
50
  ## 3. Environment Variables (Next.js)
51
 
 
192
  headers.set('Content-Type', 'application/json');
193
 
194
  const res = await fetch(`${BASE}${path}`, { ...init, headers, cache: 'no-store' });
 
 
 
 
 
195
  if (!res.ok) {
196
  const body = await res.text();
197
  throw new Error(`Sanad API error ${res.status}: ${body}`);
 
347
  ```
348
 
349
  ---
350
+ ## 12. Handling Errors
351
 
352
  Pattern:
353
  ```ts
354
  try {
355
  const res = await sanadFetch('/api/v1/analyze-narrator', { method: 'POST', body: JSON.stringify({ narrator_name }) });
356
  } catch (e: any) {
357
+ // Handle errors appropriately
358
+ console.error('API Error:', e.message);
 
359
  }
360
  ```
 
361
 
362
  ---
363
  ## 13. Logout Flow
 
433
  |---------|-------|-----|
434
  | 401 on every request | Missing Authorization header | Ensure proxy sets header after refresh |
435
  | 401 after refresh | Refresh token expired/blacklisted | Force logout & re-login |
 
436
  | CORS errors (if bypassing proxy) | Direct browser → FastAPI without proper CORS | Always use Next.js proxy or configure CORS on backend |
437
  | Cookie not set in prod | Missing `secure` or domain mismatch | Set correct domain & HTTPS |
438
 
app/api/routes.py CHANGED
@@ -22,7 +22,6 @@ from app.db.models import (
22
  )
23
  from app.agent.services import get_llm_service
24
  from app.middleware import get_current_active_user, get_user_ip
25
- from app.middleware.rate_limit import limiter, authenticated_user_limit
26
  from app.services.database import DatabaseService
27
 
28
  router = APIRouter(prefix="/api/v1", tags=["hadith-analysis"])
@@ -34,7 +33,6 @@ router = APIRouter(prefix="/api/v1", tags=["hadith-analysis"])
34
  summary="Extract narrators from hadith text",
35
  description="Analyzes Arabic hadith text and extracts the chain of narrators (sanad)",
36
  )
37
- @authenticated_user_limit()
38
  async def extract_narrators(
39
  request: HadithTextRequest,
40
  http_request: Request,
@@ -123,7 +121,6 @@ async def extract_narrators(
123
  summary="Analyze narrator reliability",
124
  description="Takes a narrator name and generates an AI-powered reliability assessment based on the model's knowledge",
125
  )
126
- @authenticated_user_limit()
127
  async def analyze_narrator(
128
  request: NarratorAnalysisRequest,
129
  http_request: Request,
@@ -196,7 +193,6 @@ async def analyze_narrator(
196
  summary="Analyze narrator chain",
197
  description="Analyzes a complete chain of narrators using enhanced Shamela data + LLM agent",
198
  )
199
- @authenticated_user_limit()
200
  async def analyze_narrator_chain(
201
  request: Request,
202
  narrator_names: List[str] = Body(...),
@@ -273,7 +269,6 @@ async def analyze_narrator_chain(
273
  summary="Extract narrators and analyze chain",
274
  description="Complete workflow: extract narrators from hadith text and analyze the complete chain",
275
  )
276
- @authenticated_user_limit()
277
  async def extract_and_analyze_hadith(
278
  request: HadithTextRequest,
279
  http_request: Request,
@@ -393,7 +388,6 @@ async def health_check():
393
  summary="Get user's extraction history",
394
  description="Get the current user's narrator extraction history",
395
  )
396
- @authenticated_user_limit()
397
  async def get_user_extractions(
398
  request: Request,
399
  current_user: User = Depends(get_current_active_user),
@@ -410,7 +404,6 @@ async def get_user_extractions(
410
  summary="Get user's analysis history",
411
  description="Get the current user's narrator analysis history",
412
  )
413
- @authenticated_user_limit()
414
  async def get_user_analyses(
415
  request: Request,
416
  current_user: User = Depends(get_current_active_user),
 
22
  )
23
  from app.agent.services import get_llm_service
24
  from app.middleware import get_current_active_user, get_user_ip
 
25
  from app.services.database import DatabaseService
26
 
27
  router = APIRouter(prefix="/api/v1", tags=["hadith-analysis"])
 
33
  summary="Extract narrators from hadith text",
34
  description="Analyzes Arabic hadith text and extracts the chain of narrators (sanad)",
35
  )
 
36
  async def extract_narrators(
37
  request: HadithTextRequest,
38
  http_request: Request,
 
121
  summary="Analyze narrator reliability",
122
  description="Takes a narrator name and generates an AI-powered reliability assessment based on the model's knowledge",
123
  )
 
124
  async def analyze_narrator(
125
  request: NarratorAnalysisRequest,
126
  http_request: Request,
 
193
  summary="Analyze narrator chain",
194
  description="Analyzes a complete chain of narrators using enhanced Shamela data + LLM agent",
195
  )
 
196
  async def analyze_narrator_chain(
197
  request: Request,
198
  narrator_names: List[str] = Body(...),
 
269
  summary="Extract narrators and analyze chain",
270
  description="Complete workflow: extract narrators from hadith text and analyze the complete chain",
271
  )
 
272
  async def extract_and_analyze_hadith(
273
  request: HadithTextRequest,
274
  http_request: Request,
 
388
  summary="Get user's extraction history",
389
  description="Get the current user's narrator extraction history",
390
  )
 
391
  async def get_user_extractions(
392
  request: Request,
393
  current_user: User = Depends(get_current_active_user),
 
404
  summary="Get user's analysis history",
405
  description="Get the current user's narrator analysis history",
406
  )
 
407
  async def get_user_analyses(
408
  request: Request,
409
  current_user: User = Depends(get_current_active_user),
app/auth/routes.py CHANGED
@@ -17,7 +17,6 @@ from app.db.models import (
17
  UserRole
18
  )
19
  from app.middleware import auth_middleware, get_user_ip
20
- from app.middleware.rate_limit import limiter, anonymous_user_limit
21
 
22
 
23
  router = APIRouter(prefix="/auth", tags=["authentication"])
@@ -84,7 +83,6 @@ async def create_user_session(
84
 
85
 
86
  @router.post("/register", response_model=AuthResponse)
87
- @anonymous_user_limit()
88
  async def register(request: Request, user_data: RegisterRequest):
89
  """Register a new user."""
90
  supabase = get_supabase_client()
@@ -182,7 +180,6 @@ async def register(request: Request, user_data: RegisterRequest):
182
 
183
 
184
  @router.post("/login", response_model=AuthResponse)
185
- @anonymous_user_limit()
186
  async def login(request: Request, credentials: LoginRequest):
187
  """Authenticate user and return tokens."""
188
  supabase = get_supabase_client()
@@ -253,7 +250,6 @@ async def login(request: Request, credentials: LoginRequest):
253
 
254
 
255
  @router.post("/refresh", response_model=AuthResponse)
256
- @anonymous_user_limit()
257
  async def refresh_token(request: Request, token_data: TokenRefreshRequest):
258
  """Refresh access token using refresh token."""
259
  try:
 
17
  UserRole
18
  )
19
  from app.middleware import auth_middleware, get_user_ip
 
20
 
21
 
22
  router = APIRouter(prefix="/auth", tags=["authentication"])
 
83
 
84
 
85
  @router.post("/register", response_model=AuthResponse)
 
86
  async def register(request: Request, user_data: RegisterRequest):
87
  """Register a new user."""
88
  supabase = get_supabase_client()
 
180
 
181
 
182
  @router.post("/login", response_model=AuthResponse)
 
183
  async def login(request: Request, credentials: LoginRequest):
184
  """Authenticate user and return tokens."""
185
  supabase = get_supabase_client()
 
250
 
251
 
252
  @router.post("/refresh", response_model=AuthResponse)
 
253
  async def refresh_token(request: Request, token_data: TokenRefreshRequest):
254
  """Refresh access token using refresh token."""
255
  try:
app/config/settings.py CHANGED
@@ -31,17 +31,6 @@ class Settings:
31
  SUPABASE_SERVICE_KEY: Optional[str] = os.getenv("SUPABASE_SERVICE_KEY")
32
  SUPABASE_ANON_KEY: Optional[str] = os.getenv("SUPABASE_ANON_KEY")
33
 
34
- # Redis Configuration
35
- REDIS_URL: str = os.getenv("REDIS_URL", "redis://localhost:6379")
36
- REDIS_HOST: str = os.getenv("REDIS_HOST", "localhost")
37
- REDIS_PORT: int = int(os.getenv("REDIS_PORT", "6379"))
38
- REDIS_DB: int = int(os.getenv("REDIS_DB", "0"))
39
- REDIS_PASSWORD: str = os.getenv("REDIS_PASSWORD", "")
40
-
41
- # Rate Limiting
42
- RATE_LIMIT_REQUESTS_PER_MINUTE: int = int(os.getenv("RATE_LIMIT_REQUESTS_PER_MINUTE", "60"))
43
- RATE_LIMIT_BURST: int = int(os.getenv("RATE_LIMIT_BURST", "10"))
44
-
45
  # Database
46
  DATABASE_URL: Optional[str] = os.getenv("DATABASE_URL")
47
 
 
31
  SUPABASE_SERVICE_KEY: Optional[str] = os.getenv("SUPABASE_SERVICE_KEY")
32
  SUPABASE_ANON_KEY: Optional[str] = os.getenv("SUPABASE_ANON_KEY")
33
 
 
 
 
 
 
 
 
 
 
 
 
34
  # Database
35
  DATABASE_URL: Optional[str] = os.getenv("DATABASE_URL")
36
 
app/main.py CHANGED
@@ -8,14 +8,6 @@ from app.config.settings import settings
8
  from app.api.routes import router
9
  from app.auth.routes import router as auth_router
10
 
11
- try:
12
- from slowapi import Limiter, _rate_limit_exceeded_handler
13
- from slowapi.errors import RateLimitExceeded
14
- from app.middleware.rate_limit import limiter
15
- RATE_LIMITING_AVAILABLE = True
16
- except ImportError:
17
- RATE_LIMITING_AVAILABLE = False
18
-
19
 
20
  # Create FastAPI application
21
  app = FastAPI(
@@ -26,23 +18,6 @@ app = FastAPI(
26
  redoc_url="/redoc" if settings.DEBUG else None,
27
  )
28
 
29
- # Add rate limiting if available
30
- if RATE_LIMITING_AVAILABLE:
31
- app.state.limiter = limiter
32
-
33
- @app.exception_handler(RateLimitExceeded)
34
- async def rate_limit_handler(request: Request, exc: RateLimitExceeded):
35
- response = JSONResponse(
36
- status_code=429,
37
- content={
38
- "error": "Rate limit exceeded",
39
- "message": f"Too many requests. Limit: {exc.detail}",
40
- "retry_after": exc.retry_after
41
- }
42
- )
43
- response.headers["Retry-After"] = str(exc.retry_after)
44
- return response
45
-
46
  # Add CORS middleware
47
  app.add_middleware(
48
  CORSMiddleware,
 
8
  from app.api.routes import router
9
  from app.auth.routes import router as auth_router
10
 
 
 
 
 
 
 
 
 
11
 
12
  # Create FastAPI application
13
  app = FastAPI(
 
18
  redoc_url="/redoc" if settings.DEBUG else None,
19
  )
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  # Add CORS middleware
22
  app.add_middleware(
23
  CORSMiddleware,
app/middleware/rate_limit.py DELETED
@@ -1,152 +0,0 @@
1
- from fastapi import Request, HTTPException, status
2
- from slowapi import Limiter, _rate_limit_exceeded_handler
3
- from slowapi.util import get_remote_address
4
- from slowapi.errors import RateLimitExceeded
5
- import redis
6
- from typing import Optional
7
-
8
- from app.config.settings import settings
9
-
10
-
11
- def get_client_ip(request: Request) -> str:
12
- """Get client IP address for rate limiting."""
13
- # Check for forwarded headers first
14
- forwarded = request.headers.get("X-Forwarded-For")
15
- if forwarded:
16
- return forwarded.split(",")[0].strip()
17
-
18
- real_ip = request.headers.get("X-Real-IP")
19
- if real_ip:
20
- return real_ip
21
-
22
- return get_remote_address(request)
23
-
24
-
25
- def get_user_id_from_token(request: Request) -> Optional[str]:
26
- """Extract user ID from JWT token for user-based rate limiting."""
27
- try:
28
- from app.middleware import auth_middleware
29
- auth_header = request.headers.get("Authorization")
30
- if auth_header and auth_header.startswith("Bearer "):
31
- token = auth_header.split(" ")[1]
32
- payload = auth_middleware.verify_token(token)
33
- return payload.get("sub")
34
- except Exception:
35
- pass
36
- return None
37
-
38
-
39
- def rate_limit_key(request: Request) -> str:
40
- """Generate rate limiting key based on user or IP."""
41
- user_id = get_user_id_from_token(request)
42
- if user_id:
43
- return f"user:{user_id}"
44
- return f"ip:{get_client_ip(request)}"
45
-
46
-
47
- # Create Redis client for rate limiting
48
- try:
49
- redis_client = redis.Redis(
50
- host=settings.REDIS_HOST,
51
- port=settings.REDIS_PORT,
52
- db=settings.REDIS_DB,
53
- password=settings.REDIS_PASSWORD if settings.REDIS_PASSWORD else None,
54
- decode_responses=True
55
- )
56
- # Test connection
57
- redis_client.ping()
58
- except Exception:
59
- # Fallback to in-memory storage if Redis is not available
60
- redis_client = None
61
-
62
-
63
- # Create limiter instance
64
- limiter = Limiter(
65
- key_func=rate_limit_key,
66
- storage_uri=settings.REDIS_URL if redis_client else "memory://",
67
- default_limits=[f"{settings.RATE_LIMIT_REQUESTS_PER_MINUTE}/minute"]
68
- )
69
-
70
-
71
- # Custom rate limit exceeded handler
72
- async def custom_rate_limit_handler(request: Request, exc: RateLimitExceeded):
73
- """Custom handler for rate limit exceeded."""
74
- response = HTTPException(
75
- status_code=status.HTTP_429_TOO_MANY_REQUESTS,
76
- detail={
77
- "error": "Rate limit exceeded",
78
- "message": f"Too many requests. Limit: {exc.detail}",
79
- "retry_after": exc.retry_after
80
- }
81
- )
82
- return response
83
-
84
-
85
- # Rate limiting decorators for different tiers
86
- def authenticated_user_limit():
87
- """Rate limit for authenticated users (higher limit)."""
88
- return limiter.limit(f"{settings.RATE_LIMIT_REQUESTS_PER_MINUTE * 2}/minute")
89
-
90
-
91
- def anonymous_user_limit():
92
- """Rate limit for anonymous users (lower limit)."""
93
- return limiter.limit(f"{settings.RATE_LIMIT_REQUESTS_PER_MINUTE}/minute")
94
-
95
-
96
- def admin_user_limit():
97
- """Rate limit for admin users (highest limit)."""
98
- return limiter.limit(f"{settings.RATE_LIMIT_REQUESTS_PER_MINUTE * 5}/minute")
99
-
100
-
101
- def burst_limit():
102
- """Burst protection limit."""
103
- return limiter.limit(f"{settings.RATE_LIMIT_BURST}/second")
104
-
105
-
106
- # Middleware class for more granular control
107
- class RateLimitMiddleware:
108
- """Rate limiting middleware with user-aware limits."""
109
-
110
- def __init__(self):
111
- self.limiter = limiter
112
- self.redis_client = redis_client
113
-
114
- async def check_rate_limit(self, request: Request, limit: str) -> bool:
115
- """Check if request exceeds rate limit."""
116
- try:
117
- key = rate_limit_key(request)
118
-
119
- if self.redis_client:
120
- # Use Redis for rate limiting
121
- current_count = self.redis_client.incr(key)
122
- if current_count == 1:
123
- # Set expiration for new key
124
- self.redis_client.expire(key, 60) # 1 minute window
125
-
126
- # Parse limit (e.g., "60/minute")
127
- limit_count = int(limit.split("/")[0])
128
- if current_count > limit_count:
129
- return False
130
-
131
- return True
132
- except Exception:
133
- # If rate limiting fails, allow the request
134
- return True
135
-
136
- def get_remaining_requests(self, request: Request, limit: str) -> int:
137
- """Get remaining requests for the current window."""
138
- try:
139
- if not self.redis_client:
140
- return 0
141
-
142
- key = rate_limit_key(request)
143
- current_count = self.redis_client.get(key) or 0
144
- limit_count = int(limit.split("/")[0])
145
-
146
- return max(0, limit_count - int(current_count))
147
- except Exception:
148
- return 0
149
-
150
-
151
- # Global instance
152
- rate_limit_middleware = RateLimitMiddleware()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -73,10 +73,6 @@ passlib[bcrypt]==1.7.4
73
  supabase==2.7.4
74
  postgrest==0.16.8
75
 
76
- # Redis and Rate Limiting
77
- redis==5.0.1
78
- slowapi==0.1.9
79
-
80
  # Additional dependencies for enhanced security
81
  cryptography==42.0.5
82
  bcrypt==4.1.2
 
73
  supabase==2.7.4
74
  postgrest==0.16.8
75
 
 
 
 
 
76
  # Additional dependencies for enhanced security
77
  cryptography==42.0.5
78
  bcrypt==4.1.2