import os import hashlib from fastapi import FastAPI, Request, Response, status from fastapi.responses import JSONResponse from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.gzip import GZipMiddleware from fastapi.staticfiles import StaticFiles from app.core.config.Config import settings # Ensure all database models are loaded into the SQLAlchemy registry import app.models.db_base # Router Imports from app.api.v1.routers.AuthenticationRouter import router as auth_router from app.api.v1.routers.UserCreationRouter import router as user_router from app.api.v1.routers.MasterDataRouter import router as geo_router from app.api.v1.routers.SettingsRouter import router as setting_router from app.api.v1.routers.FileRouter import router as file_router from app.api.v1.routers.MfaRouter import router as mfa_router from app.api.v1.routers.AdminSecurityRouter import router as admin_security_router from app.api.v1.routers.RolePermissionRouter import router as role_perm_router from app.api.v1.routers.CatalogRouter import router as catalog_router from app.api.v1.routers.MigrationRouter import router as migration_router from app.api.v1.routers.storefront import router as public_storefront_router from app.api.v1.routers.admin_storefront import router as admin_storefront_router from app.api.v1.routers.CustomerAuthRouter import router as customer_auth_router from app.api.v1.routers.CustomerProfileRouter import router as customer_profile_router from app.api.v1.routers.CartRouter import router as cart_router from app.api.v1.routers.CheckoutRouter import router as checkout_router from app.api.v1.routers.OrderRouter import router as order_router from app.api.v1.routers.PaymentRouter import router as payment_router from app.api.v1.routers.InvoiceRouter import router as invoice_router from app.api.v1.routers.PosSyncRouter import router as pos_sync_router from app.api.v1.routers.WishlistRouter import router as wishlist_router from app.api.v1.routers.ProductCompareRouter import router as product_compare_router from app.api.v1.routers.InventoryRouter import router as inventory_router from app.api.v1.routers.AdminOrderRouter import router as admin_order_router from app.api.v1.routers.AdminInvoiceRouter import router as admin_invoice_router from app.api.v1.routers.AdminCustomerRouter import router as admin_customer_router from app.api.v1.routers.DashboardRouter import router as dashboard_router from app.api.v1.routers.ServiceJobRouter import router as service_job_router # Middleware Imports from app.core.middleware.trace_middleware import RequestTraceMiddleware from app.core.middleware.security_middleware import SecurityHeadersMiddleware, RequestSizeLimitMiddleware from app.core.middleware.kill_switch_middleware import KillSwitchMiddleware from app.core.Exception import AppException app = FastAPI( title=settings.PROJECT_NAME, description="iFixKart Enterprise ERP/CRM/E-commerce Platform Backend Core Foundation", version="1.0.0" ) # Mount Uploads directory for media serving from pathlib import Path uploads_path = str(Path(__file__).resolve().parents[1] / "uploads") os.makedirs(uploads_path, exist_ok=True) app.mount("/uploads", StaticFiles(directory=uploads_path), name="uploads") # 1. CORS Configuration app.add_middleware( CORSMiddleware, allow_origins=[ "https://ifixkartdev.trionixsolution.com", "https://ifixkart.trionixsolution.com", "https://ifixkartbe.trionixsolution.com", "http://localhost:3021", "http://localhost:3022", "http://localhost:8000", "http://127.0.0.1:3021", "http://127.0.0.1:3022", "http://127.0.0.1:8000", ], allow_origin_regex=r"https?://.*", allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Enable Gzip Compression for large payloads (minimizing response size) app.add_middleware(GZipMiddleware, minimum_size=1000) # ETag and Cache-Control middleware for CDNs / Browsers caching @app.middleware("http") async def add_cache_headers_and_etag(request: Request, call_next): if request.method != "GET": return await call_next(request) response = await call_next(request) path = request.url.path # Only apply caching headers to static catalog/brand lists, NEVER layout configs which need instant admin updates if "/storefront/layout" in path: response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" response.headers["Pragma"] = "no-cache" response.headers["Expires"] = "0" return response is_cacheable = any(p in path for p in [ "/api/v1/catalog/products", "/api/v1/catalog/brands", "/api/v1/catalog/categories", "/api/v1/settings/public" ]) if is_cacheable and response.status_code == 200: body = b"" async for chunk in response.body_iterator: body += chunk etag_val = f'W/"{hashlib.md5(body).hexdigest()}"' if_none_match = request.headers.get("if-none-match") if if_none_match and if_none_match == etag_val: return Response(status_code=304, headers={ "ETag": etag_val, "Cache-Control": "public, max-age=60, must-revalidate" }) headers = dict(response.headers) headers["ETag"] = etag_val headers["Cache-Control"] = "public, max-age=60, must-revalidate" if "/layout/" in path: headers["Cache-Control"] = "public, max-age=300, must-revalidate" return Response( content=body, status_code=response.status_code, headers=headers, media_type=response.media_type ) return response # 2. Register Middleware Stack (Executed in reverse order of addition) from app.core.middleware.audit_middleware import AuditMiddleware app.add_middleware(AuditMiddleware) app.add_middleware(KillSwitchMiddleware) app.add_middleware(RequestSizeLimitMiddleware, max_bytes=3 * 1024 * 1024 * 1024) # Increased limit to 3GB app.add_middleware(SecurityHeadersMiddleware) app.add_middleware(RequestTraceMiddleware) from fastapi.exceptions import RequestValidationError from starlette.exceptions import HTTPException as StarletteHTTPException # 3. Register Global Handlers for Custom AppException with CORS safety @app.exception_handler(AppException) def app_exception_handler(request: Request, exc: AppException): req_id = getattr(request.state, "request_id", "unknown") response = JSONResponse( status_code=exc.status_code, content={"detail": exc.detail, "request_id": req_id} ) # Explicitly append CORS headers so browser doesn't block cross-origin error responses origin = request.headers.get("origin") if origin: response.headers["Access-Control-Allow-Origin"] = origin response.headers["Access-Control-Allow-Credentials"] = "true" return response @app.exception_handler(StarletteHTTPException) def http_exception_handler(request: Request, exc: StarletteHTTPException): response = JSONResponse( status_code=exc.status_code, content={"detail": exc.detail} ) origin = request.headers.get("origin") if origin: response.headers["Access-Control-Allow-Origin"] = origin response.headers["Access-Control-Allow-Credentials"] = "true" return response from fastapi.encoders import jsonable_encoder @app.exception_handler(RequestValidationError) def validation_exception_handler(request: Request, exc: RequestValidationError): response = JSONResponse( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content={"detail": jsonable_encoder(exc.errors())} ) origin = request.headers.get("origin") if origin: response.headers["Access-Control-Allow-Origin"] = origin response.headers["Access-Control-Allow-Credentials"] = "true" return response # 4. Register Router Endpoints app.include_router(auth_router) app.include_router(user_router) app.include_router(geo_router) app.include_router(setting_router) app.include_router(file_router) app.include_router(mfa_router) app.include_router(admin_security_router) app.include_router(role_perm_router) app.include_router(catalog_router) app.include_router(migration_router) app.include_router(public_storefront_router) app.include_router(admin_storefront_router) app.include_router(customer_auth_router) app.include_router(customer_profile_router) app.include_router(cart_router) app.include_router(checkout_router) app.include_router(order_router) app.include_router(payment_router) app.include_router(invoice_router) app.include_router(pos_sync_router) app.include_router(wishlist_router) app.include_router(product_compare_router) app.include_router(inventory_router) app.include_router(admin_order_router) app.include_router(admin_invoice_router) app.include_router(admin_customer_router) app.include_router(dashboard_router) app.include_router(service_job_router) @app.get("/") def read_root(): return { "status": "online", "project": settings.PROJECT_NAME, "version": "1.0.0" } # 5. Startup Hook for Automatic Database Initialization & Seeding & Media Worker @app.on_event("startup") def startup_event(): print("FastAPI Application Booting... Running automatic database schema verification and permission seeding...") from app.core.database.init_db import initialize_database try: initialize_database() print("Automatic database initialization and seeding completed successfully.") except Exception as e: print(f"Error during automatic database initialization: {e}") # Start Async Media Cleanup & Reconciliation Thread Worker import threading import time from app.core.database.db_session import SessionLocal from app.core.media.media_garbage_collector import process_async_media_cleanup, reconcile_media_database def run_media_background_worker(): last_cleanup = 0 last_reconcile = 0 while True: try: now = time.time() # Run hourly cleanup if now - last_cleanup >= 3600: last_cleanup = now db = SessionLocal() try: process_async_media_cleanup(db) finally: db.close() # Run daily reconciliation (86400s) if now - last_reconcile >= 86400: last_reconcile = now db = SessionLocal() try: reconcile_media_database(db) finally: db.close() except Exception as err: print(f"Error in Media Background Worker loop: {err}") time.sleep(300) # Check every 5 minutes worker_thread = threading.Thread(target=run_media_background_worker, daemon=True, name="MediaWorkerThread") worker_thread.start() print("Media Async Garbage Collector & Reconciliation Worker thread started.") # Start Persistent Migration Worker Background Thread from app.services.migration_engine.migration_worker import MigrationWorker def run_migration_background_worker(): worker = MigrationWorker() print(f"[{worker.worker_id}] Migration Worker daemon initialized and listening for queued jobs...") while True: try: db = SessionLocal() try: res = worker.claim_next_job(db) if res: job_id, lease_version = res worker.process_job(job_id, lease_version) finally: db.close() except Exception as err: print(f"Error in Migration Worker loop: {err}") time.sleep(2) mig_thread = threading.Thread(target=run_migration_background_worker, daemon=True, name="MigrationWorkerThread") mig_thread.start() print("Persistent Migration Worker background thread started.")