Files
Hotel-Booking/Backend/src/main.py
Iliyan Angelov 39fcfff811 update
2025-11-30 22:43:09 +02:00

295 lines
16 KiB
Python

from fastapi import FastAPI, Request, HTTPException, Depends, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from sqlalchemy.exc import IntegrityError, OperationalError
from jose.exceptions import JWTError
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from pathlib import Path
from datetime import datetime
import sys
import secrets
import os
import re
import logging
from .shared.config.settings import settings
from .shared.config.logging_config import setup_logging, get_logger
from .shared.config.database import engine, Base, get_db
from . import models
from sqlalchemy.orm import Session
logger = setup_logging()
logger.info(f'Starting {settings.APP_NAME} v{settings.APP_VERSION} in {settings.ENVIRONMENT} mode')
from .shared.middleware.error_handler import validation_exception_handler, integrity_error_handler, jwt_error_handler, http_exception_handler, general_exception_handler
from .shared.middleware.request_id import RequestIDMiddleware
from .security.middleware.security import SecurityHeadersMiddleware
from .shared.middleware.timeout import TimeoutMiddleware
from .shared.middleware.cookie_consent import CookieConsentMiddleware
from .security.middleware.csrf import CSRFProtectionMiddleware
from .shared.middleware.request_size_limit import RequestSizeLimitMiddleware
from .security.middleware.admin_ip_whitelist import AdminIPWhitelistMiddleware
if settings.is_development:
logger.info('Creating database tables (development mode)')
Base.metadata.create_all(bind=engine)
else:
try:
from .content.models.cookie_policy import CookiePolicy
from .content.models.cookie_integration_config import CookieIntegrationConfig
from .content.models.page_content import PageContent
logger.info('Ensuring required tables exist')
CookiePolicy.__table__.create(bind=engine, checkfirst=True)
CookieIntegrationConfig.__table__.create(bind=engine, checkfirst=True)
PageContent.__table__.create(bind=engine, checkfirst=True)
except Exception as e:
logger.error(f'Failed to ensure required tables exist: {e}')
from .auth.routes import auth_routes
from .content.routes import privacy_routes
app = FastAPI(title=settings.APP_NAME, description='Enterprise-grade Hotel Booking API', version=settings.APP_VERSION, docs_url='/api/docs' if not settings.is_production else None, redoc_url='/api/redoc' if not settings.is_production else None, openapi_url='/api/openapi.json' if not settings.is_production else None)
app.add_middleware(RequestIDMiddleware)
app.add_middleware(CookieConsentMiddleware)
if settings.REQUEST_TIMEOUT > 0:
app.add_middleware(TimeoutMiddleware)
app.add_middleware(SecurityHeadersMiddleware)
app.add_middleware(RequestSizeLimitMiddleware, max_size=settings.MAX_REQUEST_BODY_SIZE)
logger.info(f'Request size limiting enabled: {settings.MAX_REQUEST_BODY_SIZE // 1024 // 1024}MB max body size')
if settings.CSRF_PROTECTION_ENABLED:
app.add_middleware(CSRFProtectionMiddleware)
logger.info('CSRF protection enabled')
if settings.IP_WHITELIST_ENABLED:
app.add_middleware(AdminIPWhitelistMiddleware)
logger.info(f'Admin IP whitelisting enabled with {len(settings.ADMIN_IP_WHITELIST)} IP(s)/CIDR range(s)')
if settings.RATE_LIMIT_ENABLED:
limiter = Limiter(key_func=get_remote_address, default_limits=[f'{settings.RATE_LIMIT_PER_MINUTE}/minute'])
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
logger.info(f'Rate limiting enabled: {settings.RATE_LIMIT_PER_MINUTE} requests/minute')
# CORS middleware must be added LAST to handle OPTIONS preflight requests before other middleware
# In FastAPI/Starlette, middleware is executed in reverse order (last added = first executed = outermost)
# So adding CORS last ensures it wraps all other middleware and handles OPTIONS requests early
if settings.is_development:
# More restrictive CORS even in development for better security practices
app.add_middleware(
CORSMiddleware,
allow_origin_regex='http://(localhost|127\\.0\\.0\\.1)(:\\d+)?',
allow_credentials=True,
allow_methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], # Explicit methods
allow_headers=['Content-Type', 'Authorization', 'X-XSRF-TOKEN', 'X-Requested-With', 'X-Request-ID'] # Explicit headers
)
logger.info('CORS configured for development (allowing localhost with explicit methods/headers)')
else:
# Validate CORS_ORIGINS in production
if not settings.CORS_ORIGINS or len(settings.CORS_ORIGINS) == 0:
logger.warning('CORS_ORIGINS is empty in production. This may block legitimate requests.')
logger.warning('Please set CORS_ORIGINS environment variable with allowed origins.')
else:
# Log CORS configuration for security audit
logger.info(f'CORS configured for production with {len(settings.CORS_ORIGINS)} allowed origin(s)')
if logger.isEnabledFor(logging.DEBUG):
logger.debug(f'Allowed CORS origins: {", ".join(settings.CORS_ORIGINS)}')
app.add_middleware(CORSMiddleware, allow_origins=settings.CORS_ORIGINS or [], allow_credentials=True, allow_methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], allow_headers=['*'])
uploads_dir = Path(__file__).parent.parent / settings.UPLOAD_DIR
uploads_dir.mkdir(exist_ok=True)
app.mount('/uploads', StaticFiles(directory=str(uploads_dir)), name='uploads')
app.add_exception_handler(HTTPException, http_exception_handler)
app.add_exception_handler(RequestValidationError, validation_exception_handler)
app.add_exception_handler(IntegrityError, integrity_error_handler)
app.add_exception_handler(JWTError, jwt_error_handler)
app.add_exception_handler(Exception, general_exception_handler)
@app.get('/health', tags=['health'])
@app.get('/api/health', tags=['health'])
async def health_check(db: Session=Depends(get_db)):
health_status = {'status': 'healthy', 'timestamp': datetime.utcnow().isoformat(), 'service': settings.APP_NAME, 'version': settings.APP_VERSION, 'environment': settings.ENVIRONMENT, 'checks': {'api': 'ok', 'database': 'unknown'}}
try:
from sqlalchemy import text
db.execute(text('SELECT 1'))
health_status['checks']['database'] = 'ok'
except OperationalError as e:
health_status['status'] = 'unhealthy'
health_status['checks']['database'] = 'error'
health_status['error'] = str(e)
logger.error(f'Database health check failed: {str(e)}')
return JSONResponse(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, content=health_status)
except Exception as e:
health_status['status'] = 'unhealthy'
health_status['checks']['database'] = 'error'
health_status['error'] = str(e)
logger.error(f'Health check failed: {str(e)}')
return JSONResponse(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, content=health_status)
return health_status
@app.get('/metrics', tags=['monitoring'])
async def metrics():
return {'status': 'success', 'service': settings.APP_NAME, 'version': settings.APP_VERSION, 'environment': settings.ENVIRONMENT, 'timestamp': datetime.utcnow().isoformat()}
# Import all route modules from feature-based structure
from .auth.routes import auth_routes, user_routes
from .rooms.routes import room_routes, advanced_room_routes, rate_plan_routes
from .bookings.routes import booking_routes, group_booking_routes
from .payments.routes import payment_routes, invoice_routes
from .hotel_services.routes import service_routes, service_booking_routes
from .content.routes import (
banner_routes, page_content_routes, home_routes, about_routes,
contact_routes, contact_content_routes, footer_routes, privacy_routes,
admin_privacy_routes, terms_routes, refunds_routes, cancellation_routes,
accessibility_routes, faq_routes, blog_routes
)
from .reviews.routes import review_routes, favorite_routes
from .loyalty.routes import promotion_routes, loyalty_routes, package_routes
from .guest_management.routes import guest_profile_routes
from .notifications.routes import chat_routes, notification_routes, email_campaign_routes
from .analytics.routes import analytics_routes, report_routes, audit_routes
from .security.routes import security_routes
from .system.routes import system_settings_routes, workflow_routes, task_routes
from .ai.routes import ai_assistant_routes
# Register all routes with /api prefix (removed duplicate registrations)
# Using /api prefix as standard, API versioning can be handled via headers if needed
api_prefix = '/api'
app.include_router(auth_routes.router, prefix=api_prefix)
app.include_router(room_routes.router, prefix=api_prefix)
app.include_router(booking_routes.router, prefix=api_prefix)
app.include_router(group_booking_routes.router, prefix=api_prefix)
app.include_router(payment_routes.router, prefix=api_prefix)
app.include_router(invoice_routes.router, prefix=api_prefix)
app.include_router(banner_routes.router, prefix=api_prefix)
app.include_router(favorite_routes.router, prefix=api_prefix)
app.include_router(service_routes.router, prefix=api_prefix)
app.include_router(service_booking_routes.router, prefix=api_prefix)
app.include_router(promotion_routes.router, prefix=api_prefix)
app.include_router(report_routes.router, prefix=api_prefix)
app.include_router(review_routes.router, prefix=api_prefix)
app.include_router(user_routes.router, prefix=api_prefix)
app.include_router(audit_routes.router, prefix=api_prefix)
app.include_router(admin_privacy_routes.router, prefix=api_prefix)
app.include_router(system_settings_routes.router, prefix=api_prefix)
app.include_router(contact_routes.router, prefix=api_prefix)
app.include_router(home_routes.router, prefix=api_prefix)
app.include_router(about_routes.router, prefix=api_prefix)
app.include_router(contact_content_routes.router, prefix=api_prefix)
app.include_router(footer_routes.router, prefix=api_prefix)
app.include_router(privacy_routes.router, prefix=api_prefix)
app.include_router(terms_routes.router, prefix=api_prefix)
app.include_router(refunds_routes.router, prefix=api_prefix)
app.include_router(cancellation_routes.router, prefix=api_prefix)
app.include_router(accessibility_routes.router, prefix=api_prefix)
app.include_router(faq_routes.router, prefix=api_prefix)
app.include_router(chat_routes.router, prefix=api_prefix)
app.include_router(loyalty_routes.router, prefix=api_prefix)
app.include_router(guest_profile_routes.router, prefix=api_prefix)
app.include_router(analytics_routes.router, prefix=api_prefix)
app.include_router(workflow_routes.router, prefix=api_prefix)
app.include_router(task_routes.router, prefix=api_prefix)
app.include_router(notification_routes.router, prefix=api_prefix)
app.include_router(advanced_room_routes.router, prefix=api_prefix)
app.include_router(rate_plan_routes.router, prefix=api_prefix)
app.include_router(package_routes.router, prefix=api_prefix)
app.include_router(security_routes.router, prefix=api_prefix)
app.include_router(email_campaign_routes.router, prefix=api_prefix)
app.include_router(page_content_routes.router, prefix=api_prefix)
app.include_router(blog_routes.router, prefix=api_prefix)
app.include_router(ai_assistant_routes.router, prefix=api_prefix)
logger.info('All routes registered successfully')
def ensure_jwt_secret():
"""Generate and save JWT secret if it's using the default value.
In production, fail fast if default secret is used for security.
In development, auto-generate a secure secret if needed.
"""
default_secret = 'dev-secret-key-change-in-production-12345'
current_secret = settings.JWT_SECRET
# Security check: Fail fast in production if using default secret
if settings.is_production and (not current_secret or current_secret == default_secret):
error_msg = (
'CRITICAL SECURITY ERROR: JWT_SECRET is using default value in production! '
'Please set a secure JWT_SECRET in your environment variables.'
)
logger.error(error_msg)
raise ValueError(error_msg)
# Development mode: Auto-generate if needed
if not current_secret or current_secret == default_secret:
new_secret = secrets.token_urlsafe(64)
os.environ['JWT_SECRET'] = new_secret
env_file = Path(__file__).parent.parent / '.env'
if env_file.exists():
try:
env_content = env_file.read_text(encoding='utf-8')
jwt_pattern = re.compile(r'^JWT_SECRET=.*$', re.MULTILINE)
if jwt_pattern.search(env_content):
env_content = jwt_pattern.sub(f'JWT_SECRET={new_secret}', env_content)
else:
jwt_section_pattern = re.compile(r'(# =+.*JWT.*=+.*\n)', re.IGNORECASE | re.MULTILINE)
match = jwt_section_pattern.search(env_content)
if match:
insert_pos = match.end()
env_content = env_content[:insert_pos] + f'JWT_SECRET={new_secret}\n' + env_content[insert_pos:]
else:
env_content += f'\nJWT_SECRET={new_secret}\n'
env_file.write_text(env_content, encoding='utf-8')
logger.info('✓ JWT secret generated and saved to .env file')
except Exception as e:
logger.warning(f'Could not update .env file: {e}')
logger.info(f'Generated JWT secret (add to .env manually): JWT_SECRET={new_secret}')
else:
logger.info(f'Generated JWT secret (add to .env file): JWT_SECRET={new_secret}')
logger.info('✓ Secure JWT secret generated automatically')
else:
logger.info('✓ JWT secret is configured')
@app.on_event('startup')
async def startup_event():
ensure_jwt_secret()
# Validate encryption key configuration
try:
settings.validate_encryption_key()
except ValueError as e:
logger.error(str(e))
if settings.is_production:
raise # Fail fast in production
# Start AI Training Scheduler for automatic self-learning
try:
from .ai.services.ai_training_scheduler import get_training_scheduler
scheduler = get_training_scheduler()
scheduler.start()
logger.info('AI Training Scheduler started - automatic self-learning enabled')
except Exception as e:
logger.error(f'Failed to start AI Training Scheduler: {str(e)}', exc_info=True)
# Don't fail app startup if scheduler fails
logger.info(f'{settings.APP_NAME} started successfully')
logger.info(f'Environment: {settings.ENVIRONMENT}')
logger.info(f'Debug mode: {settings.DEBUG}')
logger.info(f'API version: {settings.API_V1_PREFIX}')
@app.on_event('shutdown')
async def shutdown_event():
# Stop AI Training Scheduler
try:
from .ai.services.ai_training_scheduler import get_training_scheduler
scheduler = get_training_scheduler()
scheduler.stop()
logger.info('AI Training Scheduler stopped')
except Exception as e:
logger.error(f'Error stopping AI Training Scheduler: {str(e)}', exc_info=True)
logger.info(f'{settings.APP_NAME} shutting down gracefully')
if __name__ == '__main__':
import uvicorn
from pathlib import Path
base_dir = Path(__file__).parent.parent
src_dir = str(base_dir / 'src')
uvicorn.run('src.main:app', host=settings.HOST, port=settings.PORT, reload=settings.is_development, log_level=settings.LOG_LEVEL.lower(), reload_dirs=[src_dir] if settings.is_development else None, reload_excludes=['*.log', '*.pyc', '*.pyo', '*.pyd', '__pycache__', '**/__pycache__/**', '*.db', '*.sqlite', '*.sqlite3'], reload_delay=0.5)