main.py 6.9 KB

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