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

133
API/routers/config.py Normal file
View file

@ -0,0 +1,133 @@
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from sqlalchemy.orm import selectinload
from models.user import User
from models.config import Config
from schemas.config import ConfigCreate, ConfigResponse, ConfigListResponse, ConfigDelete
from utils.auth import get_current_user
from utils.database import get_db
from utils.xui import XUIClient
router = APIRouter(prefix="/config")
@router.post("/add", status_code=201)
async def create_config(body: ConfigCreate, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
existing = await db.execute(
select(Config).where(
Config.user_id == current_user.id,
Config.name == body.name,
Config.deleted_at.is_(None)
)
)
if existing.scalar_one_or_none():
raise HTTPException(
status_code=409,
detail="Config name is invalid"
)
result = await db.execute(
select(User)
.where(User.id == current_user.id)
.options(selectinload(User.server))
)
user_with_server = result.scalar_one()
server = user_with_server.server
if not server or server.deleted_at is not None:
raise HTTPException(
status_code=404,
detail="The server not found"
)
xui = await XUIClient.from_server(server)
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
)
new_config = Config(
user_id=current_user.id,
name=body.name,
config=config_url
)
db.add(new_config)
await db.commit()
await db.refresh(new_config)
return ConfigResponse(
name=new_config.name,
config=new_config.config,
created_at=new_config.created_at,
bytes_used=0
)
@router.get("/get_info")
async def get_configs(current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(Config).where(
Config.user_id == current_user.id,
Config.deleted_at.is_(None)
).order_by(Config.created_at.desc())
)
configs_db = result.scalars().all()
configs = []
xui = None
for cfg in configs_db:
bytes_used = 0
try:
if xui is None:
server_result = await db.execute(
select(User)
.where(User.id == current_user.id)
.options(selectinload(User.server))
)
server = server_result.scalar_one().server
xui = await XUIClient.from_server(server)
client_email = f"{current_user.email}-{cfg.name}"
bytes_used = await xui.get_client_traffic(client_email)
except:
pass
configs.append(
ConfigResponse(
name=cfg.name,
config=cfg.config,
created_at=cfg.created_at,
bytes_used=bytes_used
)
)
return ConfigListResponse(configs=configs)
@router.delete("/delete", status_code=204)
async def delete_config(body: ConfigDelete, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(Config).where(
Config.user_id == current_user.id,
Config.name == body.name,
Config.deleted_at.is_(None)
)
)
config_record = result.scalar_one_or_none()
if not config_record:
raise HTTPException(
status_code=404,
detail="Config not found"
)
try:
server_result = await db.execute(
select(User)
.where(User.id == current_user.id)
.options(selectinload(User.server))
)
server = server_result.scalar_one().server
if server and server.inbound_id:
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)
except:
HTTPException(
status_code=500,
detail="Server error"
)
config_record.deleted_at = datetime.utcnow()
await db.commit()
return None

58
API/routers/server.py Normal file
View file

@ -0,0 +1,58 @@
from datetime import datetime
from fastapi import APIRouter, Request, Depends, HTTPException
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from models.server import Server
from schemas.server import ServerAdd, ServerDel, ServerInfo
from utils.database import get_db
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)
)
)
if existing.scalar_one_or_none():
raise HTTPException(
status_code=409,
detail="Name, ip or code is already exists"
)
new_server = Server(
name=body.name,
code=body.code,
host=ip,
port=body.port,
user=body.user,
password=body.password,
inbound_id=body.inbound_id
)
db.add(new_server)
await db.commit()
await db.refresh(new_server)
return ServerInfo.model_validate(new_server, from_attributes=True)
@router.delete("/delete", status_code=204)
async def del_server(body: ServerDel, db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(Server).where(
(Server.name == body.name) &
(Server.user == body.user) &
(Server.password == body.password) &
(Server.deleted_at.is_(None))
)
)
server = result.scalar_one_or_none()
if not server:
raise HTTPException(
status_code=404,
detail="Server not found, or invalid secure_key"
)
server.deleted_at = datetime.utcnow()
await db.commit()
return None

136
API/routers/user.py Normal file
View file

@ -0,0 +1,136 @@
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from schemas.user import UserCreate, UserResponse, UserLogin, UserTokenRevoke
from models.server import Server
from models.user import User
from models.config import Config
from models.token import AuthToken
from utils.database import get_db
from utils.auth import get_password_hash, create_token, verify_password, get_current_user, hash_token
from utils.xui import XUIClient
router = APIRouter(prefix="/user")
@router.post("/signup", status_code=201)
async def signup(body: UserCreate, db: AsyncSession = Depends(get_db)):
promo_code = (body.promo_code or "").strip()
result = await db.execute(
select(Server).where(
Server.deleted_at.is_(None),
Server.code == promo_code
)
)
server = result.scalar_one_or_none()
if not server:
raise HTTPException(
status_code=404,
detail="Promo-code is not valid"
)
existing = await db.execute(
select(User).where(User.email == body.email)
)
if existing.scalar_one_or_none():
raise HTTPException(
status_code=409,
detail="Email is busy"
)
new_user = User(
email=body.email,
pass_hash= get_password_hash(body.password),
server_id=server.id
)
db.add(new_user)
await db.commit()
await db.refresh(new_user)
access_token = await create_token(user_id=new_user.id, db=db)
return UserResponse(
id=new_user.id,
email=new_user.email,
created_at=new_user.created_at,
access_token=access_token
)
@router.post("/login", status_code=200)
async def login(body: UserLogin, db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(User).where(User.email == body.email)
)
user = result.scalar_one_or_none()
if (not user or user.deleted_at is not None) or (not verify_password(body.password, user.pass_hash)):
raise HTTPException(
status_code=401,
detail="Incorrect email or password"
)
access_token = await create_token(user_id=user.id, db=db)
return UserResponse(
id=user.id,
email=user.email,
created_at=user.created_at,
access_token=access_token
)
@router.post("/logout", status_code=200)
async def revoke_token(body: UserTokenRevoke, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
token_to_revoke = body.token_to_revoke.strip()
token_hash = hash_token(token_to_revoke)
result = await db.execute(
select(AuthToken).where(
AuthToken.token_hash == token_hash,
AuthToken.user_id == current_user.id
)
)
token_record = result.scalar_one_or_none()
if not token_record:
raise HTTPException(
status_code=404,
detail="Token not found"
)
if token_record.revoked:
return {"message": "Token already revoked"}
token_record.revoked = True
token_record.revoked_at = datetime.utcnow()
await db.commit()
return {"message": "Token successfully revoked"}
@router.delete("/delete", status_code=200)
async def delete_user(current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
if current_user.deleted_at is not None:
raise HTTPException(
status_code=400,
detail="User is already deleted"
)
try:
await db.refresh(current_user, ["server"])
server = current_user.server
if server and server.inbound_id:
xui = await XUIClient.from_server(server)
result = await db.execute(
select(Config).where(
Config.user_id == current_user.id,
Config.deleted_at.is_(None)
)
)
user_configs = result.scalars().all()
for cfg in user_configs:
client_email_xui = f"{current_user.email}-{cfg.name}"
try:
await xui.delete_client(server.inbound_id, client_email_xui)
except:
pass
except:
pass
current_user.deleted_at = datetime.utcnow()
await db.execute(
update(AuthToken)
.where(AuthToken.user_id == current_user.id)
.values(revoked=True, revoked_at=datetime.utcnow())
)
await db.execute(
update(Config)
.where(Config.user_id == current_user.id, Config.deleted_at.is_(None))
.values(deleted_at=datetime.utcnow())
)
await db.commit()
return {"message": "User has been successfully deleted"}