From 1b2fea45394d0c8c5d72c23ae6c05c221570d2b0 Mon Sep 17 00:00:00 2001 From: test2 Date: Sat, 25 Jul 2026 22:18:37 +0300 Subject: [PATCH] 3x-ui: add expiry days and start-after-first-use for clients Co-authored-by: Cursor --- app/routers/xui.py | 8 ++++++- app/services/xui.py | 8 +++---- app/services/xui_panels.py | 35 +++++++++++++++++++++++++---- app/templates/admin/xui_detail.html | 26 +++++++++++++++++++++ 4 files changed, 68 insertions(+), 9 deletions(-) diff --git a/app/routers/xui.py b/app/routers/xui.py index 28ef9fe..04120db 100644 --- a/app/routers/xui.py +++ b/app/routers/xui.py @@ -169,6 +169,8 @@ async def xui_add_client( 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), @@ -187,10 +189,11 @@ async def xui_add_client( 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(), ) - # Keep redirect short: store essentials only slim = { "protocol": result.get("protocol"), "email": result.get("email"), @@ -201,6 +204,9 @@ async def xui_add_client( "publicKey": result.get("publicKey"), "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)) return RedirectResponse( diff --git a/app/services/xui.py b/app/services/xui.py index 25eb144..f6174f8 100644 --- a/app/services/xui.py +++ b/app/services/xui.py @@ -264,7 +264,6 @@ class XuiClient: raise XuiApiError(f"Inbound #{inbound_id} — не VLESS (protocol={proto})") uuid = await self.get_new_uuid() - # Auto vision flow for Reality TCP if caller left flow empty if not flow: try: stream = inbound.get("streamSettings") or {} @@ -285,7 +284,7 @@ class XuiClient: "enable": True, "flow": flow, "totalGB": total_gb, - "expiryTime": expiry_time, + "expiryTime": int(expiry_time), "limitIp": limit_ip, "tgId": 0, "subId": _random_sub_id(), @@ -300,6 +299,7 @@ class XuiClient: "inbound_id": inbound_id, "uuid": uuid, "flow": flow, + "expiryTime": int(expiry_time), "details": details, "links": links, } @@ -329,7 +329,7 @@ class XuiClient: "allowedIPs": ["0.0.0.0/0", "::/0"], "keepAlive": 0, "totalGB": total_gb, - "expiryTime": expiry_time, + "expiryTime": int(expiry_time), "limitIp": limit_ip, "tgId": 0, "subId": _random_sub_id(), @@ -337,13 +337,13 @@ class XuiClient: } await self.add_client_raw(client, [inbound_id]) details = await self.get_client(email) - # WG has no vless:// link — expose keys for conf building return { "protocol": "wireguard", "email": email, "inbound_id": inbound_id, "privateKey": keys["privateKey"], "publicKey": keys["publicKey"], + "expiryTime": int(expiry_time), "details": details, "inbound": { "id": inbound.get("id"), diff --git a/app/services/xui_panels.py b/app/services/xui_panels.py index 73dc6e6..d77f8fe 100644 --- a/app/services/xui_panels.py +++ b/app/services/xui_panels.py @@ -140,6 +140,24 @@ async def load_available_inbounds( return await client.list_available_inbounds(protocols=protocols, enabled_only=True) +def compute_expiry_time(*, days: int, start_after_first_use: bool) -> int: + """ + 3x-ui expiryTime encoding: + 0 — unlimited + >0 — absolute deadline in unix ms + <0 — duration ms; countdown starts after first use + """ + days = int(days or 0) + if days <= 0: + return 0 + duration_ms = days * 24 * 60 * 60 * 1000 + if start_after_first_use: + return -duration_ms + import time + + return int(time.time() * 1000) + duration_ms + + async def add_xui_client( panel: XuiPanel, *, @@ -148,27 +166,36 @@ async def add_xui_client( inbound_id: int, total_gb: int = 0, limit_ip: int = 0, + expiry_days: int = 0, + start_after_first_use: bool = False, flow: str = "", comment: str = "", ) -> dict: proto = protocol.strip().lower() bytes_limit = total_gb * 1024 * 1024 * 1024 if total_gb > 0 else 0 + expiry_time = compute_expiry_time(days=expiry_days, start_after_first_use=start_after_first_use) async with _client_for(panel) as client: if proto == "vless": - return await client.add_vless_client( + result = await client.add_vless_client( email=email, inbound_id=inbound_id, total_gb=bytes_limit, + expiry_time=expiry_time, limit_ip=limit_ip, flow=flow, comment=comment, ) - if proto == "wireguard": - return await client.add_wireguard_client( + elif proto == "wireguard": + result = await client.add_wireguard_client( email=email, inbound_id=inbound_id, total_gb=bytes_limit, + expiry_time=expiry_time, limit_ip=limit_ip, comment=comment, ) - raise ValueError("Поддерживаются только vless и wireguard") + else: + raise ValueError("Поддерживаются только vless и wireguard") + result["expiry_days"] = expiry_days + result["start_after_first_use"] = start_after_first_use + return result diff --git a/app/templates/admin/xui_detail.html b/app/templates/admin/xui_detail.html index cfeb43e..7aad174 100644 --- a/app/templates/admin/xui_detail.html +++ b/app/templates/admin/xui_detail.html @@ -36,6 +36,12 @@

{{ created.email }} → inbound #{{ created.inbound_id }}

{% if created.protocol == 'vless' %} {% if created.uuid %}

UUID: {{ created.uuid }}{% if created.flow %} · flow: {{ created.flow }}{% endif %}

{% endif %} + {% if created.expiry_days %} +

+ Срок: {{ created.expiry_days }} дн. + {% if created.start_after_first_use %}· старт после первого использования{% else %}· с момента создания{% endif %} +

+ {% endif %} {% if created.links %} {% for link in created.links %}
{{ link }}
@@ -44,6 +50,12 @@

Ссылка vless:// появится в 3x-ui (Clients → Copy URL).

{% endif %} {% elif created.protocol == 'wireguard' %} + {% if created.expiry_days %} +

+ Срок: {{ created.expiry_days }} дн. + {% if created.start_after_first_use %}· старт после первого использования{% else %}· с момента создания{% endif %} +

+ {% endif %}

Ключи клиента (конфиг соберите в 3x-ui или ниже):

PrivateKey = {{ created.privateKey }}
 PublicKey = {{ created.publicKey }}
@@ -91,6 +103,9 @@ PublicKey = {{ created.publicKey }}
           
         
         
+ @@ -98,6 +113,10 @@ PublicKey = {{ created.publicKey }}
+ @@ -124,6 +143,9 @@ PublicKey = {{ created.publicKey }}
+ @@ -131,6 +153,10 @@ PublicKey = {{ created.publicKey }}
+