Skip to content

Commit 6bb7600

Browse files
authored
Merge pull request #17 from rubybui/rate-limit
add rate limiter using memcache
2 parents 4f286d7 + ad54364 commit 6bb7600

6 files changed

Lines changed: 89 additions & 6 deletions

File tree

Dockerfile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ COPY mind_matter_api mind_matter_api
1212

1313
COPY .env.example .env
1414

15+
# ================================= DEVELOPMENT ================================
16+
FROM builder AS development
17+
RUN pip install --no-cache -r requirements/dev.txt
18+
EXPOSE 2992
19+
EXPOSE 5000
20+
21+
CMD [ "flask", "run", "--host=0.0.0.0"]
22+
23+
1524
# ================================= PRODUCTION =================================
1625
FROM python:${INSTALL_PYTHON_VERSION}-slim-bullseye as production
1726

docker-compose.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ services:
1616
ports:
1717
- "5000:5000"
1818
- "2992:2992"
19+
environment:
20+
21+
FLASK_ENV: development
22+
FLASK_DEBUG: 1
23+
1924
<<: *default_volumes
2025

2126
mind-matter-flask-prod:

mind_matter_api/app.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from mind_matter_api.api import register_routes
1414

1515
from mind_matter_api.schemas import UserSchema, UserBodySchema
16+
from mind_matter_api.middleware.rate_limit import rate_limit_middleware
1617
from mind_matter_api.extensions import (
1718
cache,
1819
csrf_protect,
@@ -32,8 +33,10 @@ def create_app(config_object="mind_matter_api.settings"):
3233
:param config_object: The configuration object to use.
3334
"""
3435
app = Flask(__name__, root_path=os.path.dirname(os.path.abspath(__file__)))
35-
CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=True) # <--- THIS LINE
36-
36+
CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=False) # <
37+
38+
# Rate limiter using flask-caching (memcached )
39+
3740
logger = logging.getLogger(__name__)
3841

3942
# Load configuration
@@ -49,6 +52,11 @@ def create_app(config_object="mind_matter_api.settings"):
4952
register_routes(app)
5053
configure_logger(app)
5154

55+
app.before_request(rate_limit_middleware(
56+
limit=5,
57+
window=60,
58+
exclude_paths=["/health"]
59+
))
5260

5361
# Swagger UI
5462
Swagger(
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import time
2+
from flask import request, jsonify
3+
from mind_matter_api.extensions import cache
4+
from mind_matter_api.utils.auth import decode_auth_token # assume this exists
5+
6+
def get_rate_limit_identity():
7+
# 1. Check for Device ID header (mobile apps)
8+
device_id = request.headers.get("X-Device-ID")
9+
if device_id:
10+
return f"device:{device_id}"
11+
12+
# 2. Check for authenticated user (e.g. JWT bearer)
13+
auth_header = request.headers.get("Authorization")
14+
if auth_header and " " in auth_header:
15+
token_type, token = auth_header.split(" ", 1)
16+
if token_type.lower() == "bearer":
17+
try:
18+
user_id = decode_auth_token(token)
19+
if user_id:
20+
return f"user:{user_id}"
21+
except Exception:
22+
pass
23+
24+
# 3. Fallback to IP address
25+
return f"ip:{request.remote_addr or 'unknown'}"
26+
27+
28+
def rate_limit_middleware(limit: int, window: int, exclude_paths=None, exclude_methods=None):
29+
exclude_paths = exclude_paths or []
30+
exclude_methods = exclude_methods or []
31+
32+
def middleware():
33+
if request.path in exclude_paths or request.method in exclude_methods:
34+
return
35+
36+
identity = get_rate_limit_identity()
37+
key = f"rate-limit:{identity}:{request.endpoint}"
38+
now = time.time()
39+
40+
record = cache.get(key)
41+
42+
if record:
43+
count, first_time = record
44+
if now - first_time < window:
45+
if count >= limit:
46+
retry_after = int(window - (now - first_time) + 1)
47+
response = jsonify({
48+
"error": "Rate limit exceeded",
49+
"retry_after_seconds": retry_after
50+
})
51+
response.headers["Retry-After"] = str(retry_after)
52+
return response, 429
53+
cache.set(key, (count + 1, first_time), timeout=window * 2)
54+
else:
55+
cache.set(key, (1, now), timeout=window * 2)
56+
else:
57+
cache.set(key, (1, now), timeout=window * 2)
58+
59+
return middleware

requirements/dev.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ pep8-naming==0.14.1
1919

2020
Flask-Cors==5.0.0
2121

22-
pyjwt==2.10.1
22+

requirements/prod.txt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,14 @@ Flask-Migrate==4.0.7 # Handles database migrations with Alembic
1717

1818
# === Authentication and User Management ===
1919
Flask-Login==0.6.3 # User session management for Flask
20-
PyJWT==2.8.0 # JSON Web Token implementation
20+
pyjwt==2.10.1
2121

2222
# === Deployment ===
2323
gevent==24.11.1 # Asynchronous networking library for concurrent requests
2424
gunicorn>=19.9.0 # WSGI HTTP server for deploying Python applications
2525
supervisor==4.2.5 # Process manager for running background services
2626

2727

28-
# === Caching ===
29-
Flask-Caching>=2.0.2 # Adds caching support for better performance
3028

3129
# === Environment Variable Management ===
3230
environs==11.2.1 # Simplifies parsing and managing environment variables
@@ -52,4 +50,8 @@ pytest-cov==6.0.0
5250
WebTest==3.0.2
5351
flasgger
5452

53+
5554
Flask-Mail
55+
56+
# === Rate Limiting ===
57+
Flask-Caching==2.3.1

0 commit comments

Comments
 (0)