Show WireGuard QR, full .conf and ZIP download after 3x-ui client create

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
test2
2026-07-25 22:51:19 +03:00
co-authored by Cursor
parent 9ae64d1bd1
commit 0ef3562860
9 changed files with 465 additions and 12 deletions
+143 -1
View File
@@ -1,4 +1,5 @@
import io
import zipfile
import qrcode
from fastapi import APIRouter, Depends, Form, Request, status
@@ -286,7 +287,7 @@ async def create_xui_client_from_clients(
status_code=status.HTTP_303_SEE_OTHER,
)
try:
await xui_service.add_xui_client(
result = await xui_service.add_xui_client(
db,
panel,
protocol=protocol,
@@ -305,12 +306,153 @@ async def create_xui_client_from_clients(
f"/admin/clients?flash=error:{exc}",
status_code=status.HTTP_303_SEE_OTHER,
)
site_id = result.get("site_client_id")
if site_id:
return RedirectResponse(
f"/admin/clients/xui/{site_id}?flash=created",
status_code=status.HTTP_303_SEE_OTHER,
)
return RedirectResponse(
"/admin/clients?flash=created",
status_code=status.HTTP_303_SEE_OTHER,
)
@router.get("/clients/xui/{site_client_id}", response_class=HTMLResponse)
async def xui_site_client_detail(
site_client_id: int,
request: Request,
db: AsyncSession = Depends(get_db),
admin=Depends(require_admin),
):
if _is_redirect(admin):
return admin
row = await xui_service.get_site_client(db, site_client_id)
if not row:
return RedirectResponse("/admin/clients", status_code=status.HTTP_303_SEE_OTHER)
stem, config = await xui_service.site_client_download_payload(row)
return request.app.state.templates.TemplateResponse(
"admin/xui_client_detail.html",
{
"request": request,
"admin": admin,
"client": row,
"config": config,
"filename_stem": stem,
"app_name": request.app.state.settings.app_name,
"flash": request.query_params.get("flash"),
},
)
@router.get("/clients/xui/{site_client_id}/config")
async def xui_site_client_config(
site_client_id: int,
db: AsyncSession = Depends(get_db),
admin=Depends(require_admin),
):
if _is_redirect(admin):
return admin
row = await xui_service.get_site_client(db, site_client_id)
if not row:
return RedirectResponse("/admin/clients", status_code=status.HTTP_303_SEE_OTHER)
stem, config = await xui_service.site_client_download_payload(row)
if not config:
return RedirectResponse(
f"/admin/clients/xui/{site_client_id}?flash=error:Нет конфига",
status_code=status.HTTP_303_SEE_OTHER,
)
ext = "conf" if row.protocol == "wireguard" else "txt"
return Response(
content=config,
media_type="text/plain",
headers={"Content-Disposition": f'attachment; filename="{stem}.{ext}"'},
)
@router.get("/clients/xui/{site_client_id}/qr")
async def xui_site_client_qr(
site_client_id: int,
db: AsyncSession = Depends(get_db),
admin=Depends(require_admin),
):
if _is_redirect(admin):
return admin
row = await xui_service.get_site_client(db, site_client_id)
if not row:
return RedirectResponse("/admin/clients", status_code=status.HTTP_303_SEE_OTHER)
_, config = await xui_service.site_client_download_payload(row)
if not config:
return Response(status_code=404)
img = qrcode.make(config)
buf = io.BytesIO()
img.save(buf, format="PNG")
buf.seek(0)
return StreamingResponse(buf, media_type="image/png")
@router.get("/clients/xui/{site_client_id}/zip")
async def xui_site_client_zip(
site_client_id: int,
db: AsyncSession = Depends(get_db),
admin=Depends(require_admin),
):
if _is_redirect(admin):
return admin
row = await xui_service.get_site_client(db, site_client_id)
if not row:
return RedirectResponse("/admin/clients", status_code=status.HTTP_303_SEE_OTHER)
stem, config = await xui_service.site_client_download_payload(row)
if not config:
return RedirectResponse(
f"/admin/clients/xui/{site_client_id}?flash=error:Нет конфига",
status_code=status.HTTP_303_SEE_OTHER,
)
ext = "conf" if row.protocol == "wireguard" else "txt"
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as zf:
zf.writestr(f"{stem}.{ext}", config)
if row.protocol == "wireguard":
zf.writestr(
f"{stem}-info.txt",
"\n".join(
[
f"Email: {row.email}",
f"Protocol: {row.protocol}",
f"Address: {row.address or ''}",
f"Endpoint: {row.endpoint or ''}",
f"DNS: {row.dns or ''}",
f"MTU: {row.mtu or ''}",
f"Panel: {row.panel.name if row.panel else ''}",
"",
"Import the .conf into WireGuard / AmneziaWG, or scan the QR on the panel.",
"",
]
),
)
else:
zf.writestr(
f"{stem}-info.txt",
"\n".join(
[
f"Email: {row.email}",
f"Protocol: {row.protocol}",
f"UUID: {row.uuid or ''}",
f"Flow: {row.flow or ''}",
"",
"Import the link into a VLESS client, or scan the QR on the panel.",
"",
]
),
)
buf.seek(0)
return Response(
content=buf.getvalue(),
media_type="application/zip",
headers={"Content-Disposition": f'attachment; filename="{stem}.zip"'},
)
@router.post("/clients/{client_id}/toggle")
async def toggle_client(
client_id: int,