diff --git a/Dockerfile b/Dockerfile index 5134810..8720065 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index 14f28cb..a04aac8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,11 @@ services: ports: - "5000:5000" - "2992:2992" + environment: + + FLASK_ENV: development + FLASK_DEBUG: 1 + <<: *default_volumes mind-matter-flask-prod: diff --git a/mind_matter_api/app.py b/mind_matter_api/app.py index ce62386..88de66d 100644 --- a/mind_matter_api/app.py +++ b/mind_matter_api/app.py @@ -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, @@ -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 ) + logger = logging.getLogger(__name__) # Load configuration @@ -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( diff --git a/mind_matter_api/middleware/rate_limit.py b/mind_matter_api/middleware/rate_limit.py new file mode 100644 index 0000000..c0fe2b1 --- /dev/null +++ b/mind_matter_api/middleware/rate_limit.py @@ -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'}" + + +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 diff --git a/requirements/dev.txt b/requirements/dev.txt index ecae690..c48c8f3 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -19,4 +19,4 @@ pep8-naming==0.14.1 Flask-Cors==5.0.0 -pyjwt==2.10.1 \ No newline at end of file + diff --git a/requirements/prod.txt b/requirements/prod.txt index 3217bb9..b9d4b02 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -17,7 +17,7 @@ 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 @@ -25,8 +25,6 @@ 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 @@ -52,4 +50,8 @@ pytest-cov==6.0.0 WebTest==3.0.2 flasgger + Flask-Mail + +# === Rate Limiting === +Flask-Caching==2.3.1