Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ COPY mind_matter_api mind_matter_api

COPY .env.example .env

# ================================= DEVELOPMENT ================================
FROM builder AS development
RUN pip install --no-cache -r requirements/dev.txt
EXPOSE 2992
EXPOSE 5000

CMD [ "flask", "run", "--host=0.0.0.0"]


# ================================= PRODUCTION =================================
FROM python:${INSTALL_PYTHON_VERSION}-slim-bullseye as production

Expand Down
5 changes: 5 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ services:
ports:
- "5000:5000"
- "2992:2992"
environment:

FLASK_ENV: development
FLASK_DEBUG: 1

<<: *default_volumes

mind-matter-flask-prod:
Expand Down
12 changes: 10 additions & 2 deletions mind_matter_api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from mind_matter_api.api import register_routes

from mind_matter_api.schemas import UserSchema, UserBodySchema
from mind_matter_api.middleware.rate_limit import rate_limit_middleware
from mind_matter_api.extensions import (
cache,
csrf_protect,
Expand All @@ -32,8 +33,10 @@ def create_app(config_object="mind_matter_api.settings"):
:param config_object: The configuration object to use.
"""
app = Flask(__name__, root_path=os.path.dirname(os.path.abspath(__file__)))
CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=True) # <--- THIS LINE

CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=False) # <

# Rate limiter using flask-caching (memcached )

Comment on lines +38 to +39
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Add cache configuration documentation

The comment mentions memcached, but the actual cache configuration isn't shown in this file.

Add more detailed comments about the cache configuration:

-    # Rate limiter using flask-caching (memcached )
+    # Rate limiter using flask-caching with memcached backend
+    # Configuration is set through CACHE_TYPE and CACHE_MEMCACHED_SERVERS env vars

Let's check how the cache is configured:


🏁 Script executed:

#!/bin/bash
# Check cache configuration in settings or environment files
echo "Checking cache configuration..."
rg "CACHE_" --type py
rg "memcached|memcache" --type py

Length of output: 248


To confirm how the cache is actually configured, let’s inspect the CACHE_TYPE definition in settings.py.


🏁 Script executed:

#!/bin/bash
echo "Locating CACHE_TYPE in settings.py..."
CACHE_LINE=$(rg -n "CACHE_TYPE" mind_matter_api/settings.py | head -n1 | cut -d: -f2)
if [ -n "$CACHE_LINE" ]; then
  START=$((CACHE_LINE-2))
  [ $START -lt 1 ] && START=1
  END=$((CACHE_LINE+2))
  echo "Showing lines $START$END from settings.py:"
  sed -n "${START},${END}p" mind_matter_api/settings.py
else
  echo "CACHE_TYPE not found in settings.py."
fi

Length of output: 358


Let’s directly pull the surrounding lines for CACHE_TYPE to see how it’s configured:


🏁 Script executed:

#!/bin/bash
echo "Showing context around CACHE_TYPE in settings.py..."
rg -n "CACHE_TYPE" mind_matter_api/settings.py -C3

Length of output: 391


Clarify cache backend and configuration source

The comment in app.py still refers to “memcached,” but the actual backend is defined in mind_matter_api/settings.py (defaulting to SimpleCache). Let’s update the inline docs to:

-    # Rate limiter using flask-caching (memcached )
+    # Rate limiter using flask-caching backend (default: SimpleCache)
+    # Backend and options are controlled in mind_matter_api/settings.py:
+    #   • CACHE_TYPE (e.g. "flask_caching.backends.SimpleCache", "flask_caching.backends.MemcachedCache", etc.)
+    #   • When using MemcachedCache, set CACHE_MEMCACHED_SERVERS to your memcached host(s)

• See settings at lines 33–35 in mind_matter_api/settings.py.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Rate limiter using flask-caching (memcached )
# Rate limiter using flask-caching backend (default: SimpleCache)
# Backend and options are controlled in mind_matter_api/settings.py:
# • CACHE_TYPE (e.g. "flask_caching.backends.SimpleCache", "flask_caching.backends.MemcachedCache", etc.)
# • When using MemcachedCache, set CACHE_MEMCACHED_SERVERS to your memcached host(s)

logger = logging.getLogger(__name__)

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

app.before_request(rate_limit_middleware(
limit=5,
window=60,
exclude_paths=["/health"]
))

# Swagger UI
Swagger(
Expand Down
59 changes: 59 additions & 0 deletions mind_matter_api/middleware/rate_limit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import time
from flask import request, jsonify
from mind_matter_api.extensions import cache
from mind_matter_api.utils.auth import decode_auth_token # assume this exists

def get_rate_limit_identity():
# 1. Check for Device ID header (mobile apps)
device_id = request.headers.get("X-Device-ID")
if device_id:
return f"device:{device_id}"

# 2. Check for authenticated user (e.g. JWT bearer)
auth_header = request.headers.get("Authorization")
if auth_header and " " in auth_header:
token_type, token = auth_header.split(" ", 1)
if token_type.lower() == "bearer":
try:
user_id = decode_auth_token(token)
if user_id:
return f"user:{user_id}"
except Exception:
pass

# 3. Fallback to IP address
return f"ip:{request.remote_addr or 'unknown'}"
Comment on lines +6 to +25
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add docstring and improve identity determination

The get_rate_limit_identity function works correctly but lacks documentation and could be improved for IP address handling.

 def get_rate_limit_identity():
+    """
+    Determines the identity of the requester for rate limiting purposes.
+    
+    Attempts to identify the client in the following order:
+    1. From X-Device-ID header (for mobile apps)
+    2. From JWT bearer token in Authorization header
+    3. Fallback to client IP address
+    
+    Returns:
+        str: A string identifier in the format "device:{id}", "user:{id}", or "ip:{address}"
+    """
     # 1. Check for Device ID header (mobile apps)
     device_id = request.headers.get("X-Device-ID")
     if device_id:
         return f"device:{device_id}"

     # 2. Check for authenticated user (e.g. JWT bearer)
     auth_header = request.headers.get("Authorization")
     if auth_header and " " in auth_header:
         token_type, token = auth_header.split(" ", 1)
         if token_type.lower() == "bearer":
             try:
                 user_id = decode_auth_token(token)
                 if user_id:
                     return f"user:{user_id}"
             except Exception:
                 pass

     # 3. Fallback to IP address
-    return f"ip:{request.remote_addr or 'unknown'}"
+    # Use X-Forwarded-For header if behind proxy, fallback to remote_addr
+    ip = request.headers.get('X-Forwarded-For', request.remote_addr or 'unknown')
+    # Extract first IP if X-Forwarded-For contains multiple IPs
+    if ip and ',' in ip:
+        ip = ip.split(',')[0].strip()
+    return f"ip:{ip}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def get_rate_limit_identity():
# 1. Check for Device ID header (mobile apps)
device_id = request.headers.get("X-Device-ID")
if device_id:
return f"device:{device_id}"
# 2. Check for authenticated user (e.g. JWT bearer)
auth_header = request.headers.get("Authorization")
if auth_header and " " in auth_header:
token_type, token = auth_header.split(" ", 1)
if token_type.lower() == "bearer":
try:
user_id = decode_auth_token(token)
if user_id:
return f"user:{user_id}"
except Exception:
pass
# 3. Fallback to IP address
return f"ip:{request.remote_addr or 'unknown'}"
def get_rate_limit_identity():
"""
Determines the identity of the requester for rate limiting purposes.
Attempts to identify the client in the following order:
1. From X-Device-ID header (for mobile apps)
2. From JWT bearer token in Authorization header
3. Fallback to client IP address
Returns:
str: A string identifier in the format "device:{id}", "user:{id}", or "ip:{address}"
"""
# 1. Check for Device ID header (mobile apps)
device_id = request.headers.get("X-Device-ID")
if device_id:
return f"device:{device_id}"
# 2. Check for authenticated user (e.g. JWT bearer)
auth_header = request.headers.get("Authorization")
if auth_header and " " in auth_header:
token_type, token = auth_header.split(" ", 1)
if token_type.lower() == "bearer":
try:
user_id = decode_auth_token(token)
if user_id:
return f"user:{user_id}"
except Exception:
pass
# 3. Fallback to IP address
# Use X-Forwarded-For header if behind proxy, fallback to remote_addr
ip = request.headers.get("X-Forwarded-For", request.remote_addr or 'unknown')
# Extract first IP if X-Forwarded-For contains multiple IPs
if ip and ',' in ip:
ip = ip.split(',')[0].strip()
return f"ip:{ip}"



def rate_limit_middleware(limit: int, window: int, exclude_paths=None, exclude_methods=None):
exclude_paths = exclude_paths or []
exclude_methods = exclude_methods or []

def middleware():
if request.path in exclude_paths or request.method in exclude_methods:
return

identity = get_rate_limit_identity()
key = f"rate-limit:{identity}:{request.endpoint}"
now = time.time()

record = cache.get(key)

if record:
count, first_time = record
if now - first_time < window:
if count >= limit:
retry_after = int(window - (now - first_time) + 1)
response = jsonify({
"error": "Rate limit exceeded",
"retry_after_seconds": retry_after
})
response.headers["Retry-After"] = str(retry_after)
return response, 429
cache.set(key, (count + 1, first_time), timeout=window * 2)
else:
cache.set(key, (1, now), timeout=window * 2)
else:
cache.set(key, (1, now), timeout=window * 2)

return middleware
2 changes: 1 addition & 1 deletion requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ pep8-naming==0.14.1

Flask-Cors==5.0.0

pyjwt==2.10.1

8 changes: 5 additions & 3 deletions requirements/prod.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,14 @@ Flask-Migrate==4.0.7 # Handles database migrations with Alembic

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

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


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

# === Environment Variable Management ===
environs==11.2.1 # Simplifies parsing and managing environment variables
Expand All @@ -52,4 +50,8 @@ pytest-cov==6.0.0
WebTest==3.0.2
flasgger


Flask-Mail

# === Rate Limiting ===
Flask-Caching==2.3.1