from fastapi import FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect, Query from fastapi.staticfiles import StaticFiles from fastapi.middleware.cors import CORSMiddleware from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse import traceback import os import auth_utils import session_utils from services.global_manager import global_manager from services.chat_manager import manager import locales import config from routers import auth, orders, catalog, portfolio, files, chat, blog, admin, contact, warehouse app = FastAPI(title="Radionica 3D API") # Configure CORS origins = [ "http://localhost:5173", "http://127.0.0.1:5173", "http://localhost:5000", "https://radionica3d.me", ] extra_origins = os.getenv("CORS_ORIGINS") if extra_origins: origins.extend(extra_origins.split(",")) app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError): lang = request.query_params.get("lang", "en") errors = [] for error in exc.errors(): error_type = error.get("type", "unknown") ctx = error.get("ctx", {}) translated_msg = locales.translate_error(error_type, lang, **ctx) loc = ".".join(str(l) for l in error.get("loc", [])[1:]) errors.append({ "loc": error.get("loc"), "msg": f"{loc}: {translated_msg}" if loc else translated_msg, "type": error_type }) return JSONResponse(status_code=422, content={"detail": errors}) @app.exception_handler(Exception) async def all_exception_handler(request: Request, exc: Exception): print(f"ERROR: {exc}") traceback.print_exc() if config.DEBUG: return JSONResponse( status_code=500, content={"detail": str(exc), "traceback": traceback.format_exc()} ) return JSONResponse(status_code=500, content={"detail": "Internal server error"}) # Add custom exception logging or other middleware here if needed # Include Routers app.include_router(auth.router) app.include_router(orders.router) app.include_router(catalog.router) app.include_router(portfolio.router) app.include_router(files.router) app.include_router(chat.router) app.include_router(blog.router) app.include_router(admin.router) app.include_router(contact.router) app.include_router(warehouse.router) # WebSocket Global Handler (Centralized to handle various proxy prefixes) @app.websocket("/global") async def ws_global(websocket: WebSocket, token: str = Query(...)): payload = auth_utils.decode_token(token) if not payload: await websocket.close(code=4001) return user_id = payload.get("id") role = payload.get("role") if not user_id: await websocket.close(code=4001) return await global_manager.connect(websocket, user_id, role) session_utils.track_user_ping(user_id) # Send initial unread count if role != 'admin': await global_manager.notify_user(user_id) else: await global_manager.notify_admins() try: while True: data = await websocket.receive_text() if data == "ping": session_utils.track_user_ping(user_id) except WebSocketDisconnect: global_manager.disconnect(websocket, user_id) except Exception as e: global_manager.disconnect(websocket, user_id) @app.websocket("/chat") async def ws_chat(websocket: WebSocket, token: str = Query(...), order_id: int = Query(...)): payload = auth_utils.decode_token(token) if not payload: await websocket.close(code=4001) return role = payload.get("role") user_id = payload.get("id") if role != 'admin': user_info = db.execute_query("SELECT can_chat FROM users WHERE id = %s", (user_id,)) if not user_info or not user_info[0]['can_chat']: await websocket.close(code=4003) return order = db.execute_query("SELECT user_id FROM orders WHERE id = %s", (order_id,)) if not order: await websocket.close(code=4004) return if role != 'admin' and order[0]['user_id'] != user_id: await websocket.close(code=4003) return await manager.connect(websocket, order_id, role) try: while True: data = await websocket.receive_text() if data == "typing": await manager.broadcast_to_order(order_id, {"type": "typing", "is_admin": role == 'admin'}) elif data == "stop_typing": await manager.broadcast_to_order(order_id, {"type": "stop_typing", "is_admin": role == 'admin'}) elif data == "read": if role == 'admin': 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,)) await global_manager.notify_admins() await global_manager.notify_order_read(order_id) else: 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,)) await global_manager.notify_user(user_id) except WebSocketDisconnect: manager.disconnect(websocket, order_id) except Exception as e: manager.disconnect(websocket, order_id) # Mount Static Files if not os.path.exists("uploads"): os.makedirs("uploads") # Mount static files for uploads and previews with caching app.mount("/uploads", StaticFiles(directory="uploads", html=False), name="uploads") @app.middleware("http") async def add_cache_control_header(request, call_next): response = await call_next(request) if request.url.path.startswith("/uploads"): response.headers["Cache-Control"] = "public, max-age=604800, immutable" return response if __name__ == "__main__": import uvicorn uvicorn.run(app, host="127.0.0.1", port=8000)