Refactor API

This commit is contained in:
Lev 2026-04-16 00:07:48 +03:00
parent 9c813cdfbe
commit 8aa4828239
21 changed files with 731 additions and 363 deletions

65
API/utils/auth.py Normal file
View file

@ -0,0 +1,65 @@
import secrets
import hashlib
from datetime import datetime, timedelta
from typing import Annotated
from fastapi import Depends, HTTPException, status
from fastapi.security import APIKeyHeader
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from passlib.context import CryptContext
from models.token import AuthToken
from models.user import User
from utils.database import get_db
pwd_context = CryptContext(schemes=["bcrypt"])
API_KEY_HEADER = APIKeyHeader(name="X-API-KEY", auto_error=False)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def hash_token(raw_token: str) -> str:
return hashlib.sha256(raw_token.encode('utf-8')).hexdigest()
async def create_token(user_id: int, db: AsyncSession) -> str:
raw_token = secrets.token_urlsafe(48)
token_hash = hash_token(raw_token)
new_token = AuthToken(
user_id=user_id,
token_hash=token_hash,
expires_at=datetime.utcnow() + timedelta(days=30)
)
db.add(new_token)
await db.commit()
await db.refresh(new_token)
return raw_token
async def get_current_user(api_key: Annotated[str | None, Depends(API_KEY_HEADER)], db: AsyncSession = Depends(get_db)) -> User:
if not api_key:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token is not valid"
)
token_hash = hash_token(api_key)
result = await db.execute(
select(AuthToken).where(
AuthToken.token_hash == token_hash,
AuthToken.revoked == False,
AuthToken.expires_at > datetime.utcnow()
)
)
token_record = result.scalar_one_or_none()
if not token_record:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token is expired"
)
user = await db.get(User, token_record.user_id)
if not user or user.deleted_at is not None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User is not found"
)
return user

26
API/utils/database.py Normal file
View file

@ -0,0 +1,26 @@
from os import getenv
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 удалить
pool_pre_ping=True
)
AsyncSessionLocal = async_sessionmaker(
bind=engine,
class_=AsyncSession,
autocommit=False,
autoflush=False,
expire_on_commit=False
)
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
try:
yield session
finally:
await session.close()

119
API/utils/xui.py Normal file
View file

@ -0,0 +1,119 @@
import json
import uuid
import random
from httpx import AsyncClient
from fastapi import HTTPException
class XUIClient:
def __init__(self, base_url: str, username: str, password: str):
self.base_url = base_url.rstrip("/")
self.username = username
self.password = password
self.session = AsyncClient(timeout=20.0)
self.logged_in = False
@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)
async def _login(self):
if self.logged_in:
return
try:
resp = await self.session.post(
f"{self.base_url}/login",
json={"username": self.username, "password": self.password}
)
resp.raise_for_status()
self.logged_in = True
except:
raise HTTPException(
status_code=500,
detail="Server error"
)
async def add_client(self, inbound_id: int, 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)
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", {})
suid = random.choice(short_ids)
sni = random.choice(server_names)
pbk = reality.get("settings", {}).get("publicKey", "")
client_uuid = str(uuid.uuid4())
add_payload = {
"id": inbound_id,
"settings": json.dumps({
"clients": [{
"id": client_uuid,
"flow": "xtls-rprx-vision",
"email": client_email,
"limitIp": 0,
"totalGB": 0,
"expiryTime": 0,
"enable": True,
"tgId": "",
"subId": suid,
"comment": "",
"reset": 0
}]
})
}
resp = await self.session.post(f"{self.base_url}/panel/inbound/addClient", json=add_payload)
resp.raise_for_status()
host = inbound.get("remark")
config_url = (
f"vless://{client_uuid}@{host}?"
f"security=reality&"
f"pbk={pbk}&"
f"fp=random&"
f"sni={sni}&"
f"sid={suid}&"
f"spx=%2F&"
f"flow=xtls-rprx-vision#"
f"{display_name}"
)
return config_url
async def get_client_traffic(self, client_email: str) -> int:
await self._login()
try:
resp = await self.session.post(
f"{self.base_url}/panel/inbound/getClientTraffics/{client_email}"
)
resp.raise_for_status()
data = resp.json()
obj = data.get("obj", {})
return obj.get("down", 0) + obj.get("up", 0)
except:
return 0
async def delete_client(self, inbound_id: int, client_email: str):
await self._login()
try:
resp = await self.session.post(
f"{self.base_url}/panel/inbound/{inbound_id}/delClientByEmail/{client_email}"
)
resp.raise_for_status()
return
except:
HTTPException(
status_code=500,
detail="Server error"
)
async def close(self):
await self.session.aclose()