Track site-created 3x-ui clients and per-panel inbound whitelist for users

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
test2
2026-07-25 22:32:26 +03:00
co-authored by Cursor
parent 1b2fea4539
commit ad7e1db6b7
9 changed files with 746 additions and 13 deletions
+80 -2
View File
@@ -8,8 +8,9 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.database import get_db
from app.models import AdminUser, Protocol, VpnClient, VpnServer
from app.models import AdminUser, Protocol, VpnClient, VpnServer, XuiPanel
from app.services import vpn as vpn_service
from app.services import xui_panels as xui_service
router = APIRouter(prefix="/admin", tags=["admin"])
@@ -230,18 +231,41 @@ async def update_server(
async def clients_page(
request: Request,
protocol: str | None = None,
source: str | None = None,
db: AsyncSession = Depends(get_db),
admin=Depends(require_admin),
):
if _is_redirect(admin):
return admin
tab = (source or protocol or "wg").lower()
if tab in ("wireguard", "awg2"):
tab = "wg"
if tab not in ("wg", "xui"):
tab = "wg"
query = select(VpnClient).options(selectinload(VpnClient.server)).order_by(VpnClient.id.desc())
if protocol:
if protocol in ("wireguard", "awg2"):
query = query.join(VpnServer).where(VpnServer.protocol == protocol)
tab = "wg"
clients = (await db.execute(query)).scalars().all()
servers = (await db.execute(select(VpnServer).order_by(VpnServer.id))).scalars().all()
xui_panels = await xui_service.list_panels(db)
xui_clients = await xui_service.list_all_site_clients(db) if tab == "xui" else []
user_inbounds_by_panel: dict[int, list] = {}
if tab == "xui":
for panel in xui_panels:
rows = await xui_service.list_user_inbounds(db, panel.id)
user_inbounds_by_panel[panel.id] = [
{
"inbound_id": r.inbound_id,
"protocol": r.protocol,
"remark": r.remark,
"port": r.port,
}
for r in rows
]
return request.app.state.templates.TemplateResponse(
"admin/clients.html",
@@ -250,6 +274,10 @@ async def clients_page(
"admin": admin,
"clients": clients,
"servers": servers,
"xui_panels": xui_panels,
"xui_clients": xui_clients,
"user_inbounds_by_panel": user_inbounds_by_panel,
"tab": tab,
"filter_protocol": protocol,
"protocols": Protocol,
"app_name": request.app.state.settings.app_name,
@@ -279,6 +307,56 @@ async def create_client(
return RedirectResponse("/admin/clients?flash=created", status_code=status.HTTP_303_SEE_OTHER)
@router.post("/clients/xui")
async def create_xui_client_from_clients(
request: Request,
panel_id: int = Form(...),
protocol: str = Form(...),
email: str = Form(...),
inbound_id: int = Form(...),
total_gb: int = Form(0),
limit_ip: int = Form(0),
expiry_days: int = Form(30),
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/clients?source=xui&flash=error:Панель не найдена",
status_code=status.HTTP_303_SEE_OTHER,
)
try:
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(),
require_user_available=True,
)
except Exception as exc: # noqa: BLE001
return RedirectResponse(
f"/admin/clients?source=xui&flash=error:{exc}",
status_code=status.HTTP_303_SEE_OTHER,
)
return RedirectResponse(
"/admin/clients?source=xui&flash=created",
status_code=status.HTTP_303_SEE_OTHER,
)
@router.post("/clients/{client_id}/toggle")
async def toggle_client(
client_id: int,
+70 -2
View File
@@ -102,22 +102,83 @@ async def xui_delete(
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: available inbounds for dropdown (optionally filtered by protocol)."""
"""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:
items = await xui_service.load_available_inbounds(panel, protocol=protocol)
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,
@@ -133,8 +194,12 @@ async def xui_detail(
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)
@@ -153,6 +218,8 @@ async def xui_detail(
"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,
@@ -183,6 +250,7 @@ async def xui_add_client(
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(),