Refactor API
This commit is contained in:
parent
9c813cdfbe
commit
8aa4828239
21 changed files with 731 additions and 363 deletions
133
API/routers/config.py
Normal file
133
API/routers/config.py
Normal 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
58
API/routers/server.py
Normal 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
136
API/routers/user.py
Normal 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"}
|
||||
Loading…
Add table
Add a link
Reference in a new issue