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)
backend/services/main/.fromchat_instance_id
backend/data
backend/files
.vite
+1 -1
View File
@@ -19,4 +19,4 @@ slowapi>=0.1.9
firebase_admin>=7.1.0
PyNaCl>=1.5.0
numpy
livekit-api>=0.8.0
livekit-api>=1.0.0,<2
+40
View File
@@ -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 {
+42 -23
View File
@@ -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({
app.use("/api", apiProxy);
app.use(
"/uploads/files",
createProxyMiddleware({
target: fileStorageHost,
changeOrigin: true
}));
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<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")
}
},
// 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: [