bmc_hub/main.py

391 lines
16 KiB
Python
Raw Normal View History

2025-12-05 14:22:39 +01:00
"""
BMC Hub - FastAPI Application
Main application entry point
"""
import logging
from pathlib import Path
from fastapi import FastAPI, Request
2025-12-05 14:22:39 +01:00
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import RedirectResponse
2025-12-05 14:22:39 +01:00
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.alert_notes.backend import router as alert_notes_api
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.dashboard.backend import mission_router as mission_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
2026-01-28 07:48:10 +01:00
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
2025-12-05 14:22:39 +01:00
# 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
2025-12-05 14:22:39 +01:00
# 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)
2025-12-05 14:22:39 +01:00
logger.info("✅ System initialized successfully")
yield
# Shutdown
backup_scheduler.stop()
2025-12-05 14:22:39 +01:00
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
2025-12-05 14:22:39 +01:00
app.add_middleware(
CORSMiddleware,
allow_origins=cors_origins,
2025-12-05 14:22:39 +01:00
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")
public_paths.add("/api/v1/mission/webhook/telefoni/ringing")
public_paths.add("/api/v1/mission/webhook/telefoni/answered")
public_paths.add("/api/v1/mission/webhook/telefoni/hangup")
public_paths.add("/api/v1/mission/webhook/uptime")
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)
2025-12-05 14:22:39 +01:00
# 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(alert_notes_api, prefix="/api/v1", tags=["Alert Notes"])
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(mission_api.router, prefix="/api/v1", tags=["Mission"])
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"])
2026-01-28 07:48:10 +01:00
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"])
2026-01-28 07:48:10 +01:00
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"])
2025-12-05 14:22:39 +01:00
# Serve static files (UI)
app.mount("/static", StaticFiles(directory="static", html=True), name="static")
app.mount("/docs", StaticFiles(directory="docs"), name="docs")
2025-12-05 14:22:39 +01:00
@app.get("/health")
async def health_check():
"""Health check endpoint"""
return {
"status": "healthy",
"service": "BMC Hub",
"version": get_version()
2025-12-05 14:22:39 +01:00
}
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"
)
2025-12-05 14:22:39 +01:00
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/*"],
2025-12-05 14:22:39 +01:00
log_level="info"
)
else:
uvicorn.run(
"main:app",
host="0.0.0.0",
port=8000,
reload=False
)