Skip to content
Closed
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
11 changes: 11 additions & 0 deletions src/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from functools import wraps
from flask import request, jsonify

def require_api_key(f):
@wraps(f)
def decorated_function(*args, **kwargs):
api_key = request.headers.get('X-API-Key')
if api_key != 'your-secret-key': # Replace with actual key or env var
return jsonify({'error': 'API key required'}), 401
return f(*args, **kwargs)
return decorated_function
Comment on lines +1 to +11

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Hard-coded API key and non-constant-time compare — use env var + compare_digest.

Current code ships a plaintext secret and uses direct string equality. Replace with an env-driven secret and constant‑time comparison; also return a proper 401 with WWW-Authenticate.

Apply:

+import os
+import hmac
 from functools import wraps
 from flask import request, jsonify
 
 def require_api_key(f):
     @wraps(f)
     def decorated_function(*args, **kwargs):
-        api_key = request.headers.get('X-API-Key')
-        if api_key != 'your-secret-key':  # Replace with actual key or env var
-            return jsonify({'error': 'API key required'}), 401
+        API_KEY = os.getenv('API_KEY', '')
+        # Prefer X-API-Key; optionally support "Authorization: Api-Key <key>"
+        header_key = request.headers.get('X-API-Key') or (
+            request.headers.get('Authorization').split(' ', 1)[1]
+            if request.headers.get('Authorization', '').startswith('Api-Key ')
+            else None
+        )
+        if not API_KEY or not header_key or not hmac.compare_digest(header_key, API_KEY):
+            resp = jsonify({'error': 'Unauthorized'})
+            resp.status_code = 401
+            resp.headers['WWW-Authenticate'] = 'ApiKey realm="api"'
+            return resp
         return f(*args, **kwargs)
     return decorated_function
📝 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
from functools import wraps
from flask import request, jsonify
def require_api_key(f):
@wraps(f)
def decorated_function(*args, **kwargs):
api_key = request.headers.get('X-API-Key')
if api_key != 'your-secret-key': # Replace with actual key or env var
return jsonify({'error': 'API key required'}), 401
return f(*args, **kwargs)
return decorated_function
import os
import hmac
from functools import wraps
from flask import request, jsonify
def require_api_key(f):
@wraps(f)
def decorated_function(*args, **kwargs):
# Load secret from environment
API_KEY = os.getenv('API_KEY', '')
# Prefer X-API-Key; optionally support "Authorization: Api-Key <key>"
header_key = request.headers.get('X-API-Key') or (
request.headers.get('Authorization').split(' ', 1)[1]
if request.headers.get('Authorization', '').startswith('Api-Key ')
else None
)
# Constant-time compare and ensure key is set
if not API_KEY or not header_key or not hmac.compare_digest(header_key, API_KEY):
resp = jsonify({'error': 'Unauthorized'})
resp.status_code = 401
resp.headers['WWW-Authenticate'] = 'ApiKey realm="api"'
return resp
return f(*args, **kwargs)
return decorated_function
🤖 Prompt for AI Agents
In src/auth.py around lines 1 to 11, the code currently hard-codes the API key
and uses direct string equality; change it to read the secret from an
environment variable (e.g., os.environ.get('API_KEY', '')) and use
hmac.compare_digest() for constant-time comparison to avoid timing attacks;
update imports to include os and hmac, retrieve the provided key from
request.headers.get('X-API-Key', '') (defaulting to empty string), compare with
compare_digest, and on failure return a 401 response that includes the
WWW-Authenticate header (e.g., {'error':'API key required'}, 401,
{'WWW-Authenticate':'API key'}) rather than just a JSON body.

Comment on lines +4 to +11

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider rate-limiting failed auth attempts.

If the rate limiter runs only after successful auth, attackers can spray invalid keys. Either reorder decorators at callsites (rate limit outermost) or add a lightweight per-IP failure counter here.

🤖 Prompt for AI Agents
In src/auth.py around lines 4 to 11, the decorator allows unlimited failed API
key attempts which enables key-spraying; change callsite decorator order so a
rate-limiter is the outermost decorator or add a lightweight per-IP failure
counter here: on a failed API key lookup read client IP (respect
X-Forwarded-For), increment a short-lived counter in a shared store (Redis or an
in-process TTL cache), if the counter exceeds a configured threshold return a
429 or small backoff delay, and reset/decay the counter on successful auth;
ensure counters use a configurable window and avoid blocking legitimate proxied
IPs by correctly handling forwarded headers and make the API key compare use a
secure env var rather than a hardcoded string.

21 changes: 21 additions & 0 deletions src/rate_limiter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from collections import defaultdict
from datetime import datetime, timedelta
from flask import abort, current_app

Comment on lines +1 to +4

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing imports and unused import break runtime (F821) and linting.

wraps and request are used but not imported; current_app is unused.

Apply:

 from collections import defaultdict
 from datetime import datetime, timedelta
-from flask import abort, current_app
+from flask import abort, request
+from functools import wraps
📝 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
from collections import defaultdict
from datetime import datetime, timedelta
from flask import abort, current_app
from collections import defaultdict
from datetime import datetime, timedelta
from flask import abort, request
from functools import wraps
🤖 Prompt for AI Agents
In src/rate_limiter.py lines 1-4, the module currently imports current_app but
uses wraps and request without importing them; remove the unused current_app
import and add the missing imports from functools and flask by importing wraps
from functools and request from flask so the decorator and request references
resolve and lint/runtime errors are eliminated.

# Simple rate limiter using memory (use Redis for production)
rate_limit = defaultdict(list)

def rate_limit(max_requests=100, window_minutes=1):
def decorator(f):
Comment on lines +5 to +9

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Name collision: rate_limit dict overshadowed by rate_limit function → TypeError at runtime.

After defining the function, rate_limit refers to the function, so subscripting it (rate_limit[client_ip]) will blow up.

Apply:

 # Simple rate limiter using memory (use Redis for production)
-rate_limit = defaultdict(list)
+REQUEST_LOG = defaultdict(list)
 
 def rate_limit(max_requests=100, window_minutes=1):
     def decorator(f):
         @wraps(f)
         def decorated_function(*args, **kwargs):
-            client_ip = request.remote_addr
+            # Be proxy-aware
+            forwarded_for = request.headers.get('X-Forwarded-For')
+            client_ip = forwarded_for.split(',')[0].strip() if forwarded_for else (request.remote_addr or 'unknown')
             now = datetime.utcnow()
             window_start = now - timedelta(minutes=window_minutes)
-            rate_limit[client_ip] = [req_time for req_time in rate_limit[client_ip] if req_time > window_start]
-            if len(rate_limit[client_ip]) >= max_requests:
+            REQUEST_LOG[client_ip] = [t for t in REQUEST_LOG[client_ip] if t > window_start]
+            if len(REQUEST_LOG[client_ip]) >= max_requests:
                 abort(429, description="Rate limit exceeded")
-            rate_limit[client_ip].append(now)
+            REQUEST_LOG[client_ip].append(now)
             return f(*args, **kwargs)

Also applies to: 15-19

🧰 Tools
🪛 Ruff (0.12.2)

8-8: Redefinition of unused rate_limit from line 6

(F811)

🤖 Prompt for AI Agents
In src/rate_limiter.py around lines 5-9 (and similarly lines 15-19), the
top-level dict named rate_limit shadows the decorator function of the same name
causing TypeError when the code later subscripts the dict; rename the storage
variable (e.g., rate_limit_store or requests_by_ip) and update all references
inside the module and inside the decorator to use that new name, leaving the
decorator function named rate_limit; ensure imports or other modules using the
storage variable are updated if referenced elsewhere.

@wraps(f)
def decorated_function(*args, **kwargs):
client_ip = request.remote_addr
now = datetime.utcnow()
window_start = now - timedelta(minutes=window_minutes)
rate_limit[client_ip] = [req_time for req_time in rate_limit[client_ip] if req_time > window_start]
if len(rate_limit[client_ip]) >= max_requests:
abort(429, description="Rate limit exceeded")
rate_limit[client_ip].append(now)
return f(*args, **kwargs)
return decorated_function
return decorator
45 changes: 45 additions & 0 deletions src/security/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import JWTError, jwt
from passlib.context import CryptContext
from datetime import datetime, timedelta
from typing import Optional

# Security settings
SECRET_KEY = "your-secret-key" # Should be loaded from config
Comment on lines +7 to +9

Copilot AI Sep 10, 2025

Copy link

Choose a reason for hiding this comment

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

Hardcoded secret key poses a security risk. This should be loaded from environment variables or a secure configuration file to prevent exposure in version control.

Suggested change
# Security settings
SECRET_KEY = "your-secret-key" # Should be loaded from config
import os
# Security settings
SECRET_KEY = os.environ.get("SECRET_KEY")
if not SECRET_KEY:
raise RuntimeError("SECRET_KEY environment variable not set")

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 issue (security): Hardcoded secret key should be loaded securely from configuration.

Use environment variables or a secure config system to prevent exposure of sensitive keys.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

critical

Hardcoding secrets like SECRET_KEY is a critical security vulnerability. The comment indicates it should be loaded from config, but the implementation uses a hardcoded value. This secret must be loaded from a secure source, such as environment variables or a secret management service, to prevent unauthorized access and token forgery in production environments. You should import os and use os.getenv('SECRET_KEY'). The application should fail to start if the key is not set in production.

Suggested change
SECRET_KEY = "your-secret-key" # Should be loaded from config
SECRET_KEY = os.getenv("SECRET_KEY") # Should be loaded from config

ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
security = HTTPBearer()

Comment on lines +13 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify dependencies are declared.

Requires fastapi, python-jose, passlib[bcrypt]. Ensure they’re in requirements/pyproject.


🏁 Script executed:

#!/bin/bash
# Check for required deps
rg -nP '^(fastapi|python-jose|passlib(\[bcrypt\])?)' requirements* pyproject.toml setup.cfg setup.py || true

Length of output: 152


🏁 Script executed:

#!/bin/bash
# Locate dependency declaration files and verify required packages
dep_files=$(find . -maxdepth 2 -type f \( -name 'pyproject.toml' -o -name 'requirements.txt' \))
echo "Found files:"
echo "$dep_files"
for f in $dep_files; do
  echo "---- Checking $f ----"
  grep -nE 'fastapi|python-jose|passlib(\[bcrypt\])?' "$f" || echo "No required deps in $f"
done

Length of output: 265


Declare missing dependencies
python-jose and passlib[bcrypt] aren’t listed in your dependency files—add them to pyproject.toml (under [tool.poetry.dependencies]) or dependencies/requirements.txt alongside fastapi to ensure the authentication code will install correctly.

🤖 Prompt for AI Agents
In src/security/auth.py around lines 13 to 15, the code uses CryptContext and
likely jose for JWT handling but the required packages are not declared; add
"passlib[bcrypt]" and "python-jose" to your project's dependencies (either under
[tool.poetry.dependencies] in pyproject.toml or in dependencies/requirements.txt
alongside fastapi), pin appropriate versions or use compatible ranges, and then
run your package manager (poetry install or pip install -r requirements.txt) to
ensure the authentication code can import and run.

def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password):
return pwd_context.hash(password)

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The ACCESS_TOKEN_EXPIRE_MINUTES constant is defined as 30, but the default expiration time for the token is hardcoded to 15 minutes. This is inconsistent and can be misleading. The default expiration should use the defined constant to ensure consistency.

Suggested change
expire = datetime.utcnow() + timedelta(minutes=15)
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)

to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
Comment on lines +28 to +30

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (code-quality): We've found these issues:

Suggested change
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
to_encode["exp"] = expire
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)


async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
Comment on lines +43 to +44

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (code-quality): Explicitly raise from a previous error (raise-from-previous-error)

Suggested change
except JWTError:
raise credentials_exception
except JWTError as e:
raise credentials_exception from e

return username
31 changes: 31 additions & 0 deletions src/security/rate_limiter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from collections import defaultdict
from datetime import datetime, timedelta

Copilot AI Sep 10, 2025

Copy link

Choose a reason for hiding this comment

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

The datetime and timedelta imports are unused. Only time.time() is used for timestamp operations.

Suggested change
from datetime import datetime, timedelta

Copilot uses AI. Check for mistakes.
from typing import Optional
import time

class RateLimiter:
def __init__(self, max_requests: int = 100, window_seconds: int = 3600):
self.max_requests = max_requests
self.window_seconds = window_seconds
self.requests = defaultdict(list)

def is_allowed(self, identifier: str) -> bool:
now = time.time()
window_start = now - self.window_seconds
self.requests[identifier] = [
timestamp for timestamp in self.requests[identifier]
if timestamp > window_start
]
if len(self.requests[identifier]) < self.max_requests:
self.requests[identifier].append(now)
return True
return False

def get_remaining_requests(self, identifier: str) -> int:
now = time.time()
window_start = now - self.window_seconds
self.requests[identifier] = [
timestamp for timestamp in self.requests[identifier]
if timestamp > window_start
]
return max(0, self.max_requests - len(self.requests[identifier]))
Comment on lines +24 to +31

Copilot AI Sep 10, 2025

Copy link

Choose a reason for hiding this comment

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

The window cleanup logic is duplicated between is_allowed and get_remaining_requests methods. Consider extracting this into a private helper method to reduce code duplication.

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +31

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

This RateLimiter implementation has two issues:

  1. It is not thread-safe. In a concurrent environment, multiple requests for the same identifier can cause a race condition, potentially allowing more requests than max_requests.
  2. Code is duplicated. The logic to clean up old request timestamps is repeated in is_allowed and get_remaining_requests.

Both issues can be solved by introducing a lock (from the threading module) for thread safety and refactoring the cleanup logic into a private method. You will need to add import threading at the top of the file.

class RateLimiter:
    def __init__(self, max_requests: int = 100, window_seconds: int = 3600):
        self.max_requests = max_requests
        self.window_seconds = window_seconds
        self.requests = defaultdict(list)
        self._lock = threading.Lock()

    def _cleanup_requests(self, identifier: str):
        """Removes timestamps outside the current window. Must be called within a lock."""
        now = time.time()
        window_start = now - self.window_seconds
        self.requests[identifier] = [
            timestamp for timestamp in self.requests[identifier]
            if timestamp > window_start
        ]

    def is_allowed(self, identifier: str) -> bool:
        with self._lock:
            self._cleanup_requests(identifier)
            if len(self.requests[identifier]) < self.max_requests:
                self.requests[identifier].append(time.time())
                return True
            return False

    def get_remaining_requests(self, identifier: str) -> int:
        with self._lock:
            self._cleanup_requests(identifier)
            return max(0, self.max_requests - len(self.requests[identifier]))

20 changes: 20 additions & 0 deletions src/unified_api_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from flask import Flask, jsonify
from flask_cors import CORS
from auth import require_api_key
from rate_limiter import rate_limit
Comment on lines +3 to +4

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify import style based on packaging layout.

from auth import ... assumes auth.py is importable from sys.path root. If src is a package, use from src.auth import ... or relative imports.

Run:

Expected:

  • If no src/__init__.py, top-level imports may fail when running as a module; consider PYTHONPATH=src or packaging.
  • If src/__init__.py exists, prefer from src.auth import require_api_key.

🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Is src a package?
fd -a '^__init__\.py$' src || true
# Where are direct imports used?
rg -nP 'from (auth|rate_limiter) import '

Length of output: 637


Use package-relative imports in src
In src/unified_api_server.py (lines 3–4), update to:

- from auth import require_api_key
- from rate_limiter import rate_limit
+ from .auth import require_api_key
+ from .rate_limiter import rate_limit

Apply the same convention across all modules under src/ and adjust imports in deployment/cloud-run/ (e.g. secure_api_server.py:20, debug_api_import.py:85).

📝 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
from auth import require_api_key
from rate_limiter import rate_limit
from .auth import require_api_key
from .rate_limiter import rate_limit
🤖 Prompt for AI Agents
In src/unified_api_server.py around lines 3-4, change the top-level imports to
use package-qualified imports (e.g., import from src.auth and src.rate_limiter
instead of bare module names) and ensure you apply the same package-relative
(package-qualified) convention across all modules under src/; also update any
imports in deployment/cloud-run (for example secure_api_server.py line ~20 and
debug_api_import.py line ~85) to import from the src package so all references
resolve consistently when the project is installed/run as a package.


app = Flask(__name__)
CORS(app) # Enable CORS for all routes

Comment on lines +1 to +8

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

CORS: restrict origins via env; default can remain permissive for dev.

Avoid blanket “allow-all” in prod. Load allowed origins from CORS_ORIGINS.

Apply:

-from flask import Flask, jsonify
+import os
+from flask import Flask, jsonify
 from flask_cors import CORS
 from auth import require_api_key
 from rate_limiter import rate_limit
 
 app = Flask(__name__)
-CORS(app)  # Enable CORS for all routes
+# Restrict in prod via CORS_ORIGINS="https://app.example.com,https://admin.example.com"
+origins_env = os.getenv('CORS_ORIGINS', '').strip()
+if origins_env:
+    origins = [o.strip() for o in origins_env.split(',') if o.strip()]
+    CORS(app, resources={r"/api/*": {"origins": origins}})
+else:
+    CORS(app)  # permissive default for dev
📝 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
from flask import Flask, jsonify
from flask_cors import CORS
from auth import require_api_key
from rate_limiter import rate_limit
app = Flask(__name__)
CORS(app) # Enable CORS for all routes
import os
from flask import Flask, jsonify
from flask_cors import CORS
from auth import require_api_key
from rate_limiter import rate_limit
app = Flask(__name__)
# Restrict CORS origins in prod via CORS_ORIGINS="https://app.example.com,https://admin.example.com"
origins_env = os.getenv('CORS_ORIGINS', '').strip()
if origins_env:
origins = [o.strip() for o in origins_env.split(',') if o.strip()]
CORS(app, resources={r"/api/*": {"origins": origins}})
else:
CORS(app) # permissive default for development
🤖 Prompt for AI Agents
In src/unified_api_server.py lines 1-8, CORS is currently enabled permissively
for all origins; change it to read allowed origins from the CORS_ORIGINS
environment variable and use that list when initializing CORS (fall back to the
existing permissive behavior only when CORS_ORIGINS is not set or when running
in a development environment). Concretely: import os, read
os.getenv("CORS_ORIGINS"), split on commas into a list, trim whitespace, and
pass that list as the origins configuration to CORS when creating the app; keep
a safe default that allows all origins only if no env var is provided (or when
FLASK_ENV indicates development). Ensure the code handles a single origin and
multiple origins uniformly and does not hardcode "*" in production.

@app.route('/api/health')
def health():
return jsonify({'status': 'healthy'})

@app.route('/api/protected', methods=['POST'])
@require_api_key
@rate_limit(max_requests=10, window_minutes=1)
def protected():
Comment on lines +13 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Decorator order: rate-limit should wrap auth to throttle invalid-key hammering.

Current order runs auth first. Swap them so the limiter executes before auth.

Apply:

 @app.route('/api/protected', methods=['POST'])
-@require_api_key
-@rate_limit(max_requests=10, window_minutes=1)
+@rate_limit(max_requests=10, window_minutes=1)
+@require_api_key
 def protected():
     return jsonify({'message': 'Protected endpoint'})
📝 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
@app.route('/api/protected', methods=['POST'])
@require_api_key
@rate_limit(max_requests=10, window_minutes=1)
def protected():
@app.route('/api/protected', methods=['POST'])
@rate_limit(max_requests=10, window_minutes=1)
@require_api_key
def protected():
return jsonify({'message': 'Protected endpoint'})
🤖 Prompt for AI Agents
In src/unified_api_server.py around lines 13 to 16, the decorators are ordered
so authentication runs before rate limiting, allowing repeated invalid-key
attempts to bypass throttling; swap the decorator order so rate_limit is the
outermost decorator (appears above require_api_key) so the rate limiter executes
before authentication, i.e., place @rate_limit(max_requests=10,
window_minutes=1) on the line immediately above @require_api_key.

return jsonify({'message': 'Protected endpoint'})

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
Loading