Refactor API
This commit is contained in:
parent
9c813cdfbe
commit
8aa4828239
21 changed files with 731 additions and 363 deletions
65
API/utils/auth.py
Normal file
65
API/utils/auth.py
Normal 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
26
API/utils/database.py
Normal 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
119
API/utils/xui.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue