"""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, )