301 lines
9.7 KiB
Python
301 lines
9.7 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),
|
|
for_users: int = Query(0),
|
|
db: AsyncSession = Depends(get_db),
|
|
admin=Depends(require_admin),
|
|
):
|
|
"""JSON: inbounds for dropdown. for_users=1 — only admin-whitelisted."""
|
|
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:
|
|
if for_users:
|
|
rows = await xui_service.list_user_inbounds(db, panel_id, protocol=protocol)
|
|
items = [
|
|
{
|
|
"id": r.inbound_id,
|
|
"protocol": r.protocol,
|
|
"remark": r.remark,
|
|
"port": r.port,
|
|
"enable": r.enable,
|
|
"is_available_for_users": True,
|
|
}
|
|
for r in rows
|
|
]
|
|
else:
|
|
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.post("/{panel_id}/inbounds/sync")
|
|
async def xui_inbounds_sync(
|
|
panel_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
admin=Depends(require_admin),
|
|
):
|
|
if _is_redirect(admin):
|
|
return admin
|
|
try:
|
|
await xui_service.sync_panel_inbounds(db, panel_id)
|
|
except Exception as exc: # noqa: BLE001
|
|
return RedirectResponse(
|
|
f"/admin/xui/{panel_id}?flash=error:{exc}",
|
|
status_code=status.HTTP_303_SEE_OTHER,
|
|
)
|
|
return RedirectResponse(
|
|
f"/admin/xui/{panel_id}?flash=inbounds_synced",
|
|
status_code=status.HTTP_303_SEE_OTHER,
|
|
)
|
|
|
|
|
|
@router.post("/{panel_id}/inbounds/users")
|
|
async def xui_inbounds_save_users(
|
|
panel_id: int,
|
|
request: Request,
|
|
db: AsyncSession = Depends(get_db),
|
|
admin=Depends(require_admin),
|
|
):
|
|
if _is_redirect(admin):
|
|
return admin
|
|
form = await request.form()
|
|
raw = form.getlist("inbound_ids")
|
|
inbound_ids = [int(v) for v in raw if str(v).isdigit()]
|
|
try:
|
|
await xui_service.save_user_inbounds(db, panel_id, inbound_ids)
|
|
except Exception as exc: # noqa: BLE001
|
|
return RedirectResponse(
|
|
f"/admin/xui/{panel_id}?flash=error:{exc}",
|
|
status_code=status.HTTP_303_SEE_OTHER,
|
|
)
|
|
return RedirectResponse(
|
|
f"/admin/xui/{panel_id}?flash=inbounds_saved",
|
|
status_code=status.HTTP_303_SEE_OTHER,
|
|
)
|
|
|
|
|
|
@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
|
|
site_clients = await xui_service.list_site_clients(db, panel_id)
|
|
panel_inbounds = await xui_service.list_panel_inbounds(db, panel_id)
|
|
try:
|
|
data = await xui_service.fetch_panel_data(panel)
|
|
if not panel_inbounds:
|
|
panel_inbounds = await xui_service.sync_panel_inbounds(db, panel_id)
|
|
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,
|
|
"site_clients": site_clients,
|
|
"panel_inbounds": panel_inbounds,
|
|
"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),
|
|
expiry_days: int = Form(0),
|
|
start_after_first_use: str = Form(""),
|
|
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(
|
|
db,
|
|
panel,
|
|
protocol=protocol,
|
|
email=email.strip(),
|
|
inbound_id=inbound_id,
|
|
total_gb=total_gb,
|
|
limit_ip=limit_ip,
|
|
expiry_days=expiry_days,
|
|
start_after_first_use=start_after_first_use == "1",
|
|
flow=flow.strip(),
|
|
comment=comment.strip(),
|
|
)
|
|
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"),
|
|
"allowedIPs": result.get("allowedIPs") or [],
|
|
"address": result.get("address"),
|
|
"endpoint": result.get("endpoint"),
|
|
"dns": result.get("dns"),
|
|
"mtu": result.get("mtu"),
|
|
"config_text": result.get("config_text"),
|
|
"links": result.get("links") or [],
|
|
"inbound": result.get("inbound"),
|
|
"expiry_days": result.get("expiry_days"),
|
|
"start_after_first_use": result.get("start_after_first_use"),
|
|
"expiryTime": result.get("expiryTime"),
|
|
}
|
|
payload = quote(json.dumps(slim, ensure_ascii=False))
|
|
site_id = result.get("site_client_id")
|
|
if site_id and result.get("protocol") == "wireguard" and result.get("config_text"):
|
|
return RedirectResponse(
|
|
f"/admin/clients/xui/{site_id}?flash=created",
|
|
status_code=status.HTTP_303_SEE_OTHER,
|
|
)
|
|
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,
|
|
)
|