""" BMC Hub - FastAPI Application Main application entry point """ import logging from pathlib import Path from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.responses import RedirectResponse from contextlib import asynccontextmanager from app.core.config import settings from app.core.database import init_db from app.core.auth_service import AuthService from app.core.database import execute_query_single def get_version(): """Read version from VERSION file""" try: version_file = Path(__file__).parent / "VERSION" return version_file.read_text().strip() except Exception: return "unknown" # Import Feature Routers from app.customers.backend import router as customers_api from app.customers.backend import views as customers_views from app.customers.backend import bmc_office_router # from app.hardware.backend import router as hardware_api # Replaced by hardware module from app.billing.backend import router as billing_api from app.billing.frontend import views as billing_views from app.system.backend import router as system_api from app.system.backend import sync_router from app.dashboard.backend import views as dashboard_views from app.dashboard.backend import router as dashboard_api from app.prepaid.backend import router as prepaid_api from app.prepaid.backend import views as prepaid_views from app.fixed_price.backend import router as fixed_price_api from app.fixed_price.frontend import views as fixed_price_views from app.subscriptions.backend import router as subscriptions_api from app.subscriptions.frontend import views as subscriptions_views from app.products.backend import router as products_api from app.products.frontend import views as products_views from app.ticket.backend import router as ticket_api from app.ticket.frontend import views as ticket_views from app.vendors.backend import router as vendors_api from app.vendors.backend import views as vendors_views from app.timetracking.backend import router as timetracking_api from app.timetracking.frontend import views as timetracking_views from app.contacts.backend import views as contacts_views from app.contacts.backend import router_simple as contacts_api from app.tags.backend import router as tags_api from app.tags.backend import views as tags_views from app.emails.backend import router as emails_api from app.emails.frontend import views as emails_views from app.settings.backend import router as settings_api from app.settings.backend import email_templates as email_templates_api from app.settings.backend import views as settings_views from app.backups.backend.router import router as backups_api from app.backups.frontend import views as backups_views from app.backups.backend.scheduler import backup_scheduler from app.conversations.backend import router as conversations_api from app.conversations.frontend import views as conversations_views from app.opportunities.backend import router as opportunities_api from app.opportunities.frontend import views as opportunities_views from app.auth.backend import router as auth_api from app.auth.backend import views as auth_views from app.auth.backend import admin as auth_admin_api from app.devportal.backend import router as devportal_api from app.devportal.backend import views as devportal_views from app.routers import anydesk # Modules from app.modules.webshop.backend import router as webshop_api from app.modules.webshop.frontend import views as webshop_views from app.modules.sag.backend import router as sag_api from app.modules.sag.backend import reminders as sag_reminders_api from app.modules.sag.frontend import views as sag_views from app.modules.hardware.backend import router as hardware_module_api from app.modules.hardware.frontend import views as hardware_module_views from app.modules.locations.backend import router as locations_api from app.modules.locations.frontend import views as locations_views from app.modules.nextcloud.backend import router as nextcloud_api from app.modules.search.backend import router as search_api from app.modules.wiki.backend import router as wiki_api from app.fixed_price.backend import router as fixed_price_api from app.modules.telefoni.backend import router as telefoni_api from app.modules.telefoni.frontend import views as telefoni_views from app.modules.calendar.backend import router as calendar_api from app.modules.calendar.frontend import views as calendar_views from app.modules.orders.backend import router as orders_api from app.modules.orders.frontend import views as orders_views # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.StreamHandler(), logging.FileHandler('logs/app.log') ] ) logger = logging.getLogger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): """Lifecycle management - startup and shutdown""" # Startup logger.info("🚀 Starting BMC Hub...") logger.info(f"Database: {settings.DATABASE_URL}") init_db() # Start unified scheduler (handles backups + email fetch + reminders) backup_scheduler.start() # Register reminder scheduler job from app.jobs.check_reminders import check_reminders from apscheduler.triggers.interval import IntervalTrigger from apscheduler.triggers.cron import CronTrigger backup_scheduler.scheduler.add_job( func=check_reminders, trigger=IntervalTrigger(minutes=5), id='check_reminders', name='Check Reminders', max_instances=1, replace_existing=True ) logger.info("✅ Reminder job scheduled (every 5 minutes)") # Register subscription invoice processing job from app.jobs.process_subscriptions import process_subscriptions backup_scheduler.scheduler.add_job( func=process_subscriptions, trigger=CronTrigger(hour=4, minute=0), id='process_subscriptions', name='Process Subscription Invoices', max_instances=1, replace_existing=True ) logger.info("✅ Subscription invoice job scheduled (daily at 04:00)") if settings.ESET_ENABLED and settings.ESET_SYNC_ENABLED: from app.jobs.eset_sync import run_eset_sync backup_scheduler.scheduler.add_job( func=run_eset_sync, trigger=IntervalTrigger(minutes=settings.ESET_SYNC_INTERVAL_MINUTES), id='eset_sync', name='ESET Sync', max_instances=1, replace_existing=True ) logger.info("✅ ESET sync job scheduled (every %d minutes)", settings.ESET_SYNC_INTERVAL_MINUTES) logger.info("✅ System initialized successfully") yield # Shutdown backup_scheduler.stop() logger.info("👋 Shutting down...") # Create FastAPI app app = FastAPI( title="BMC Hub API", description=""" Central management system for BMC Networks. **Key Features:** - Customer management - Hardware tracking - Service management - Billing integration """, version="1.0.0", lifespan=lifespan, docs_url="/api/docs", redoc_url="/api/redoc", openapi_url="/api/openapi.json" ) # CORS middleware - use CORS_ORIGINS if set, otherwise fallback to ALLOWED_ORIGINS cors_origins = settings.CORS_ORIGINS.split(",") if settings.CORS_ORIGINS else settings.ALLOWED_ORIGINS app.add_middleware( CORSMiddleware, allow_origins=cors_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Global auth middleware @app.middleware("http") async def auth_middleware(request: Request, call_next): path = request.url.path public_paths = { "/health", "/login", "/api/v1/auth/login" } # Yealink Action URL callbacks (secured inside telefoni module by token/IP) public_paths.add("/api/v1/telefoni/established") public_paths.add("/api/v1/telefoni/terminated") if settings.DEV_ALLOW_ARCHIVED_IMPORT: public_paths.add("/api/v1/ticket/archived/simply/import") public_paths.add("/api/v1/ticket/archived/simply/modules") public_paths.add("/api/v1/ticket/archived/simply/ticket") public_paths.add("/api/v1/ticket/archived/simply/record") if path in public_paths or path.startswith("/static") or path.startswith("/docs"): return await call_next(request) token = None auth_header = request.headers.get("Authorization") if auth_header and auth_header.lower().startswith("bearer "): token = auth_header.split(" ", 1)[1] else: token = request.cookies.get("access_token") payload = AuthService.verify_token(token) if token else None if not payload: if path.startswith("/api"): from fastapi.responses import JSONResponse return JSONResponse( status_code=401, content={"detail": "Not authenticated"} ) return RedirectResponse(url="/login") user_id_value = payload.get("sub") or payload.get("user_id") if user_id_value is not None: try: request.state.user_id = int(user_id_value) except (TypeError, ValueError): request.state.user_id = None if path.startswith("/api") and not payload.get("shadow_admin"): if not payload.get("sub"): from fastapi.responses import JSONResponse return JSONResponse( status_code=401, content={"detail": "Invalid token"} ) user_id = int(payload.get("sub")) user = execute_query_single( "SELECT is_2fa_enabled FROM users WHERE user_id = %s", (user_id,) ) is_2fa_enabled = bool(user and user.get("is_2fa_enabled")) if not is_2fa_enabled: allowed_2fa_paths = ( "/api/v1/auth/2fa", "/api/v1/auth/me", "/api/v1/auth/logout" ) if not path.startswith(allowed_2fa_paths): from fastapi.responses import JSONResponse return JSONResponse( status_code=403, content={"detail": "2FA required"} ) return await call_next(request) # Include routers app.include_router(customers_api.router, prefix="/api/v1", tags=["Customers"]) app.include_router(bmc_office_router.router, prefix="/api/v1", tags=["BMC Office"]) # app.include_router(hardware_api.router, prefix="/api/v1", tags=["Hardware"]) # Replaced by hardware module app.include_router(billing_api.router, prefix="/api/v1", tags=["Billing"]) app.include_router(system_api.router, prefix="/api/v1", tags=["System"]) app.include_router(dashboard_api.router, prefix="/api/v1", tags=["Dashboard"]) app.include_router(sync_router.router, prefix="/api/v1/system", tags=["System Sync"]) app.include_router(prepaid_api.router, prefix="/api/v1", tags=["Prepaid Cards"]) app.include_router(fixed_price_api.router, prefix="/api/v1", tags=["Fixed-Price Agreements"]) app.include_router(subscriptions_api.router, prefix="/api/v1", tags=["Subscriptions"]) app.include_router(products_api.router, prefix="/api/v1", tags=["Products"]) app.include_router(ticket_api.router, prefix="/api/v1/ticket", tags=["Tickets"]) app.include_router(vendors_api.router, prefix="/api/v1", tags=["Vendors"]) app.include_router(contacts_api.router, prefix="/api/v1", tags=["Contacts"]) app.include_router(timetracking_api, prefix="/api/v1", tags=["Time Tracking"]) app.include_router(tags_api.router, prefix="/api/v1", tags=["Tags"]) app.include_router(emails_api.router, prefix="/api/v1", tags=["Emails"]) app.include_router(settings_api.router, prefix="/api/v1", tags=["Settings"]) app.include_router(email_templates_api.router, prefix="/api/v1", tags=["Email Templates"]) app.include_router(backups_api, prefix="/api/v1", tags=["Backups"]) app.include_router(conversations_api.router, prefix="/api/v1", tags=["Conversations"]) app.include_router(opportunities_api.router, prefix="/api/v1", tags=["Opportunities"]) app.include_router(auth_api.router, prefix="/api/v1/auth", tags=["Auth"]) app.include_router(auth_admin_api.router, prefix="/api/v1", tags=["Auth Admin"]) app.include_router(anydesk.router, prefix="/api/v1", tags=["Remote Support"]) # Module Routers app.include_router(webshop_api.router, prefix="/api/v1", tags=["Webshop"]) app.include_router(sag_api.router, prefix="/api/v1", tags=["Cases"]) app.include_router(sag_reminders_api.router, tags=["Reminders"]) # No prefix - endpoints have full path app.include_router(hardware_module_api.router, prefix="/api/v1", tags=["Hardware Module"]) app.include_router(locations_api, prefix="/api/v1", tags=["Locations"]) app.include_router(nextcloud_api.router, prefix="/api/v1/nextcloud", tags=["Nextcloud"]) app.include_router(search_api.router, prefix="/api/v1", tags=["Search"]) app.include_router(wiki_api.router, prefix="/api/v1/wiki", tags=["Wiki"]) app.include_router(devportal_api.router, prefix="/api/v1/devportal", tags=["Devportal"]) app.include_router(telefoni_api.router, prefix="/api/v1", tags=["Telefoni"]) app.include_router(calendar_api.router, prefix="/api/v1", tags=["Calendar"]) app.include_router(orders_api.router, prefix="/api/v1", tags=["Orders"]) # Frontend Routers app.include_router(dashboard_views.router, tags=["Frontend"]) app.include_router(customers_views.router, tags=["Frontend"]) app.include_router(prepaid_views.router, tags=["Frontend"]) app.include_router(fixed_price_views.router, tags=["Frontend"]) app.include_router(subscriptions_views.router, tags=["Frontend"]) app.include_router(products_views.router, tags=["Frontend"]) app.include_router(vendors_views.router, tags=["Frontend"]) app.include_router(timetracking_views.router, tags=["Frontend"]) app.include_router(billing_views.router, tags=["Frontend"]) app.include_router(ticket_views.router, prefix="/ticket", tags=["Frontend"]) app.include_router(contacts_views.router, tags=["Frontend"]) app.include_router(tags_views.router, tags=["Frontend"]) app.include_router(settings_views.router, tags=["Frontend"]) app.include_router(emails_views.router, tags=["Frontend"]) app.include_router(backups_views.router, tags=["Frontend"]) app.include_router(conversations_views.router, tags=["Frontend"]) app.include_router(webshop_views.router, tags=["Frontend"]) app.include_router(opportunities_views.router, tags=["Frontend"]) app.include_router(auth_views.router, tags=["Frontend"]) app.include_router(sag_views.router, tags=["Frontend"]) app.include_router(hardware_module_views.router, tags=["Frontend"]) app.include_router(locations_views.router, tags=["Frontend"]) app.include_router(devportal_views.router, tags=["Frontend"]) app.include_router(telefoni_views.router, tags=["Frontend"]) app.include_router(calendar_views.router, tags=["Frontend"]) app.include_router(orders_views.router, tags=["Frontend"]) # Serve static files (UI) app.mount("/static", StaticFiles(directory="static", html=True), name="static") app.mount("/docs", StaticFiles(directory="docs"), name="docs") @app.get("/health") async def health_check(): """Health check endpoint""" return { "status": "healthy", "service": "BMC Hub", "version": get_version() } if __name__ == "__main__": import uvicorn import os # Only enable reload in local development (not in Docker) - check both variables enable_reload = ( os.getenv("ENABLE_RELOAD", "false").lower() == "true" or os.getenv("API_RELOAD", "false").lower() == "true" ) if enable_reload: uvicorn.run( "main:app", host="0.0.0.0", port=8000, reload=True, reload_includes=["*.py"], reload_dirs=["app"], reload_excludes=[".git/*", "*.pyc", "__pycache__/*", "logs/*", "uploads/*", "data/*"], log_level="info" ) else: uvicorn.run( "main:app", host="0.0.0.0", port=8000, reload=False )