main.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. from fastapi import FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect, Query
  2. from fastapi.staticfiles import StaticFiles
  3. from fastapi.middleware.cors import CORSMiddleware
  4. from fastapi.exceptions import RequestValidationError
  5. from fastapi.responses import JSONResponse
  6. import traceback
  7. import os
  8. import auth_utils
  9. import session_utils
  10. from services.global_manager import global_manager
  11. from services.chat_manager import manager
  12. import locales
  13. import config
  14. from routers import auth, orders, catalog, portfolio, files, chat, blog, admin, contact, warehouse
  15. app = FastAPI(title="Radionica 3D API")
  16. # Configure CORS
  17. origins = [
  18. "http://localhost:5173",
  19. "http://127.0.0.1:5173",
  20. "http://localhost:5000",
  21. "https://radionica3d.me",
  22. ]
  23. extra_origins = os.getenv("CORS_ORIGINS")
  24. if extra_origins:
  25. origins.extend(extra_origins.split(","))
  26. app.add_middleware(
  27. CORSMiddleware,
  28. allow_origins=origins,
  29. allow_credentials=True,
  30. allow_methods=["*"],
  31. allow_headers=["*"],
  32. )
  33. @app.exception_handler(RequestValidationError)
  34. async def validation_exception_handler(request: Request, exc: RequestValidationError):
  35. lang = request.query_params.get("lang", "en")
  36. errors = []
  37. for error in exc.errors():
  38. error_type = error.get("type", "unknown")
  39. ctx = error.get("ctx", {})
  40. translated_msg = locales.translate_error(error_type, lang, **ctx)
  41. loc = ".".join(str(l) for l in error.get("loc", [])[1:])
  42. errors.append({
  43. "loc": error.get("loc"),
  44. "msg": f"{loc}: {translated_msg}" if loc else translated_msg,
  45. "type": error_type
  46. })
  47. return JSONResponse(status_code=422, content={"detail": errors})
  48. @app.exception_handler(Exception)
  49. async def all_exception_handler(request: Request, exc: Exception):
  50. print(f"ERROR: {exc}")
  51. traceback.print_exc()
  52. if config.DEBUG:
  53. return JSONResponse(
  54. status_code=500,
  55. content={"detail": str(exc), "traceback": traceback.format_exc()}
  56. )
  57. return JSONResponse(status_code=500, content={"detail": "Internal server error"})
  58. # Add custom exception logging or other middleware here if needed
  59. # Include Routers
  60. app.include_router(auth.router)
  61. app.include_router(orders.router)
  62. app.include_router(catalog.router)
  63. app.include_router(portfolio.router)
  64. app.include_router(files.router)
  65. app.include_router(chat.router)
  66. app.include_router(blog.router)
  67. app.include_router(admin.router)
  68. app.include_router(contact.router)
  69. app.include_router(warehouse.router)
  70. # WebSocket Global Handler (Centralized to handle various proxy prefixes)
  71. @app.websocket("/global")
  72. async def ws_global(websocket: WebSocket, token: str = Query(...)):
  73. payload = auth_utils.decode_token(token)
  74. if not payload:
  75. await websocket.close(code=4001)
  76. return
  77. user_id = payload.get("id")
  78. role = payload.get("role")
  79. if not user_id:
  80. await websocket.close(code=4001)
  81. return
  82. await global_manager.connect(websocket, user_id, role)
  83. session_utils.track_user_ping(user_id)
  84. # Send initial unread count
  85. if role != 'admin':
  86. await global_manager.notify_user(user_id)
  87. else:
  88. await global_manager.notify_admins()
  89. try:
  90. while True:
  91. data = await websocket.receive_text()
  92. if data == "ping":
  93. session_utils.track_user_ping(user_id)
  94. except WebSocketDisconnect:
  95. global_manager.disconnect(websocket, user_id)
  96. except Exception as e:
  97. global_manager.disconnect(websocket, user_id)
  98. @app.websocket("/chat")
  99. async def ws_chat(websocket: WebSocket, token: str = Query(...), order_id: int = Query(...)):
  100. payload = auth_utils.decode_token(token)
  101. if not payload:
  102. await websocket.close(code=4001)
  103. return
  104. role = payload.get("role")
  105. user_id = payload.get("id")
  106. if role != 'admin':
  107. user_info = db.execute_query("SELECT can_chat FROM users WHERE id = %s", (user_id,))
  108. if not user_info or not user_info[0]['can_chat']:
  109. await websocket.close(code=4003)
  110. return
  111. order = db.execute_query("SELECT user_id FROM orders WHERE id = %s", (order_id,))
  112. if not order:
  113. await websocket.close(code=4004)
  114. return
  115. if role != 'admin' and order[0]['user_id'] != user_id:
  116. await websocket.close(code=4003)
  117. return
  118. await manager.connect(websocket, order_id, role)
  119. try:
  120. while True:
  121. data = await websocket.receive_text()
  122. if data == "typing":
  123. await manager.broadcast_to_order(order_id, {"type": "typing", "is_admin": role == 'admin'})
  124. elif data == "stop_typing":
  125. await manager.broadcast_to_order(order_id, {"type": "stop_typing", "is_admin": role == 'admin'})
  126. elif data == "read":
  127. if role == 'admin':
  128. db.execute_commit("UPDATE order_messages SET is_read = TRUE WHERE order_id = %s AND is_from_admin = FALSE AND is_read = FALSE", (order_id,))
  129. await global_manager.notify_admins()
  130. await global_manager.notify_order_read(order_id)
  131. else:
  132. db.execute_commit("UPDATE order_messages SET is_read = TRUE WHERE order_id = %s AND is_from_admin = TRUE AND is_read = FALSE", (order_id,))
  133. await global_manager.notify_user(user_id)
  134. except WebSocketDisconnect:
  135. manager.disconnect(websocket, order_id)
  136. except Exception as e:
  137. manager.disconnect(websocket, order_id)
  138. # Mount Static Files
  139. if not os.path.exists("uploads"):
  140. os.makedirs("uploads")
  141. # Mount static files for uploads and previews with caching
  142. app.mount("/uploads", StaticFiles(directory="uploads", html=False), name="uploads")
  143. @app.middleware("http")
  144. async def add_cache_control_header(request, call_next):
  145. response = await call_next(request)
  146. if request.url.path.startswith("/uploads"):
  147. response.headers["Cache-Control"] = "public, max-age=604800, immutable"
  148. return response
  149. if __name__ == "__main__":
  150. import uvicorn
  151. uvicorn.run(app, host="127.0.0.1", port=8000)