303 lines
12 KiB
Python
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.")
|
|
|
|
|