222 lines
5.0 KiB
Python
222 lines
5.0 KiB
Python
"""
|
|
ⒸAngelaMos | 2025
|
|
app.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uvicorn
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import (
|
|
Depends,
|
|
FastAPI,
|
|
Request,
|
|
)
|
|
from fastapi_420 import (
|
|
RateLimiter,
|
|
RateLimiterSettings,
|
|
ScopedRateLimiter,
|
|
FingerprintSettings,
|
|
StorageSettings,
|
|
set_global_limiter,
|
|
RateLimitDep,
|
|
)
|
|
from fastapi_420.types import Algorithm, FingerprintLevel
|
|
|
|
|
|
storage_settings = StorageSettings(
|
|
REDIS_URL = "redis://localhost:6767/0",
|
|
MEMORY_MAX_KEYS = 50_000,
|
|
)
|
|
|
|
fingerprint_settings = FingerprintSettings(
|
|
LEVEL = FingerprintLevel.NORMAL,
|
|
TRUST_X_FORWARDED_FOR = True,
|
|
)
|
|
|
|
settings = RateLimiterSettings(
|
|
ALGORITHM = Algorithm.SLIDING_WINDOW,
|
|
KEY_PREFIX = "myapp",
|
|
INCLUDE_HEADERS = True,
|
|
FAIL_OPEN = True,
|
|
LOG_VIOLATIONS = True,
|
|
storage = storage_settings,
|
|
fingerprint = fingerprint_settings,
|
|
)
|
|
|
|
limiter = RateLimiter(settings = settings)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
await limiter.init()
|
|
set_global_limiter(limiter)
|
|
yield
|
|
await limiter.close()
|
|
|
|
|
|
app = FastAPI(
|
|
title = "Rate Limited API",
|
|
description = "Example API demonstrating fastapi_420 rate limiting",
|
|
lifespan = lifespan,
|
|
)
|
|
|
|
auth_limiter = ScopedRateLimiter(
|
|
prefix = "/auth",
|
|
default_rules = ["5/minute",
|
|
"20/hour"],
|
|
endpoint_rules = {
|
|
"POST:/auth/login": ["3/minute",
|
|
"10/hour"],
|
|
"POST:/auth/register": ["2/minute",
|
|
"5/hour"],
|
|
"POST:/auth/forgot-password": ["2/minute",
|
|
"5/hour"],
|
|
},
|
|
)
|
|
|
|
public_limiter = ScopedRateLimiter(
|
|
prefix = "/public",
|
|
default_rules = ["100/minute",
|
|
"1000/hour"],
|
|
)
|
|
|
|
user_limiter = ScopedRateLimiter(
|
|
prefix = "/api",
|
|
default_rules = ["60/minute",
|
|
"500/hour"],
|
|
endpoint_rules = {
|
|
"POST:/api/upload": ["10/minute"],
|
|
"GET:/api/export": ["5/minute"],
|
|
},
|
|
)
|
|
|
|
|
|
@app.post("/auth/login", dependencies = [Depends(auth_limiter)])
|
|
async def login(username: str, password: str):
|
|
"""
|
|
Strict rate limit: 3/minute, 10/hour
|
|
Prevents brute force attacks
|
|
"""
|
|
return {"token": "fake_jwt_token", "user": username}
|
|
|
|
|
|
@app.post("/auth/register", dependencies = [Depends(auth_limiter)])
|
|
async def register(username: str, email: str, password: str):
|
|
"""
|
|
Very strict: 2/minute, 5/hour
|
|
Prevents mass account creation
|
|
"""
|
|
return {"user_id": 123, "username": username}
|
|
|
|
|
|
@app.post("/auth/forgot-password", dependencies = [Depends(auth_limiter)])
|
|
async def forgot_password(email: str):
|
|
"""
|
|
Very strict: 2/minute, 5/hour
|
|
Prevents email bombing
|
|
"""
|
|
return {"message": "If that email exists, we sent a reset link"}
|
|
|
|
|
|
@app.get("/public/products")
|
|
@limiter.limit("100/minute")
|
|
async def list_products(request: Request):
|
|
"""
|
|
Relaxed limit for public browsing
|
|
Uses decorator style
|
|
"""
|
|
return {"products": [{"id": 1, "name": "Widget"}]}
|
|
|
|
|
|
@app.get("/public/search")
|
|
@limiter.limit("50/minute")
|
|
async def search(request: Request, q: str):
|
|
"""
|
|
Slightly stricter for search (more expensive)
|
|
"""
|
|
return {"results": [], "query": q}
|
|
|
|
|
|
@app.get("/api/me", dependencies = [Depends(user_limiter)])
|
|
async def get_current_user():
|
|
"""
|
|
Standard user endpoint: 60/minute
|
|
"""
|
|
return {"user_id": 123, "username": "johndoe"}
|
|
|
|
|
|
@app.get(
|
|
"/api/dashboard",
|
|
dependencies = [Depends(user_limiter)],
|
|
)
|
|
async def dashboard():
|
|
"""
|
|
Standard user endpoint: 60/minute
|
|
"""
|
|
return {"stats": {"visits": 1000, "conversions": 50}}
|
|
|
|
|
|
@app.post("/api/upload", dependencies = [Depends(user_limiter)])
|
|
async def upload_file():
|
|
"""
|
|
Stricter for uploads: 10/minute
|
|
"""
|
|
return {"file_id": "abc123", "status": "uploaded"}
|
|
|
|
|
|
@app.get("/api/export", dependencies = [Depends(user_limiter)])
|
|
async def export_data():
|
|
"""
|
|
Very strict for exports: 5/minute
|
|
Expensive operation
|
|
"""
|
|
return {"download_url": "/files/export_123.csv"}
|
|
|
|
|
|
@app.get(
|
|
"/api/settings",
|
|
dependencies = [Depends(RateLimitDep("30/minute"))],
|
|
)
|
|
async def get_settings():
|
|
"""
|
|
Using RateLimitDep directly for one off limits
|
|
"""
|
|
return {"theme": "dark", "notifications": True}
|
|
|
|
|
|
@app.get("/admin/users")
|
|
async def admin_list_users():
|
|
"""
|
|
No rate limit for admin endpoints
|
|
In production you would add auth middleware
|
|
"""
|
|
return {"users": [{"id": 1, "username": "admin"}]}
|
|
|
|
|
|
@app.get("/admin/stats")
|
|
async def admin_stats():
|
|
"""
|
|
No rate limit
|
|
"""
|
|
return {"total_users": 10000, "active_today": 500}
|
|
|
|
|
|
@app.get("/health")
|
|
async def health():
|
|
"""
|
|
Health check, excluded from rate limiting by default
|
|
"""
|
|
storage_healthy = await limiter._storage.health_check(
|
|
) if limiter._storage else False
|
|
return {
|
|
"status": "healthy",
|
|
"limiter_initialized": limiter.is_initialized,
|
|
"storage_healthy": storage_healthy,
|
|
}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
uvicorn.run(app, host = "0.0.0.0", port = 8000)
|