Files
vpn/app/routers/xui.py
T

215 lines
6.6 KiB
Python

"""Admin routes for 3x-ui API panels."""
from __future__ import annotations
import json
from urllib.parse import quote
from fastapi import APIRouter, Depends, Form, Query, Request, status
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.models import XuiPanel
from app.routers.admin import _is_redirect, require_admin
from app.services import xui_panels as xui_service
router = APIRouter(prefix="/admin/xui", tags=["xui"])
@router.get("", response_class=HTMLResponse)
async def xui_list(request: Request, db: AsyncSession = Depends(get_db), admin=Depends(require_admin)):
if _is_redirect(admin):
return admin
panels = await xui_service.list_panels(db)
return request.app.state.templates.TemplateResponse(
"admin/xui.html",
{
"request": request,
"admin": admin,
"panels": panels,
"app_name": request.app.state.settings.app_name,
"flash": request.query_params.get("flash"),
},
)
@router.post("")
async def xui_create(
name: str = Form(""),
base_url: str = Form(...),
username: str = Form(""),
password: str = Form(""),
api_token: str = Form(""),
verify_ssl: str = Form(""),
db: AsyncSession = Depends(get_db),
admin=Depends(require_admin),
):
if _is_redirect(admin):
return admin
try:
await xui_service.create_panel(
db,
name=name,
base_url=base_url,
username=username,
password=password,
api_token=api_token,
verify_ssl=verify_ssl == "1",
)
except Exception as exc: # noqa: BLE001
return RedirectResponse(
f"/admin/xui?flash=error:{exc}",
status_code=status.HTTP_303_SEE_OTHER,
)
return RedirectResponse("/admin/xui?flash=created", status_code=status.HTTP_303_SEE_OTHER)
@router.post("/{panel_id}/check")
async def xui_check(
panel_id: int,
db: AsyncSession = Depends(get_db),
admin=Depends(require_admin),
):
if _is_redirect(admin):
return admin
try:
await xui_service.check_panel(db, panel_id)
except Exception as exc: # noqa: BLE001
return RedirectResponse(
f"/admin/xui?flash=error:{exc}",
status_code=status.HTTP_303_SEE_OTHER,
)
return RedirectResponse(
f"/admin/xui/{panel_id}?flash=ok",
status_code=status.HTTP_303_SEE_OTHER,
)
@router.post("/{panel_id}/delete")
async def xui_delete(
panel_id: int,
db: AsyncSession = Depends(get_db),
admin=Depends(require_admin),
):
if _is_redirect(admin):
return admin
await xui_service.delete_panel(db, panel_id)
return RedirectResponse("/admin/xui?flash=deleted", status_code=status.HTTP_303_SEE_OTHER)
@router.get("/{panel_id}/inbounds")
async def xui_inbounds_api(
panel_id: int,
protocol: str | None = Query(None),
db: AsyncSession = Depends(get_db),
admin=Depends(require_admin),
):
"""JSON: available inbounds for dropdown (optionally filtered by protocol)."""
if _is_redirect(admin):
return JSONResponse({"success": False, "error": "unauthorized"}, status_code=401)
panel = await db.get(XuiPanel, panel_id)
if not panel:
return JSONResponse({"success": False, "error": "not found"}, status_code=404)
try:
items = await xui_service.load_available_inbounds(panel, protocol=protocol)
return JSONResponse({"success": True, "obj": items})
except Exception as exc: # noqa: BLE001
return JSONResponse({"success": False, "error": str(exc)}, status_code=400)
@router.get("/{panel_id}", response_class=HTMLResponse)
async def xui_detail(
panel_id: int,
request: Request,
db: AsyncSession = Depends(get_db),
admin=Depends(require_admin),
):
if _is_redirect(admin):
return admin
panel = await db.get(XuiPanel, panel_id)
if not panel:
return RedirectResponse("/admin/xui", status_code=status.HTTP_303_SEE_OTHER)
data = None
error = None
try:
data = await xui_service.fetch_panel_data(panel)
except Exception as exc: # noqa: BLE001
error = str(exc)
created = None
created_raw = request.query_params.get("created")
if created_raw:
try:
created = json.loads(created_raw)
except json.JSONDecodeError:
created = None
return request.app.state.templates.TemplateResponse(
"admin/xui_detail.html",
{
"request": request,
"admin": admin,
"panel": panel,
"data": data,
"created": created,
"error": error,
"app_name": request.app.state.settings.app_name,
"flash": request.query_params.get("flash"),
},
)
@router.post("/{panel_id}/clients")
async def xui_add_client(
panel_id: int,
protocol: str = Form(...),
email: str = Form(...),
inbound_id: int = Form(...),
total_gb: int = Form(0),
limit_ip: int = Form(0),
flow: str = Form(""),
comment: str = Form(""),
db: AsyncSession = Depends(get_db),
admin=Depends(require_admin),
):
if _is_redirect(admin):
return admin
panel = await db.get(XuiPanel, panel_id)
if not panel:
return RedirectResponse("/admin/xui", status_code=status.HTTP_303_SEE_OTHER)
try:
result = await xui_service.add_xui_client(
panel,
protocol=protocol,
email=email.strip(),
inbound_id=inbound_id,
total_gb=total_gb,
limit_ip=limit_ip,
flow=flow.strip(),
comment=comment.strip(),
)
# Keep redirect short: store essentials only
slim = {
"protocol": result.get("protocol"),
"email": result.get("email"),
"inbound_id": result.get("inbound_id"),
"uuid": result.get("uuid"),
"flow": result.get("flow"),
"privateKey": result.get("privateKey"),
"publicKey": result.get("publicKey"),
"links": result.get("links") or [],
"inbound": result.get("inbound"),
}
payload = quote(json.dumps(slim, ensure_ascii=False))
return RedirectResponse(
f"/admin/xui/{panel_id}?flash=client_created&created={payload}",
status_code=status.HTTP_303_SEE_OTHER,
)
except Exception as exc: # noqa: BLE001
return RedirectResponse(
f"/admin/xui/{panel_id}?flash=error:{exc}",
status_code=status.HTTP_303_SEE_OTHER,
)