main.py 6.4 KB

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