diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f6d7162..d5a64da 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -42,25 +42,21 @@ jobs: tags: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} + labels: | + org.opencontainers.image.source=${{ github.repositoryUrl }} - name: Deploy to Azure Web App uses: azure/webapps-deploy@v2 with: app-name: ${{ secrets.AZURE_WEBAPP_NAME }} publish-profile: ${{ secrets.AZURE_PUBLISH_PROFILE }} - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-push.outputs.digest }} + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} + runtime: "linux" - name: Log in to Azure (for app service settings) uses: azure/login@v1 with: creds: ${{ secrets.AZURE_CREDENTIALS }} - # AZURE_CREDENTIALS should be a JSON string for a Service Principal: - # { - # "clientId": "", - # "clientSecret": "", - # "subscriptionId": "", - # "tenantId": "" - # } - name: Configure environment variables uses: azure/appservice-settings@v1 @@ -88,4 +84,4 @@ jobs: "value": "5000", "slotSetting": false } - ] + ] \ No newline at end of file diff --git a/API-Core/.env.example b/API-Core/.env.example index 8396059..cd57cb3 100644 --- a/API-Core/.env.example +++ b/API-Core/.env.example @@ -28,6 +28,12 @@ PORT=5432 DBNAME=postgres # Full Supabase URI (use URL-encoded password here) -# You can encode password using: -# >>> import urllib.parse; urllib.parse.quote("your-password") +#>>>>>>>>> + # You can encode password using: + # >>> import urllib.parse; urllib.parse.quote("your-password") +#<<<<<<<<< SUPABASE_CONN="postgresql://:@${HOST}:${PORT}/${DBNAME}" + +#caching +REDIS_URL=redis://localhost:6379/0 +CACHE_ENABLED=true diff --git a/API-Core/app/__init__.py b/API-Core/app/__init__.py index f7f63b7..e9a77b3 100644 --- a/API-Core/app/__init__.py +++ b/API-Core/app/__init__.py @@ -1,7 +1,7 @@ from http import HTTPStatus from dotenv import load_dotenv -from flask import Flask, jsonify +from flask import Flask, jsonify, request import os from datetime import timedelta import logging @@ -13,14 +13,13 @@ from .models.user import TokenBlockList from .middleware.security import SecurityMiddleware from .middleware.caching import cache_manager -from .middleware.monitoring import monitoring_bp, init_monitoring +from .middleware.monitoring import init_monitoring # Configure logging before app creation logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def create_app(config=None): - """Application factory with enhanced configuration and error handling""" app = Flask(__name__) # Load environment variables FIRST @@ -32,7 +31,7 @@ def create_app(config=None): # Configure application settings configure_app(app, config) - # Swagger Config + # Swagger Config - MUST COME FIRST app.config['API_TITLE'] = 'Student Marketplace API documentation with endpoints ' app.config['API_DESCRIPTION'] = 'Marketplace API documentation with endpoints for items, users, auth, etc.' app.config['API_VERSION'] = 'v1' @@ -41,6 +40,7 @@ def create_app(config=None): app.config['OPENAPI_SWAGGER_UI_PATH'] = '/' app.config['OPENAPI_SWAGGER_UI_URL'] = 'https://cdn.jsdelivr.net/npm/swagger-ui-dist/' + # Initialize API IMMEDIATELY AFTER SWAGGER CONFIG api.init_app(app) api.spec.components.security_scheme( "BearerAuth", @@ -51,7 +51,7 @@ def create_app(config=None): }, ) - # Configure logging FIRST before any other operations + # Configure logging configure_logging(app) logger = logging.getLogger(__name__) logger.info("Initializing application...") @@ -59,21 +59,18 @@ def create_app(config=None): # Initialize extensions initialize_extensions(app) - # Initialize middleware - initialize_middleware(app) - # Initialize monitoring init_monitoring(app) - # Register error handlers (before blueprints) + # Register error handlers register_error_handlers(app) + # Initialize middleware + app = initialize_middleware(app) + # Register blueprints register_blueprints(app) - # Register monitoring blueprint - app.register_blueprint(monitoring_bp) - # Configure teardown context @app.teardown_appcontext def shutdown_session(exception=None): @@ -82,19 +79,75 @@ def shutdown_session(exception=None): logger.info("Application initialized successfully") return app + def initialize_middleware(app): - """Initialize application middleware.""" - # Security middleware - security = SecurityMiddleware(app) - - # Cache manager + # Initialize security middleware with custom CSP + SecurityMiddleware(app) + + # Initialize cache manager cache_manager.init_app(app) - - # Set application start time for metrics + + # Start cache cleaner + if not hasattr(app, 'cache_cleaner_running') and app.config.get('ENABLE_CACHE_CLEANER', True): + start_cache_cleaner(app) + + return app + + +def start_cache_cleaner(app): + """Start background thread for cache maintenance.""" + from threading import Thread import time - app.start_time = time.time() - - logger.info("Middleware initialized successfully") + + def cleaner(): + while True: + time.sleep(60 * 5) # Clean every 5 minutes + try: + with app.app_context(): + cache_manager.cleanup_expired() + except Exception as e: + logger.error(f"Cache cleaner error: {str(e)}") + + thread = Thread(target=cleaner, daemon=True) + thread.start() + app.cache_cleaner_running = True + logger.info("Started cache cleanup thread") + + +def add_security_headers(app): + """Add security headers to all responses.""" + + @app.after_request + def add_security_headers(response): + # Security headers configuration + headers = { + 'X-Content-Type-Options': 'nosniff', + 'X-Frame-Options': 'DENY', + 'X-XSS-Protection': '1; mode=block', + 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains', + 'Referrer-Policy': 'strict-origin-when-cross-origin', + } + + # Relax CSP for Swagger UI + if request.path == '/': # Only for Swagger UI route + csp = "default-src 'self'; " \ + "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; " \ + "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; " \ + "img-src 'self' data: https://*.jsdelivr.net; " \ + "font-src 'self' https://cdn.jsdelivr.net; " \ + "connect-src 'self'" + headers['Content-Security-Policy'] = csp + else: + # Stricter CSP for other routes + headers['Content-Security-Policy'] = "default-src 'self';" + + # Add headers to response + for header, value in headers.items(): + response.headers[header] = value + + return response + + return app def configure_app(app, config=None): """Centralized configuration management""" @@ -107,13 +160,19 @@ def configure_app(app, config=None): # Optimized connection pooling settings for Supabase app.config['SQLALCHEMY_ENGINE_OPTIONS'] = { - 'pool_size': 1, # Max connections per worker - 'max_overflow': 0, # No additional connections beyond pool_size - 'pool_timeout': 10, # Wait time for connection (seconds) - 'pool_recycle': 300, # Recycle connections every 5 minutes (300s) - 'pool_pre_ping': True, # Check connection health before use - 'pool_use_lifo': True # Use Last-In-First-Out queue for better connection reuse + 'pool_size': 1, + 'max_overflow': 0, + 'pool_timeout': 10, + 'pool_recycle': 300, + 'pool_pre_ping': True, + 'pool_use_lifo': True } + # cache configuration + app.config.update({ + 'CACHE_DEFAULT_TIMEOUT': 300, + 'CACHE_ENABLED': True, + 'ENABLE_CACHE_CLEANER': True + }) # JWT Configuration app.config.update({ @@ -207,24 +266,26 @@ def register_blueprints(app): """Register blueprints with conflict checking""" from .blueprints.auth.routes import auth_bp from .blueprints.items.routes import items_crud_bp - # from .blueprints.routes import items_bp + from .blueprints.routes import items_bp from .blueprints.item_images.images import images_crud_bp from .blueprints.admin.listing import admin_listings_bp from .blueprints.admin.view import report_bp from .blueprints.messages.routes import msg_bp from .blueprints.admin.users import admin_bp from .blueprints.admin.stats import admin_stat_bp + from .blueprints.views.health import health_bp blueprints = [ (auth_bp, '/api/auth'), - # (items_bp, '/api/admin'), + (items_bp, '/api/admin'), (items_crud_bp, '/api/items'), (images_crud_bp, '/api/items'), (msg_bp, '/api/messages'), (admin_bp, '/api/admin'), (admin_listings_bp, '/api/admin'), (report_bp, '/api/admin'), - (admin_stat_bp, '/api/admin') + (admin_stat_bp, '/api/admin'), + (health_bp, '/api/health') ] for blueprint, url_prefix in blueprints: diff --git a/API-Core/app/blueprints/views/health.py b/API-Core/app/blueprints/views/health.py new file mode 100644 index 0000000..9edaa02 --- /dev/null +++ b/API-Core/app/blueprints/views/health.py @@ -0,0 +1,153 @@ +from flask import jsonify, current_app +from flask_smorest import Blueprint +from http import HTTPStatus +import time +import logging +import psutil +from ...extensions import db +from ...middleware.monitoring import HealthChecker + +logger = logging.getLogger(__name__) + +# Create health blueprint +health_bp = Blueprint( + 'System Health', + 'health', + url_prefix='/api/health', + description='Health views endpoints' +) + + +@health_bp.route('/health', methods=['GET']) +@health_bp.doc( + description="Comprehensive health check of application components", + tags=["System Health"] +) +@health_bp.response(200, description="System is healthy") +@health_bp.response(503, description="System is unhealthy") +def health_check(): + """ + Check overall system health including database and resources + + Returns: + - status: Overall health status + - checks: Detailed component statuses + """ + try: + db_health = HealthChecker.check_database() + system_health = HealthChecker.check_system_resources() + + overall_status = 'healthy' + if db_health['status'] != 'healthy' or system_health['status'] != 'healthy': + overall_status = 'unhealthy' + + response = { + 'status': overall_status, + 'timestamp': time.time(), + 'checks': { + 'database': db_health, + 'system': system_health + } + } + + status_code = HTTPStatus.OK if overall_status == 'healthy' else HTTPStatus.SERVICE_UNAVAILABLE + return jsonify(response), status_code + + except Exception as e: + logger.error(f"Health check failed: {str(e)}") + return jsonify({ + 'status': 'unhealthy', + 'error': "An internal error has occurred!", + 'timestamp': time.time() + }), HTTPStatus.SERVICE_UNAVAILABLE + + +@health_bp.route('/ready', methods=['GET']) +@health_bp.doc( + description="Readiness check for service discovery", + tags=["System Health"] +) +@health_bp.response(200, description="Service is ready") +@health_bp.response(503, description="Service is not ready") +def readiness_check(): + """ + Kubernetes-ready endpoint for service readiness + + Returns: + - status: Current readiness status + """ + try: + db_health = HealthChecker.check_database() + if db_health['status'] == 'healthy': + return jsonify({ + 'status': 'ready', + 'timestamp': time.time() + }), HTTPStatus.OK + else: + return jsonify({ + 'status': 'not_ready', + 'reason': 'database_unavailable', + 'timestamp': time.time() + }), HTTPStatus.SERVICE_UNAVAILABLE + + except Exception as e: + logger.error(f"Readiness check failed: {str(e)}") + return jsonify({ + 'status': 'not_ready', + 'error': "An internal error has occurred!", + 'timestamp': time.time() + }), HTTPStatus.SERVICE_UNAVAILABLE + + +@health_bp.route('/live', methods=['GET']) +@health_bp.doc( + description="Liveness check for process views", + tags=["System Health"] +) +@health_bp.response(200, description="Service is alive") +def liveness_check(): + """ + Kubernetes liveness probe endpoint + + Returns: + - status: Always returns 'alive' if reachable + """ + return jsonify({ + 'status': 'alive', + 'timestamp': time.time() + }), HTTPStatus.OK + + +@health_bp.route('/metrics', methods=['GET']) +@health_bp.doc( + description="Application performance metrics", + tags=["System Health"] +) +@health_bp.response(200, description="Metrics data") +@health_bp.response(500, description="Metrics collection error") +def metrics_endpoint(): + """ + Get detailed application performance metrics + + Returns: + - application: App-specific metrics + - system: Resource utilization metrics + """ + try: + metrics = HealthChecker.get_application_metrics() + system_health = HealthChecker.check_system_resources() + + response = { + 'application': metrics, + 'system': system_health, + 'timestamp': time.time() + } + + return jsonify(response), HTTPStatus.OK + + except Exception as e: + logger.error(f"Metrics endpoint failed: {str(e)}") + return jsonify({ + 'error': "An internal error has occurred!", + 'timestamp': time.time() + }), HTTPStatus.INTERNAL_SERVER_ERROR diff --git a/API-Core/app/middleware/caching.py b/API-Core/app/middleware/caching.py index 1189913..86924b1 100644 --- a/API-Core/app/middleware/caching.py +++ b/API-Core/app/middleware/caching.py @@ -10,12 +10,21 @@ logger = logging.getLogger(__name__) + class CacheManager: - """Simple in-memory cache manager.""" - + """Enhanced in-memory cache manager with app initialization support.""" + def __init__(self): self.cache = {} self.cache_stats = {'hits': 0, 'misses': 0} + self.default_timeout = 300 # Default timeout in seconds + self.enabled = True + + def init_app(self, app): + """Initialize with Flask app configuration.""" + self.default_timeout = app.config.get('CACHE_DEFAULT_TIMEOUT', 300) + self.enabled = app.config.get('CACHE_ENABLED', True) + logger.info(f"Cache initialized with timeout: {self.default_timeout}s") def get(self, key): """Get value from cache.""" diff --git a/API-Core/app/middleware/monitoring.py b/API-Core/app/middleware/monitoring.py index 7be3e04..672d87e 100644 --- a/API-Core/app/middleware/monitoring.py +++ b/API-Core/app/middleware/monitoring.py @@ -7,33 +7,41 @@ import os from flask import jsonify, current_app, g from sqlalchemy import text -from app.extensions import db +from ..extensions import db logger = logging.getLogger(__name__) class HealthChecker: """Health check utilities for monitoring.""" - + @staticmethod def check_database(): """Check database connectivity.""" try: - # Simple query to test database connection + start_time = time.perf_counter() db.session.execute(text('SELECT 1')) db.session.commit() - return {'status': 'healthy', 'response_time': 0} + response_time = time.perf_counter() - start_time + return { + 'status': 'healthy', + 'response_time_ms': round(response_time * 1000, 2) + } except Exception as e: logger.error(f"Database health check failed: {str(e)}") - return {'status': 'unhealthy', 'error': str(e)} - + return { + 'status': 'unhealthy', + 'error': "An internal error has occurred!", + 'response_time_ms': -1 + } + @staticmethod def check_system_resources(): """Check system resource usage.""" try: - cpu_percent = psutil.cpu_percent(interval=1) + cpu_percent = psutil.cpu_percent(interval=0.5) memory = psutil.virtual_memory() disk = psutil.disk_usage('/') - + return { 'status': 'healthy', 'cpu_percent': cpu_percent, @@ -44,197 +52,125 @@ def check_system_resources(): } except Exception as e: logger.error(f"System resource check failed: {str(e)}") - return {'status': 'unhealthy', 'error': str(e)} - + return {'status': 'unhealthy', 'error': "An internal error has occurred!"} + @staticmethod def get_application_metrics(): """Get application-specific metrics.""" try: - from app.middleware.caching import cache_manager - - # Get cache statistics - cache_stats = cache_manager.get_stats() - # Get basic app info metrics = { 'app_name': current_app.config.get('APP_NAME', 'StudentMarketplace'), 'version': current_app.config.get('VERSION', '1.0.0'), 'environment': current_app.config.get('FLASK_ENV', 'production'), 'uptime_seconds': time.time() - current_app.config.get('START_TIME', time.time()), - 'cache_stats': cache_stats + 'request_metrics': RequestMetrics.get_global_stats() } - - return metrics - except Exception as e: - logger.error(f"Application metrics check failed: {str(e)}") - return {'error': str(e)} + # Add cache stats if available + if hasattr(current_app, 'cache_manager'): + metrics['cache_stats'] = current_app.cache_manager.get_stats() -def create_health_endpoints(app): - """Create health check endpoints.""" - - @app.route('/health') - def health_check(): - """Basic health check endpoint.""" - try: - # Check database - db_health = HealthChecker.check_database() - - # Check system resources - system_health = HealthChecker.check_system_resources() - - # Determine overall health - overall_status = 'healthy' - if db_health['status'] != 'healthy' or system_health['status'] != 'healthy': - overall_status = 'unhealthy' - - response = { - 'status': overall_status, - 'timestamp': time.time(), - 'checks': { - 'database': db_health, - 'system': system_health - } - } - - status_code = 200 if overall_status == 'healthy' else 503 - return jsonify(response), status_code - - except Exception as e: - logger.error(f"Health check failed: {str(e)}") - return jsonify({ - 'status': 'unhealthy', - 'error': str(e), - 'timestamp': time.time() - }), 503 - - @app.route('/ready') - def readiness_check(): - """Readiness check for Kubernetes.""" - try: - # Check if app is ready to serve requests - db_health = HealthChecker.check_database() - - if db_health['status'] == 'healthy': - return jsonify({ - 'status': 'ready', - 'timestamp': time.time() - }), 200 - else: - return jsonify({ - 'status': 'not_ready', - 'reason': 'database_unavailable', - 'timestamp': time.time() - }), 503 - - except Exception as e: - logger.error(f"Readiness check failed: {str(e)}") - return jsonify({ - 'status': 'not_ready', - 'error': str(e), - 'timestamp': time.time() - }), 503 - - @app.route('/live') - def liveness_check(): - """Liveness check for Kubernetes.""" - # Simple liveness check - if we can respond, we're alive - return jsonify({ - 'status': 'alive', - 'timestamp': time.time() - }), 200 - - @app.route('/metrics') - def metrics_endpoint(): - """Application metrics endpoint.""" - try: - metrics = HealthChecker.get_application_metrics() - system_health = HealthChecker.check_system_resources() - - response = { - 'application': metrics, - 'system': system_health, - 'timestamp': time.time() - } - - return jsonify(response), 200 - + return metrics except Exception as e: - logger.error(f"Metrics endpoint failed: {str(e)}") - return jsonify({ - 'error': str(e), - 'timestamp': time.time() - }), 500 + logger.error(f"Application metrics check failed: {str(e)}") + return {'error': "An internal error has occurred!"} class RequestMetrics: """Track request metrics.""" - + + _instance = None + def __init__(self): self.request_count = 0 self.response_times = [] self.error_count = 0 - + self.status_codes = {} + + @classmethod + def get_global_instance(cls): + """Get singleton instance.""" + if cls._instance is None: + cls._instance = RequestMetrics() + return cls._instance + + @classmethod + def get_global_stats(cls): + """Get global request statistics.""" + return cls.get_global_instance().get_stats() + def record_request(self, response_time, status_code): """Record request metrics.""" self.request_count += 1 self.response_times.append(response_time) - + + # Track status code distribution + status_category = f"{status_code // 100}xx" + self.status_codes[status_category] = self.status_codes.get(status_category, 0) + 1 + if status_code >= 400: self.error_count += 1 - + # Keep only last 1000 response times if len(self.response_times) > 1000: self.response_times = self.response_times[-1000:] - + def get_stats(self): """Get request statistics.""" if not self.response_times: return { 'request_count': self.request_count, 'error_count': self.error_count, + 'status_codes': self.status_codes, 'error_rate': 0, 'avg_response_time': 0, 'min_response_time': 0, 'max_response_time': 0 } - + avg_response_time = sum(self.response_times) / len(self.response_times) error_rate = (self.error_count / self.request_count * 100) if self.request_count > 0 else 0 - + return { 'request_count': self.request_count, 'error_count': self.error_count, + 'status_codes': self.status_codes, 'error_rate': round(error_rate, 2), 'avg_response_time': round(avg_response_time, 3), 'min_response_time': round(min(self.response_times), 3), - 'max_response_time': round(max(self.response_times), 3) + 'max_response_time': round(max(self.response_times), 3), + 'p95_response_time': round( + sorted(self.response_times)[int(len(self.response_times) * 0.95)], + 3 + ) } -# Global metrics instance -request_metrics = RequestMetrics() - - def init_monitoring(app): """Initialize monitoring middleware.""" - # Record start time app.config['START_TIME'] = time.time() - - # Create health endpoints - create_health_endpoints(app) - + + # Initialize request metrics + RequestMetrics.get_global_instance() + @app.before_request def before_request(): """Record request start time.""" - g.start_time = time.time() - + g.start_time = time.perf_counter() + @app.after_request def after_request(response): """Record request metrics.""" if hasattr(g, 'start_time'): - response_time = time.time() - g.start_time - request_metrics.record_request(response_time, response.status_code) - + response_time = time.perf_counter() - g.start_time + RequestMetrics.get_global_instance().record_request( + response_time, + response.status_code + ) + + # Add monitoring headers + response.headers['X-Response-Time'] = f"{response_time:.4f}s" + response.headers['X-Request-Count'] = RequestMetrics.get_global_instance().request_count return response \ No newline at end of file diff --git a/API-Core/app/middleware/security.py b/API-Core/app/middleware/security.py index 61b05c7..fef2d6f 100644 --- a/API-Core/app/middleware/security.py +++ b/API-Core/app/middleware/security.py @@ -1,156 +1,38 @@ -""" -Security middleware for enhanced application security. -""" -import logging -import uuid -from functools import wraps -from flask import request, g, current_app, abort, jsonify -from werkzeug.exceptions import TooManyRequests -import time -import hashlib +from flask import request -logger = logging.getLogger(__name__) class SecurityMiddleware: - """Enhanced security middleware with comprehensive protection.""" - - def __init__(self, app=None): + def __init__(self, app): self.app = app - self.rate_limit_storage = {} - if app is not None: - self.init_app(app) - - def init_app(self, app): - """Initialize security middleware with Flask app.""" - app.before_request(self.before_request) - app.after_request(self.after_request) - - # Configure security settings - app.config.setdefault('SECURITY_HEADERS_ENABLED', True) - app.config.setdefault('RATE_LIMIT_ENABLED', True) - app.config.setdefault('CORRELATION_ID_ENABLED', True) - - def before_request(self): - """Execute before each request.""" - # Add correlation ID - if current_app.config.get('CORRELATION_ID_ENABLED', True): - g.correlation_id = str(uuid.uuid4()) - - # Rate limiting - if current_app.config.get('RATE_LIMIT_ENABLED', True): - self._check_rate_limit() - - # Log request - logger.info( - "Request started", - extra={ - 'correlation_id': getattr(g, 'correlation_id', None), - 'method': request.method, - 'path': request.path, - 'remote_addr': request.remote_addr - } - ) - - def after_request(self, response): - """Execute after each request.""" - # Add security headers - if current_app.config.get('SECURITY_HEADERS_ENABLED', True): - self._add_security_headers(response) - - # Add correlation ID to response - if hasattr(g, 'correlation_id'): - response.headers['X-Correlation-ID'] = g.correlation_id - - return response - - def _add_security_headers(self, response): - """Add comprehensive security headers.""" - # Content Security Policy - csp = ( - "default-src 'self'; " - "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " - "style-src 'self' 'unsafe-inline'; " - "img-src 'self' data: https:; " - "font-src 'self' data:; " - "connect-src 'self'; " - "frame-ancestors 'none';" - ) - response.headers['Content-Security-Policy'] = csp - - # Security headers - response.headers['X-Content-Type-Options'] = 'nosniff' - response.headers['X-Frame-Options'] = 'DENY' - response.headers['X-XSS-Protection'] = '1; mode=block' - response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin' - - # HTTPS enforcement (if enabled) - if current_app.config.get('FORCE_HTTPS', False): - response.headers['Strict-Transport-Security'] = ( - 'max-age=31536000; includeSubDomains; preload' - ) - - def _check_rate_limit(self): - """Simple rate limiting implementation.""" - client_id = self._get_client_id() - current_time = time.time() - - # Clean old entries - self._cleanup_rate_limit_storage(current_time) - - # Check rate limit - if client_id in self.rate_limit_storage: - requests = self.rate_limit_storage[client_id] - # Allow 100 requests per minute - if len(requests) >= 100: - raise TooManyRequests("Rate limit exceeded") - - # Add current request - if client_id not in self.rate_limit_storage: - self.rate_limit_storage[client_id] = [] - - self.rate_limit_storage[client_id].append(current_time) - - def _get_client_id(self): - """Get client identifier for rate limiting.""" - # Use IP address as client identifier - return hashlib.md5( - (request.remote_addr or 'unknown').encode() - ).hexdigest() - - def _cleanup_rate_limit_storage(self, current_time): - """Clean up old rate limit entries.""" - cutoff_time = current_time - 60 # 1 minute window - - for client_id in list(self.rate_limit_storage.keys()): - self.rate_limit_storage[client_id] = [ - req_time for req_time in self.rate_limit_storage[client_id] - if req_time > cutoff_time - ] - - if not self.rate_limit_storage[client_id]: - del self.rate_limit_storage[client_id] + self.setup_headers() + def setup_headers(self): + @self.app.after_request + def add_security_headers(response): + # Base security headers + headers = { + 'X-Content-Type-Options': 'nosniff', + 'X-Frame-Options': 'DENY', + 'X-XSS-Protection': '1; mode=block', + 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains', + 'Referrer-Policy': 'strict-origin-when-cross-origin', + } -def require_api_key(f): - """Decorator to require API key authentication.""" - @wraps(f) - def decorated_function(*args, **kwargs): - api_key = request.headers.get('X-API-Key') - - if not api_key: - return jsonify({'error': 'API key required'}), 401 - - # Validate API key (implement your validation logic) - if not _validate_api_key(api_key): - return jsonify({'error': 'Invalid API key'}), 401 - - return f(*args, **kwargs) - - return decorated_function + # Special CSP for Swagger UI + if request.path == '/': + csp = "default-src 'self'; " \ + "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; " \ + "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; " \ + "img-src 'self' data: https://cdn.jsdelivr.net; " \ + "font-src 'self' https://cdn.jsdelivr.net; " \ + "connect-src 'self'" + headers['Content-Security-Policy'] = csp + else: + # Stricter CSP for other routes + headers['Content-Security-Policy'] = "default-src 'self';" + # Add headers to response + for header, value in headers.items(): + response.headers[header] = value -def _validate_api_key(api_key): - """Validate API key (implement your validation logic).""" - # This is a placeholder - implement actual validation - valid_keys = current_app.config.get('VALID_API_KEYS', []) - return api_key in valid_keys \ No newline at end of file + return response diff --git a/API-Core/app/services/enhanced_item.py b/API-Core/app/services/enhanced_item.py index df93658..989e31d 100644 --- a/API-Core/app/services/enhanced_item.py +++ b/API-Core/app/services/enhanced_item.py @@ -7,13 +7,13 @@ from ..models import Item, ItemStatus, ItemCategory, ItemCondition, User from ..extensions import db -from ..middleware.caching import cached, cache_invalidate_pattern +from ..middleware.caching import cached, invalidate_cache_pattern from ..models.user import UserInstitution class EnhancedItemService: """Enhanced item service with caching and optimizations.""" - + @staticmethod @cached(timeout=300, key_prefix="popular_items") def get_popular_items(limit: int = 10) -> List[Dict[str, Any]]: @@ -25,12 +25,12 @@ def get_popular_items(limit: int = 10) -> List[Dict[str, Any]]: .order_by(Item.created_at.desc()) .limit(limit) .all()) - + return [item.to_dict() for item in items] except Exception as e: current_app.logger.error(f"Error fetching popular items: {e}") return [] - + @staticmethod @cached(timeout=600, key_prefix="category_stats") def get_category_statistics() -> Dict[str, int]: @@ -43,15 +43,15 @@ def get_category_statistics() -> Dict[str, int]: .filter_by(status=ItemStatus.AVAILABLE) .group_by(Item.category) .all()) - + return { - category.name if category else "Uncategorized": count + category.name if category else "Uncategorized": count for category, count in stats } except Exception as e: current_app.logger.error(f"Error fetching category stats: {e}") return {} - + @staticmethod def get_optimized_filtered_items(filters: Dict[str, Any]) -> Dict[str, Any]: """Optimized filtered items query with eager loading.""" @@ -62,28 +62,27 @@ def get_optimized_filtered_items(filters: Dict[str, Any]) -> Dict[str, Any]: joinedload(Item.seller), joinedload(Item.images) )) - - # Apply filters + if filters.get('category'): query = query.filter(Item.category == filters['category']) - + if filters.get('school'): query = query.join(Item.seller).filter( User.institution == filters['school'] ) - + if filters.get('min_price'): query = query.filter(Item.price >= filters['min_price']) - + if filters.get('max_price'): query = query.filter(Item.price <= filters['max_price']) - + if filters.get('condition'): query = query.filter(Item.condition == filters['condition']) - + if filters.get('status'): query = query.filter(Item.status == filters['status']) - + if filters.get('keyword'): keyword = f"%{filters['keyword']}%" query = query.filter( @@ -92,27 +91,27 @@ def get_optimized_filtered_items(filters: Dict[str, Any]) -> Dict[str, Any]: Item.description.ilike(keyword) ) ) - + # Apply sorting sort_by = filters.get('sort_by', 'created_at') sort_order = filters.get('sort_order', 'desc') - + if sort_by in ['price', 'created_at'] and sort_order in ['asc', 'desc']: column = getattr(Item, sort_by) if sort_order == 'desc': column = column.desc() query = query.order_by(column) - + # Pagination page = filters.get('page', 1) per_page = min(filters.get('per_page', 20), 100) # Limit max per_page - + paginated = query.paginate( - page=page, - per_page=per_page, + page=page, + per_page=per_page, error_out=False ) - + return { 'items': [item.to_dict() for item in paginated.items], 'total': paginated.total, @@ -120,11 +119,11 @@ def get_optimized_filtered_items(filters: Dict[str, Any]) -> Dict[str, Any]: 'per_page': paginated.per_page, 'pages': paginated.pages } - + except Exception as e: current_app.logger.error(f"Error in optimized filtered items: {e}") raise - + @staticmethod def invalidate_item_caches(item_id: Optional[int] = None): """Invalidate relevant caches when items change.""" @@ -133,8 +132,8 @@ def invalidate_item_caches(item_id: Optional[int] = None): "category_stats:*", "filtered_items:*" ] - + for pattern in patterns: - cache_invalidate_pattern(pattern) + invalidate_cache_pattern(pattern) current_app.logger.info(f"Invalidated item caches for item_id: {item_id}") \ No newline at end of file diff --git a/API-Core/tests/test_monitoring.py b/API-Core/tests/test_monitoring.py index 2e9104d..ddbad54 100644 --- a/API-Core/tests/test_monitoring.py +++ b/API-Core/tests/test_monitoring.py @@ -1,4 +1,4 @@ -"""Tests for monitoring and health check functionality.""" +"""Tests for views and health check functionality.""" import pytest import json @@ -89,7 +89,7 @@ def test_error_logging(self, client, caplog): class TestSecurityMonitoring: - """Test security monitoring features.""" + """Test security views features.""" def test_failed_auth_attempts_logged(self, client, caplog): """Test that failed authentication attempts are logged."""