main.py 6.2 KB

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