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

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

Hardcoded API keys are a critical security risk. The key should be loaded from a secure source like an environment variable, not stored in the code. This also makes the key configurable for different environments (dev, staging, prod) without code changes.

Please ensure import os is added at the top of the file.

Suggested change
if api_key != 'your-secret-key': # Replace with actual key or env var
secret_key = os.environ.get('API_KEY')
if not secret_key or api_key != secret_key:

return jsonify({'error': 'API key required'}), 401
return f(*args, **kwargs)
return decorated_function
128 changes: 128 additions & 0 deletions src/emotion_endpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
from flask import Blueprint, request, jsonify
from flask_restx import Api, Resource, fields
import logging
from typing import Dict, Any, Optional
import time

logger = logging.getLogger(__name__)

# Create emotion endpoint blueprint
emotion_bp = Blueprint('emotion', __name__, url_prefix='/api/analyze')

# Create API namespace
api = Api(emotion_bp, doc=False, title='Emotion Analysis API', version='1.0')

# Define request/response models
emotion_request = api.model('EmotionRequest', {
'text': fields.String(required=True, description='Text to analyze for emotions'),
'generate_summary': fields.Boolean(required=False, default=False, description='Generate text summary')
})

emotion_response = api.model('EmotionResponse', {
'emotions': fields.List(fields.String, description='Detected emotions'),
'confidence_scores': fields.List(fields.Float, description='Confidence scores for each emotion'),
'summary': fields.String(description='Text summary (if requested)'),
'processing_time': fields.Float(description='Processing time in seconds'),
'text_length': fields.Integer(description='Length of input text'),
'timestamp': fields.String(description='Analysis timestamp')
})

@api.route('/journal')
class EmotionAnalysis(Resource):
"""Emotion analysis endpoint for journal entries."""

@api.expect(emotion_request)
@api.marshal_with(emotion_response)
def post(self):
"""Analyze emotions in journal text."""
try:
start_time = time.time()

# Get request data
data = request.get_json()
if not data or 'text' not in data:
return {'error': 'Text is required'}, 400

text = data['text']
generate_summary = data.get('generate_summary', False)

# Validate input
if not isinstance(text, str) or len(text.strip()) == 0:
return {'error': 'Text must be a non-empty string'}, 400

if len(text) > 10000: # 10k character limit
return {'error': 'Text too long (max 10,000 characters)'}, 400
Comment on lines +42 to +54

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 manual validation for the request data is redundant. Flask-RESTX's @api.expect(emotion_request) decorator already handles the validation of required fields, data types, and other constraints defined in the emotion_request model. Removing this manual validation code will make the endpoint logic cleaner and rely on the framework's validation, which provides consistent error responses. You can add length validation to your emotion_request model, e.g., fields.String(..., min_length=1, max_length=10000).

Suggested change
data = request.get_json()
if not data or 'text' not in data:
return {'error': 'Text is required'}, 400
text = data['text']
generate_summary = data.get('generate_summary', False)
# Validate input
if not isinstance(text, str) or len(text.strip()) == 0:
return {'error': 'Text must be a non-empty string'}, 400
if len(text) > 10000: # 10k character limit
return {'error': 'Text too long (max 10,000 characters)'}, 400
# Get request data - validation is handled by @api.expect
data = request.get_json()
text = data['text']
generate_summary = data.get('generate_summary', False)


# Mock emotion analysis (replace with actual model integration)
emotions, confidence_scores = self._analyze_emotions(text)

# Generate summary if requested
summary = None
if generate_summary:
summary = self._generate_summary(text)

processing_time = time.time() - start_time

# Prepare response
response = {
'emotions': emotions,
'confidence_scores': confidence_scores,
'summary': summary,
'processing_time': round(processing_time, 3),
'text_length': len(text),
'timestamp': time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime())
}

logger.info(f"Emotion analysis completed: {len(emotions)} emotions detected in {processing_time:.3f}s")
return response, 200

except Exception as e:
logger.error(f"Emotion analysis failed: {e}")
return {'error': 'Emotion analysis failed'}, 500

def _analyze_emotions(self, text: str) -> tuple[list[str], list[float]]:
"""Analyze emotions in text (mock implementation)."""
# Mock emotion detection - replace with actual SAMO BERT model
emotions = []
confidence_scores = []

# Simple keyword-based emotion detection for demo
text_lower = text.lower()

emotion_keywords = {
'joy': ['happy', 'excited', 'joyful', 'cheerful', 'delighted'],
'sadness': ['sad', 'depressed', 'melancholy', 'gloomy', 'sorrowful'],
'anger': ['angry', 'mad', 'furious', 'irritated', 'annoyed'],
'fear': ['afraid', 'scared', 'terrified', 'anxious', 'worried'],
'surprise': ['surprised', 'shocked', 'amazed', 'astonished'],
'disgust': ['disgusted', 'revolted', 'repulsed', 'sickened']
}

for emotion, keywords in emotion_keywords.items():
confidence = sum(1 for keyword in keywords if keyword in text_lower) / len(keywords)
if confidence > 0.1: # Threshold for detection
emotions.append(emotion)
confidence_scores.append(min(confidence * 2, 1.0)) # Scale to 0-1

# If no emotions detected, add neutral
if not emotions:
emotions = ['neutral']
confidence_scores = [0.5]

return emotions, confidence_scores

def _generate_summary(self, text: str) -> str:
"""Generate text summary (mock implementation)."""
# Mock summarization - replace with actual T5 model
words = text.split()
if len(words) <= 20:
return text

# Simple extractive summary (first 20 words)
summary_words = words[:20]
return ' '.join(summary_words) + '...'

def register_emotion_endpoints(app):
"""Register emotion endpoints with the Flask app."""
app.register_blueprint(emotion_bp)
logger.info("Emotion endpoints registered: /api/analyze/journal")
83 changes: 83 additions & 0 deletions src/health_endpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from flask import Blueprint, jsonify, request
from health_monitor import health_monitor
import logging

logger = logging.getLogger(__name__)

# Create health endpoints blueprint
health_bp = Blueprint('health', __name__, url_prefix='/api/health')

@health_bp.route('/', methods=['GET'])
def health_check():
"""Basic health check endpoint."""
try:
summary = health_monitor.get_health_summary()
status_code = 200 if summary["status"] in ["healthy", "warning"] else 503
return jsonify(summary), status_code
except Exception as e:
logger.error(f"Health check failed: {e}")
return jsonify({
"status": "error",
"message": "Health check failed"
}), 500

@health_bp.route('/detailed', methods=['GET'])
def detailed_health():
"""Detailed health check with system metrics."""
try:
health_data = health_monitor.get_system_health()
status_code = 200 if health_data["status"] in ["healthy", "warning"] else 503
return jsonify(health_data), status_code
Comment on lines +25 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.

high

Calling health_monitor.get_system_health() on every request to /detailed, /ready, and /metrics introduces a significant performance bottleneck. The get_system_health() function includes a 1-second blocking call (psutil.cpu_percent(interval=1)), making each of these endpoint calls slow. Frequent calls from monitoring systems could degrade the application's overall performance.

A better approach is to cache the health status. You could use a background thread to periodically update the health data (e.g., every 5-10 seconds) and have the endpoints serve the cached data. This would make the health checks fast and non-blocking.

except Exception as e:
logger.error(f"Detailed health check failed: {e}")
return jsonify({
"status": "error",
"message": "Detailed health check failed"
}), 500

@health_bp.route('/ready', methods=['GET'])
def readiness_check():
"""Kubernetes readiness probe endpoint."""
try:
health_data = health_monitor.get_system_health()
if health_data["status"] in ["healthy", "warning"]:
return jsonify({"ready": True}), 200
else:
return jsonify({"ready": False, "reason": health_data["status"]}), 503
except Exception as e:
logger.error(f"Readiness check failed: {e}")
return jsonify({"ready": False, "reason": "error"}), 503

@health_bp.route('/live', methods=['GET'])
def liveness_check():
"""Kubernetes liveness probe endpoint."""
try:
# Simple liveness check - just verify the service is responding
return jsonify({"alive": True}), 200
except Exception as e:
logger.error(f"Liveness check failed: {e}")
return jsonify({"alive": False}), 500

@health_bp.route('/metrics', methods=['GET'])
def health_metrics():
"""Health metrics endpoint for monitoring systems."""
try:
health_data = health_monitor.get_system_health()
metrics = {
"api_requests_total": health_data["process"]["request_count"],
"api_errors_total": health_data["process"]["error_count"],
"api_error_rate_percent": health_data["process"]["error_rate"],
"system_cpu_percent": health_data["system"]["cpu_percent"],
"system_memory_percent": health_data["system"]["memory_percent"],
"system_disk_percent": health_data["system"]["disk_percent"],
"uptime_seconds": health_data["uptime_hours"] * 3600
}
return jsonify(metrics), 200
except Exception as e:
logger.error(f"Metrics collection failed: {e}")
return jsonify({"error": "Metrics collection failed"}), 500

def register_health_endpoints(app):
"""Register health endpoints with the Flask app."""
app.register_blueprint(health_bp)
logger.info("Health endpoints registered: /api/health/*")
90 changes: 90 additions & 0 deletions src/health_monitor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import time
import psutil
from datetime import datetime
from typing import Dict, Any, Optional
import logging

logger = logging.getLogger(__name__)

class HealthMonitor:
"""Health monitoring system for API endpoints and system resources."""

def __init__(self):
self.start_time = time.time()
self.request_count = 0
self.error_count = 0
self.last_health_check = None

def get_system_health(self) -> Dict[str, Any]:
"""Get comprehensive system health metrics."""
try:
# System resource usage
cpu_percent = psutil.cpu_percent(interval=1)
memory = psutil.virtual_memory()
disk = psutil.disk_usage('/')

# Process information
process = psutil.Process()
process_memory = process.memory_info().rss / 1024 / 1024 # MB

# Uptime calculation
uptime_seconds = time.time() - self.start_time
uptime_hours = uptime_seconds / 3600

health_data = {
"status": "healthy",
"timestamp": datetime.utcnow().isoformat(),
"uptime_hours": round(uptime_hours, 2),
"system": {
"cpu_percent": cpu_percent,
"memory_percent": memory.percent,
"memory_available_gb": round(memory.available / 1024**3, 2),
"disk_percent": disk.percent,
"disk_free_gb": round(disk.free / 1024**3, 2)
},
"process": {
"memory_mb": round(process_memory, 2),
"request_count": self.request_count,
"error_count": self.error_count,
"error_rate": round(self.error_count / max(self.request_count, 1) * 100, 2)
},
"last_health_check": self.last_health_check
}

# Determine overall health status
if cpu_percent > 90 or memory.percent > 90 or disk.percent > 90:
health_data["status"] = "warning"
if cpu_percent > 95 or memory.percent > 95 or disk.percent > 95:
health_data["status"] = "critical"
if self.error_count > 0 and self.error_count / max(self.request_count, 1) > 0.1:
health_data["status"] = "degraded"
Comment on lines +55 to +60

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

The logic for determining the overall health status has a potential flaw in prioritization. The status is updated sequentially. If a condition for 'critical' is met, the status is set to 'critical'. However, if a subsequent condition for 'degraded' is also met, it will overwrite 'critical' with 'degraded'. 'critical' should likely have the highest precedence. Consider reordering the checks from most to least severe to ensure the most critical status is always reported.

Suggested change
if cpu_percent > 90 or memory.percent > 90 or disk.percent > 90:
health_data["status"] = "warning"
if cpu_percent > 95 or memory.percent > 95 or disk.percent > 95:
health_data["status"] = "critical"
if self.error_count > 0 and self.error_count / max(self.request_count, 1) > 0.1:
health_data["status"] = "degraded"
# Determine overall health status (most to least critical)
if cpu_percent > 95 or memory.percent > 95 or disk.percent > 95:
health_data["status"] = "critical"
elif self.error_count > 0 and self.error_count / max(self.request_count, 1) > 0.1:
health_data["status"] = "degraded"
elif cpu_percent > 90 or memory.percent > 90 or disk.percent > 90:
health_data["status"] = "warning"


self.last_health_check = health_data["timestamp"]
return health_data

except Exception as e:
logger.error(f"Health check failed: {e}")
return {
"status": "error",
"timestamp": datetime.utcnow().isoformat(),
"error": str(e)
}

def record_request(self, success: bool = True):
"""Record a request for health monitoring."""
self.request_count += 1
if not success:
self.error_count += 1

def get_health_summary(self) -> Dict[str, Any]:
"""Get a simplified health summary for quick checks."""
health = self.get_system_health()
return {
"status": health["status"],
"uptime_hours": health["uptime_hours"],
"request_count": health["process"]["request_count"],
"error_rate": health["process"]["error_rate"]
}

# Global health monitor instance
health_monitor = HealthMonitor()
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

# 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):
@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
Comment on lines +1 to +21

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

This rate limiter implementation has several critical issues that will cause it to fail at runtime:

  1. Missing Imports: The code uses wraps and request without importing them from functools and flask respectively. This will raise a NameError.
  2. Name Shadowing: The global dictionary rate_limit on line 6 has the same name as the decorator function rate_limit on line 8. This is confusing and can lead to unexpected behavior. The dictionary should be renamed (e.g., _rate_limits).

The suggested code fixes these issues.

from collections import defaultdict
from datetime import datetime, timedelta
from functools import wraps
from flask import abort, request, current_app

# Simple rate limiter using memory (use Redis for production)
_rate_limits = 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
            now = datetime.utcnow()
            window_start = now - timedelta(minutes=window_minutes)
            _rate_limits[client_ip] = [req_time for req_time in _rate_limits[client_ip] if req_time > window_start]
            if len(_rate_limits[client_ip]) >= max_requests:
                abort(429, description="Rate limit exceeded")
            _rate_limits[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

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

Hardcoded secrets are a critical security vulnerability. This key should be loaded from environment variables or a secret manager, not committed to source code. Additionally, this file appears to use FastAPI dependencies (Depends, HTTPBearer), which is inconsistent with the Flask framework used in the rest of the application. If this file is not used, it should be removed to avoid confusion and potential security risks.

Remember to import os.

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

ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

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

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)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt

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
return username
Loading
Loading