From c7f4eb8a485be200fd274bc143cee0a92f90f6db Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Mon, 27 Apr 2026 14:52:15 +0300 Subject: [PATCH] Fix --- .gitignore | 1 + backend/requirements.txt | 2 +- backend/services/main/routes/account.py | 40 +++++++++++++++ deployment/frontend/server.ts | 67 ++++++++++++++++--------- frontend/vite.config.ts | 4 +- 5 files changed, 88 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index 823971d..804b658 100644 --- a/.gitignore +++ b/.gitignore @@ -576,6 +576,7 @@ buck-out/ # Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) +backend/services/main/.fromchat_instance_id backend/data backend/files .vite diff --git a/backend/requirements.txt b/backend/requirements.txt index 23f41c5..e14deca 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -19,4 +19,4 @@ slowapi>=0.1.9 firebase_admin>=7.1.0 PyNaCl>=1.5.0 numpy -livekit-api>=0.8.0 \ No newline at end of file +livekit-api>=1.0.0,<2 diff --git a/backend/services/main/routes/account.py b/backend/services/main/routes/account.py index 706c5ef..54df405 100644 --- a/backend/services/main/routes/account.py +++ b/backend/services/main/routes/account.py @@ -1,6 +1,8 @@ from datetime import datetime from collections import defaultdict, deque +import logging import time +from pathlib import Path from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status, Request from sqlalchemy.orm import Session from sqlalchemy import inspect, text @@ -20,6 +22,38 @@ from ..security.profanity import contains_profanity from ..security.rate_limit import rate_limit_per_ip from ..key_lifecycle import destroy_message_keys_for_user router = APIRouter() +_logger = logging.getLogger(__name__) + +_SERVER_INSTANCE_ID: str | None = None +_INSTANCE_ID_FILE = Path(__file__).resolve().parent.parent / ".fromchat_instance_id" + + +def get_server_instance_id() -> str: + """Stable server fingerprint; UUID generated once and persisted next to the main service package.""" + global _SERVER_INSTANCE_ID + if _SERVER_INSTANCE_ID is not None: + return _SERVER_INSTANCE_ID + path = _INSTANCE_ID_FILE + try: + if path.is_file(): + text = path.read_text(encoding="utf-8").strip() + if text: + _SERVER_INSTANCE_ID = text + return _SERVER_INSTANCE_ID + except OSError as exc: + _logger.warning("Could not read instance id from %s: %s", path, exc) + iid = str(uuid.uuid4()) + try: + path.write_text(iid + "\n", encoding="utf-8") + except OSError as exc: + _logger.warning( + "Could not persist instance id to %s (%s); using in-process id only.", + path, + exc, + ) + _SERVER_INSTANCE_ID = iid + return iid + _FAILED_ATTEMPT_WINDOW_SECONDS = 300 _FAILED_ATTEMPT_THRESHOLD = 5 @@ -71,6 +105,12 @@ def convert_user(user: User) -> dict: "deleted": (user.deleted or user.suspended) or False # Treat suspended as deleted } +@router.get("/instance_id") +def get_instance_id_public(): + """Public deploy fingerprint (used when the client changes server host/port).""" + return {"instance_id": get_server_instance_id()} + + @router.get("/check_auth") def check_auth(current_user: User = Depends(get_current_user)): return { diff --git a/deployment/frontend/server.ts b/deployment/frontend/server.ts index a4579a6..d358d9c 100644 --- a/deployment/frontend/server.ts +++ b/deployment/frontend/server.ts @@ -1,38 +1,57 @@ -import express from 'express'; -import { createProxyMiddleware } from 'http-proxy-middleware'; -import { resolve } from 'path'; +import http from "http"; +import type { IncomingMessage } from "http"; +import type { Duplex } from "stream"; +import express from "express"; +import { createProxyMiddleware } from "http-proxy-middleware"; +import { resolve } from "path"; const app = express(); -const port = process.env.PORT || 3000; -const lan = process.env.LAN_IP; -const backendHost = - process.env.BACKEND_HOST || (lan ? `http://${lan}:8300` : "http://localhost:8300"); -const fileStorageHost = - process.env.FILE_STORAGE_HOST || (lan ? `http://${lan}:8302` : "http://localhost:8302"); +/** Same default as Vite dev server: HTTPS reverse-proxy entry (API + WebSocket on /api). */ +const port = Number(process.env.PORT) || 8301; +const backendHost = process.env.BACKEND_HOST || "http://127.0.0.1:8300"; +const fileStorageHost = process.env.FILE_STORAGE_HOST || "http://localhost:8302"; const filePath = process.env.STATIC_FILE_PATH || "."; -// API proxy middleware -app.use('/api', createProxyMiddleware({ +// API + WebSocket (e.g. /api/chat/ws) — must attach upgrade on the HTTP server, not app.listen(). +const apiProxy = createProxyMiddleware({ target: backendHost, changeOrigin: true, - pathRewrite: { '^/api': '' }, - ws: true -})); + pathRewrite: { "^/api": "" }, + ws: true, +}); -// File serving proxy middleware -app.use('/uploads/files', createProxyMiddleware({ - target: fileStorageHost, - changeOrigin: true -})); +app.use("/api", apiProxy); + +app.use( + "/uploads/files", + createProxyMiddleware({ + target: fileStorageHost, + changeOrigin: true, + }), +); -// Serve static files app.use(express.static(resolve(filePath))); -// SPA routing - catch all handler for client-side routing app.use((_req, res) => { - res.sendFile(resolve(filePath, 'index.html')); + res.sendFile(resolve(filePath, "index.html")); }); -app.listen(port, () => { - console.log(`Server launched on http://localhost:${port}`); +const server = http.createServer(app); + +type ProxyWithUpgrade = ReturnType & { + upgrade?: (req: IncomingMessage, socket: Duplex, head: Buffer) => void; +}; + +server.on("upgrade", (req, socket, head) => { + const path = req.url?.split("?")[0] ?? ""; + const upgrade = (apiProxy as ProxyWithUpgrade).upgrade; + if (path.startsWith("/api") && upgrade) { + upgrade.call(apiProxy, req, socket, head); + } else { + socket.destroy(); + } +}); + +server.listen(port, () => { + console.log(`Server listening on http://localhost:${port} (API+WS → ${backendHost})`); }); diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 554b21f..012b201 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -77,6 +77,7 @@ export default defineConfig({ "@fromchat/protocol": path.resolve(__dirname, "./packages/fromchat-protocol/src/index.ts") } }, + // Dev entry on 8301: browser uses same-origin `/api` (HTTP + WebSocket, e.g. `/api/chat/ws`). server: { host: '0.0.0.0', port: 8301, @@ -86,7 +87,8 @@ export default defineConfig({ target: _backendProxy, changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, ""), - ws: true + ws: true, + proxyTimeout: 0, } }, allowedHosts: [