ifixkart-backend/Backend/app/main.py

303 lines
12 KiB
Python

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.")