This commit is contained in:
2026-04-27 14:52:15 +03:00
Unverified
parent 66a1060685
commit c7f4eb8a48
5 changed files with 88 additions and 26 deletions
+1
View File
@@ -576,6 +576,7 @@ buck-out/
# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) # 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/data
backend/files backend/files
.vite .vite
+1 -1
View File
@@ -19,4 +19,4 @@ slowapi>=0.1.9
firebase_admin>=7.1.0 firebase_admin>=7.1.0
PyNaCl>=1.5.0 PyNaCl>=1.5.0
numpy numpy
livekit-api>=0.8.0 livekit-api>=1.0.0,<2
+40
View File
@@ -1,6 +1,8 @@
from datetime import datetime from datetime import datetime
from collections import defaultdict, deque from collections import defaultdict, deque
import logging
import time import time
from pathlib import Path
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status, Request from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status, Request
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import inspect, text 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 ..security.rate_limit import rate_limit_per_ip
from ..key_lifecycle import destroy_message_keys_for_user from ..key_lifecycle import destroy_message_keys_for_user
router = APIRouter() 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_WINDOW_SECONDS = 300
_FAILED_ATTEMPT_THRESHOLD = 5 _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 "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") @router.get("/check_auth")
def check_auth(current_user: User = Depends(get_current_user)): def check_auth(current_user: User = Depends(get_current_user)):
return { return {
+42 -23
View File
@@ -1,38 +1,57 @@
import express from 'express'; import http from "http";
import { createProxyMiddleware } from 'http-proxy-middleware'; import type { IncomingMessage } from "http";
import { resolve } from 'path'; import type { Duplex } from "stream";
import express from "express";
import { createProxyMiddleware } from "http-proxy-middleware";
import { resolve } from "path";
const app = express(); const app = express();
const port = process.env.PORT || 3000; /** Same default as Vite dev server: HTTPS reverse-proxy entry (API + WebSocket on /api). */
const lan = process.env.LAN_IP; const port = Number(process.env.PORT) || 8301;
const backendHost = const backendHost = process.env.BACKEND_HOST || "http://127.0.0.1:8300";
process.env.BACKEND_HOST || (lan ? `http://${lan}:8300` : "http://localhost:8300"); const fileStorageHost = process.env.FILE_STORAGE_HOST || "http://localhost:8302";
const fileStorageHost =
process.env.FILE_STORAGE_HOST || (lan ? `http://${lan}:8302` : "http://localhost:8302");
const filePath = process.env.STATIC_FILE_PATH || "."; const filePath = process.env.STATIC_FILE_PATH || ".";
// API proxy middleware // API + WebSocket (e.g. /api/chat/ws) — must attach upgrade on the HTTP server, not app.listen().
app.use('/api', createProxyMiddleware({ const apiProxy = createProxyMiddleware({
target: backendHost, target: backendHost,
changeOrigin: true, changeOrigin: true,
pathRewrite: { '^/api': '' }, pathRewrite: { "^/api": "" },
ws: true ws: true,
})); });
// File serving proxy middleware app.use("/api", apiProxy);
app.use('/uploads/files', createProxyMiddleware({
app.use(
"/uploads/files",
createProxyMiddleware({
target: fileStorageHost, target: fileStorageHost,
changeOrigin: true changeOrigin: true,
})); }),
);
// Serve static files
app.use(express.static(resolve(filePath))); app.use(express.static(resolve(filePath)));
// SPA routing - catch all handler for client-side routing
app.use((_req, res) => { app.use((_req, res) => {
res.sendFile(resolve(filePath, 'index.html')); res.sendFile(resolve(filePath, "index.html"));
}); });
app.listen(port, () => { const server = http.createServer(app);
console.log(`Server launched on http://localhost:${port}`);
type ProxyWithUpgrade = ReturnType<typeof createProxyMiddleware> & {
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})`);
}); });
+3 -1
View File
@@ -77,6 +77,7 @@ export default defineConfig({
"@fromchat/protocol": path.resolve(__dirname, "./packages/fromchat-protocol/src/index.ts") "@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: { server: {
host: '0.0.0.0', host: '0.0.0.0',
port: 8301, port: 8301,
@@ -86,7 +87,8 @@ export default defineConfig({
target: _backendProxy, target: _backendProxy,
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""), rewrite: (path) => path.replace(/^\/api/, ""),
ws: true ws: true,
proxyTimeout: 0,
} }
}, },
allowedHosts: [ allowedHosts: [