From 0e71fe7abe779154d6b9febbf6c700358e0a7490 Mon Sep 17 00:00:00 2001 From: Lev Date: Thu, 16 Apr 2026 21:07:58 +0300 Subject: [PATCH] Fix + Docker --- .gitignore | 3 +- API/Dockerfile | 7 +++++ API/models/server.py | 3 +- API/requirements.txt | 2 +- API/routers/config.py | 5 ++-- API/routers/server.py | 11 ++++--- API/schemas/server.py | 4 +++ API/utils/database.py | 4 +-- API/utils/xui.py | 68 +++++++++++++++++++++++++++++++------------ docker-compose.yml | 20 +++++++++++-- 10 files changed, 92 insertions(+), 35 deletions(-) create mode 100644 API/Dockerfile diff --git a/.gitignore b/.gitignore index cf397e1..f9861ea 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ API/__pycache__ API/models/__pycache__ API/routers/__pycache__ -API/schemas/__pycache__ \ No newline at end of file +API/schemas/__pycache__ +API/utils/__pycache__ \ No newline at end of file diff --git a/API/Dockerfile b/API/Dockerfile new file mode 100644 index 0000000..338fccb --- /dev/null +++ b/API/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.14.4-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +EXPOSE 8000 +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/API/models/server.py b/API/models/server.py index cd8c631..e371997 100644 --- a/API/models/server.py +++ b/API/models/server.py @@ -9,10 +9,11 @@ class Server(Base): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) name: Mapped[str] = mapped_column(String(64), nullable=False, unique=True) code: Mapped[str] = mapped_column(String(128), nullable=False, unique=True, index=True) - host: Mapped[str] = mapped_column(String(128), nullable=False, unique=True) + host: Mapped[str] = mapped_column(String(128), nullable=False) port: Mapped[int] = mapped_column(nullable=False) user: Mapped[str] = mapped_column(String(64), nullable=False) password: Mapped[str] = mapped_column(String(64), nullable=False) inbound_id: Mapped[int] = mapped_column(nullable=False) + version: Mapped[str] = mapped_column(String(16), nullable=False, default="stable") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False) deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) diff --git a/API/requirements.txt b/API/requirements.txt index d12fedb..e531270 100644 --- a/API/requirements.txt +++ b/API/requirements.txt @@ -4,4 +4,4 @@ psycopg2-binary asyncpg passlib bcrypt==4.3.0 -httpx \ No newline at end of file +httpx diff --git a/API/routers/config.py b/API/routers/config.py index a54d36c..27c91c3 100644 --- a/API/routers/config.py +++ b/API/routers/config.py @@ -42,7 +42,6 @@ async def create_config(body: ConfigCreate, current_user: User = Depends(get_cur client_email_xui = f"{current_user.email}-{body.name}" display_name = f"SpectralVPN-{body.name}" config_url = await xui.add_client( - inbound_id=server.inbound_id, client_email=client_email_xui, display_name=display_name ) @@ -119,10 +118,10 @@ async def delete_config(body: ConfigDelete, current_user: User = Depends(get_cur .options(selectinload(User.server)) ) server = server_result.scalar_one().server - if server and server.inbound_id: + if server: xui = await XUIClient.from_server(server) client_email_xui = f"{current_user.email}-{body.name}" - await xui.delete_client(server.inbound_id, client_email_xui) + await xui.delete_client(client_email_xui) except: HTTPException( status_code=500, diff --git a/API/routers/server.py b/API/routers/server.py index ea14886..3369b67 100644 --- a/API/routers/server.py +++ b/API/routers/server.py @@ -10,27 +10,26 @@ router = APIRouter(prefix="/server") @router.post("/add", status_code=201) async def add_server(body: ServerAdd, request: Request, db: AsyncSession = Depends(get_db)): - ip = request.client.host existing = await db.execute( select(Server).where( (Server.name == body.name) | - (Server.code == body.code) | - (Server.host == ip) + (Server.code == body.code) ) ) if existing.scalar_one_or_none(): raise HTTPException( status_code=409, - detail="Name, ip or code is already exists" + detail="Nameor code is already exists" ) new_server = Server( name=body.name, code=body.code, - host=ip, + host=body.host, port=body.port, user=body.user, password=body.password, - inbound_id=body.inbound_id + inbound_id=body.inbound_id, + version=body.version ) db.add(new_server) await db.commit() diff --git a/API/schemas/server.py b/API/schemas/server.py index b7d2c9c..f9ff915 100644 --- a/API/schemas/server.py +++ b/API/schemas/server.py @@ -1,4 +1,5 @@ from datetime import datetime +from typing import Literal from pydantic import BaseModel, Field class ServerBase(BaseModel): @@ -7,9 +8,11 @@ class ServerBase(BaseModel): password: str = Field(max_length=64) class ServerAdd(ServerBase): + host: str = Field(max_length=128) port: int = Field(ge=1, le=65536) code: str = Field(max_length=128) inbound_id: int + version: Literal["legacy", "stable"] class ServerDel(ServerBase): pass @@ -19,3 +22,4 @@ class ServerInfo(BaseModel): name: str = Field(max_length=64) code: str created_at: datetime + version: str diff --git a/API/utils/database.py b/API/utils/database.py index 772775d..33ba21c 100644 --- a/API/utils/database.py +++ b/API/utils/database.py @@ -3,9 +3,7 @@ from typing import AsyncGenerator from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker,AsyncEngine engine: AsyncEngine = create_async_engine( - #Todo Изменить на env, плдключение к db - f"postgresql+asyncpg://spectral:spectral@localhost:5432/spectral", - echo=True, #Todo удалить + f"postgresql+asyncpg://{getenv("DB_USER")}:{getenv("DB_PASSWORD")}@db:5432/{getenv("DB_NAME")}", pool_pre_ping=True ) diff --git a/API/utils/xui.py b/API/utils/xui.py index cf6fc22..c80114b 100644 --- a/API/utils/xui.py +++ b/API/utils/xui.py @@ -1,22 +1,26 @@ import json import uuid import random +from urllib.parse import quote from httpx import AsyncClient from fastapi import HTTPException class XUIClient: - def __init__(self, base_url: str, username: str, password: str): + def __init__(self, host: str, base_url: str, username: str, password: str, inbound_id: int, version: str): + self.host = host self.base_url = base_url.rstrip("/") self.username = username self.password = password + self.inbound_id = inbound_id self.session = AsyncClient(timeout=20.0) self.logged_in = False + self.version = version @classmethod async def from_server(cls, server): #TODO заменить на https base_url = f"http://{server.host}:{server.port}" - return cls(base_url, server.user, server.password) + return cls(server.host, base_url, server.user, server.password, server.inbound_id, server.version) async def _login(self): if self.logged_in: @@ -34,28 +38,28 @@ class XUIClient: detail="Server error" ) - async def add_client(self, inbound_id: int, client_email: str, display_name: str) -> str: + async def add_client(self, client_email: str, display_name: str) -> str: await self._login() resp = await self.session.post(f"{self.base_url}/panel/inbound/list") resp.raise_for_status() data = resp.json() - inbounds = data.get("obj", []) - inbound = next((i for i in inbounds if i.get("id") == inbound_id), None) + inbounds = data.get("obj") + inbound = next((i for i in inbounds if i.get("id") == self.inbound_id), None) if not inbound: raise HTTPException( status_code=404, detail=f"Server not found" ) - stream_settings = json.loads(inbound.get("streamSettings", "{}")) - reality = stream_settings.get("realitySettings", {}) - short_ids = reality.get("shortIds", [""]) - server_names = reality.get("serverNames", {}) + stream_settings = json.loads(inbound.get("streamSettings")) + reality = stream_settings.get("realitySettings") + short_ids = reality.get("shortIds") + server_names = reality.get("serverNames") suid = random.choice(short_ids) sni = random.choice(server_names) - pbk = reality.get("settings", {}).get("publicKey", "") + pbk = reality.get("settings").get("publicKey") client_uuid = str(uuid.uuid4()) add_payload = { - "id": inbound_id, + "id": self.inbound_id, "settings": json.dumps({ "clients": [{ "id": client_uuid, @@ -74,15 +78,43 @@ class XUIClient: } resp = await self.session.post(f"{self.base_url}/panel/inbound/addClient", json=add_payload) resp.raise_for_status() - host = inbound.get("remark") + resp = await self.session.post(f"{self.base_url}/panel/inbound/list") + resp.raise_for_status() + data = resp.json() + inbounds = data.get("obj") + inbound = next((i for i in inbounds if i.get("id") == self.inbound_id), None) + if not inbound: + raise HTTPException( + status_code=500, + detail="Server error" + ) + settings = json.loads(inbound.get("settings")) + clients = settings.get("clients") + current_client = next((i for i in clients if i.get("email") == client_email), None) + if not current_client: + raise HTTPException( + status_code=500, + detail="Server error" + ) + client_uuid = current_client.get("id") + sub_id = current_client.get("subId") + stream_settings = json.loads(inbound.get("streamSettings")) + reality = stream_settings.get("realitySettings") + server_names = reality.get("serverNames") + setting = reality.get("settings") + pbk = setting.get("publicKey") + fp = setting.get("fingerprint") + spx = quote(setting.get("spiderX"), safe='') + sni = random.choice(server_names) + port = inbound.get("port") config_url = ( - f"vless://{client_uuid}@{host}?" + f"vless://{client_uuid}@{self.host}:{port}?" f"security=reality&" f"pbk={pbk}&" - f"fp=random&" + f"fp={fp}&" f"sni={sni}&" - f"sid={suid}&" - f"spx=%2F&" + f"sid={sub_id}&" + f"spx={spx}&" f"flow=xtls-rprx-vision#" f"{display_name}" ) @@ -101,11 +133,11 @@ class XUIClient: except: return 0 - async def delete_client(self, inbound_id: int, client_email: str): + async def delete_client(self, client_email: str): await self._login() try: resp = await self.session.post( - f"{self.base_url}/panel/inbound/{inbound_id}/delClientByEmail/{client_email}" + f"{self.base_url}/panel/inbound/{self.inbound_id}/delClientByEmail/{client_email}" ) resp.raise_for_status() return diff --git a/docker-compose.yml b/docker-compose.yml index 518e6b2..52fc2e5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,22 @@ services: - #api: - # Todo + api: + build: + context: ./API + dockerfile: Dockerfile + container_name: spectralvpn-api + restart: unless-stopped + depends_on: + db: + condition: service_healthy + environment: + DB_NAME: ${DB_NAME} + DB_USER: ${DB_USER} + DB_PASSWORD: ${DB_PASSWORD} + extra_hosts: + - host.docker.internal:172.17.0.1 + ports: + - 8000:8000 + #web: # Todo db: