3x-ui: add expiry days and start-after-first-use for clients
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+7
-1
@@ -169,6 +169,8 @@ async def xui_add_client(
|
|||||||
inbound_id: int = Form(...),
|
inbound_id: int = Form(...),
|
||||||
total_gb: int = Form(0),
|
total_gb: int = Form(0),
|
||||||
limit_ip: int = Form(0),
|
limit_ip: int = Form(0),
|
||||||
|
expiry_days: int = Form(0),
|
||||||
|
start_after_first_use: str = Form(""),
|
||||||
flow: str = Form(""),
|
flow: str = Form(""),
|
||||||
comment: str = Form(""),
|
comment: str = Form(""),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
@@ -187,10 +189,11 @@ async def xui_add_client(
|
|||||||
inbound_id=inbound_id,
|
inbound_id=inbound_id,
|
||||||
total_gb=total_gb,
|
total_gb=total_gb,
|
||||||
limit_ip=limit_ip,
|
limit_ip=limit_ip,
|
||||||
|
expiry_days=expiry_days,
|
||||||
|
start_after_first_use=start_after_first_use == "1",
|
||||||
flow=flow.strip(),
|
flow=flow.strip(),
|
||||||
comment=comment.strip(),
|
comment=comment.strip(),
|
||||||
)
|
)
|
||||||
# Keep redirect short: store essentials only
|
|
||||||
slim = {
|
slim = {
|
||||||
"protocol": result.get("protocol"),
|
"protocol": result.get("protocol"),
|
||||||
"email": result.get("email"),
|
"email": result.get("email"),
|
||||||
@@ -201,6 +204,9 @@ async def xui_add_client(
|
|||||||
"publicKey": result.get("publicKey"),
|
"publicKey": result.get("publicKey"),
|
||||||
"links": result.get("links") or [],
|
"links": result.get("links") or [],
|
||||||
"inbound": result.get("inbound"),
|
"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))
|
payload = quote(json.dumps(slim, ensure_ascii=False))
|
||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
|
|||||||
+4
-4
@@ -264,7 +264,6 @@ class XuiClient:
|
|||||||
raise XuiApiError(f"Inbound #{inbound_id} — не VLESS (protocol={proto})")
|
raise XuiApiError(f"Inbound #{inbound_id} — не VLESS (protocol={proto})")
|
||||||
|
|
||||||
uuid = await self.get_new_uuid()
|
uuid = await self.get_new_uuid()
|
||||||
# Auto vision flow for Reality TCP if caller left flow empty
|
|
||||||
if not flow:
|
if not flow:
|
||||||
try:
|
try:
|
||||||
stream = inbound.get("streamSettings") or {}
|
stream = inbound.get("streamSettings") or {}
|
||||||
@@ -285,7 +284,7 @@ class XuiClient:
|
|||||||
"enable": True,
|
"enable": True,
|
||||||
"flow": flow,
|
"flow": flow,
|
||||||
"totalGB": total_gb,
|
"totalGB": total_gb,
|
||||||
"expiryTime": expiry_time,
|
"expiryTime": int(expiry_time),
|
||||||
"limitIp": limit_ip,
|
"limitIp": limit_ip,
|
||||||
"tgId": 0,
|
"tgId": 0,
|
||||||
"subId": _random_sub_id(),
|
"subId": _random_sub_id(),
|
||||||
@@ -300,6 +299,7 @@ class XuiClient:
|
|||||||
"inbound_id": inbound_id,
|
"inbound_id": inbound_id,
|
||||||
"uuid": uuid,
|
"uuid": uuid,
|
||||||
"flow": flow,
|
"flow": flow,
|
||||||
|
"expiryTime": int(expiry_time),
|
||||||
"details": details,
|
"details": details,
|
||||||
"links": links,
|
"links": links,
|
||||||
}
|
}
|
||||||
@@ -329,7 +329,7 @@ class XuiClient:
|
|||||||
"allowedIPs": ["0.0.0.0/0", "::/0"],
|
"allowedIPs": ["0.0.0.0/0", "::/0"],
|
||||||
"keepAlive": 0,
|
"keepAlive": 0,
|
||||||
"totalGB": total_gb,
|
"totalGB": total_gb,
|
||||||
"expiryTime": expiry_time,
|
"expiryTime": int(expiry_time),
|
||||||
"limitIp": limit_ip,
|
"limitIp": limit_ip,
|
||||||
"tgId": 0,
|
"tgId": 0,
|
||||||
"subId": _random_sub_id(),
|
"subId": _random_sub_id(),
|
||||||
@@ -337,13 +337,13 @@ class XuiClient:
|
|||||||
}
|
}
|
||||||
await self.add_client_raw(client, [inbound_id])
|
await self.add_client_raw(client, [inbound_id])
|
||||||
details = await self.get_client(email)
|
details = await self.get_client(email)
|
||||||
# WG has no vless:// link — expose keys for conf building
|
|
||||||
return {
|
return {
|
||||||
"protocol": "wireguard",
|
"protocol": "wireguard",
|
||||||
"email": email,
|
"email": email,
|
||||||
"inbound_id": inbound_id,
|
"inbound_id": inbound_id,
|
||||||
"privateKey": keys["privateKey"],
|
"privateKey": keys["privateKey"],
|
||||||
"publicKey": keys["publicKey"],
|
"publicKey": keys["publicKey"],
|
||||||
|
"expiryTime": int(expiry_time),
|
||||||
"details": details,
|
"details": details,
|
||||||
"inbound": {
|
"inbound": {
|
||||||
"id": inbound.get("id"),
|
"id": inbound.get("id"),
|
||||||
|
|||||||
@@ -140,6 +140,24 @@ async def load_available_inbounds(
|
|||||||
return await client.list_available_inbounds(protocols=protocols, enabled_only=True)
|
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(
|
async def add_xui_client(
|
||||||
panel: XuiPanel,
|
panel: XuiPanel,
|
||||||
*,
|
*,
|
||||||
@@ -148,27 +166,36 @@ async def add_xui_client(
|
|||||||
inbound_id: int,
|
inbound_id: int,
|
||||||
total_gb: int = 0,
|
total_gb: int = 0,
|
||||||
limit_ip: int = 0,
|
limit_ip: int = 0,
|
||||||
|
expiry_days: int = 0,
|
||||||
|
start_after_first_use: bool = False,
|
||||||
flow: str = "",
|
flow: str = "",
|
||||||
comment: str = "",
|
comment: str = "",
|
||||||
) -> dict:
|
) -> dict:
|
||||||
proto = protocol.strip().lower()
|
proto = protocol.strip().lower()
|
||||||
bytes_limit = total_gb * 1024 * 1024 * 1024 if total_gb > 0 else 0
|
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:
|
async with _client_for(panel) as client:
|
||||||
if proto == "vless":
|
if proto == "vless":
|
||||||
return await client.add_vless_client(
|
result = await client.add_vless_client(
|
||||||
email=email,
|
email=email,
|
||||||
inbound_id=inbound_id,
|
inbound_id=inbound_id,
|
||||||
total_gb=bytes_limit,
|
total_gb=bytes_limit,
|
||||||
|
expiry_time=expiry_time,
|
||||||
limit_ip=limit_ip,
|
limit_ip=limit_ip,
|
||||||
flow=flow,
|
flow=flow,
|
||||||
comment=comment,
|
comment=comment,
|
||||||
)
|
)
|
||||||
if proto == "wireguard":
|
elif proto == "wireguard":
|
||||||
return await client.add_wireguard_client(
|
result = await client.add_wireguard_client(
|
||||||
email=email,
|
email=email,
|
||||||
inbound_id=inbound_id,
|
inbound_id=inbound_id,
|
||||||
total_gb=bytes_limit,
|
total_gb=bytes_limit,
|
||||||
|
expiry_time=expiry_time,
|
||||||
limit_ip=limit_ip,
|
limit_ip=limit_ip,
|
||||||
comment=comment,
|
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
|
||||||
|
|||||||
@@ -36,6 +36,12 @@
|
|||||||
<p style="margin:0"><strong>{{ created.email }}</strong> → inbound #{{ created.inbound_id }}</p>
|
<p style="margin:0"><strong>{{ created.email }}</strong> → inbound #{{ created.inbound_id }}</p>
|
||||||
{% if created.protocol == 'vless' %}
|
{% if created.protocol == 'vless' %}
|
||||||
{% if created.uuid %}<p class="muted" style="margin:0">UUID: <code>{{ created.uuid }}</code>{% if created.flow %} · flow: {{ created.flow }}{% endif %}</p>{% endif %}
|
{% if created.uuid %}<p class="muted" style="margin:0">UUID: <code>{{ created.uuid }}</code>{% if created.flow %} · flow: {{ created.flow }}{% endif %}</p>{% endif %}
|
||||||
|
{% if created.expiry_days %}
|
||||||
|
<p class="muted" style="margin:0">
|
||||||
|
Срок: {{ created.expiry_days }} дн.
|
||||||
|
{% if created.start_after_first_use %}· старт после первого использования{% else %}· с момента создания{% endif %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
{% if created.links %}
|
{% if created.links %}
|
||||||
{% for link in created.links %}
|
{% for link in created.links %}
|
||||||
<pre class="mono">{{ link }}</pre>
|
<pre class="mono">{{ link }}</pre>
|
||||||
@@ -44,6 +50,12 @@
|
|||||||
<p class="muted">Ссылка vless:// появится в 3x-ui (Clients → Copy URL).</p>
|
<p class="muted">Ссылка vless:// появится в 3x-ui (Clients → Copy URL).</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif created.protocol == 'wireguard' %}
|
{% elif created.protocol == 'wireguard' %}
|
||||||
|
{% if created.expiry_days %}
|
||||||
|
<p class="muted" style="margin:0">
|
||||||
|
Срок: {{ created.expiry_days }} дн.
|
||||||
|
{% if created.start_after_first_use %}· старт после первого использования{% else %}· с момента создания{% endif %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
<p class="muted" style="margin:0">Ключи клиента (конфиг соберите в 3x-ui или ниже):</p>
|
<p class="muted" style="margin:0">Ключи клиента (конфиг соберите в 3x-ui или ниже):</p>
|
||||||
<pre class="mono">PrivateKey = {{ created.privateKey }}
|
<pre class="mono">PrivateKey = {{ created.privateKey }}
|
||||||
PublicKey = {{ created.publicKey }}
|
PublicKey = {{ created.publicKey }}
|
||||||
@@ -91,6 +103,9 @@ PublicKey = {{ created.publicKey }}
|
|||||||
<input name="flow" placeholder="xtls-rprx-vision" />
|
<input name="flow" placeholder="xtls-rprx-vision" />
|
||||||
</label>
|
</label>
|
||||||
<div class="form-grid-2">
|
<div class="form-grid-2">
|
||||||
|
<label>Срок действия (дни, 0 = безлимит)
|
||||||
|
<input type="number" name="expiry_days" value="30" min="0" />
|
||||||
|
</label>
|
||||||
<label>Лимит GB
|
<label>Лимит GB
|
||||||
<input type="number" name="total_gb" value="0" min="0" />
|
<input type="number" name="total_gb" value="0" min="0" />
|
||||||
</label>
|
</label>
|
||||||
@@ -98,6 +113,10 @@ PublicKey = {{ created.publicKey }}
|
|||||||
<input type="number" name="limit_ip" value="0" min="0" />
|
<input type="number" name="limit_ip" value="0" min="0" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<label class="check">
|
||||||
|
<input type="checkbox" name="start_after_first_use" value="1" checked />
|
||||||
|
Старт после первого использования
|
||||||
|
</label>
|
||||||
<label>Комментарий
|
<label>Комментарий
|
||||||
<input name="comment" />
|
<input name="comment" />
|
||||||
</label>
|
</label>
|
||||||
@@ -124,6 +143,9 @@ PublicKey = {{ created.publicKey }}
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<div class="form-grid-2">
|
<div class="form-grid-2">
|
||||||
|
<label>Срок действия (дни, 0 = безлимит)
|
||||||
|
<input type="number" name="expiry_days" value="30" min="0" />
|
||||||
|
</label>
|
||||||
<label>Лимит GB
|
<label>Лимит GB
|
||||||
<input type="number" name="total_gb" value="0" min="0" />
|
<input type="number" name="total_gb" value="0" min="0" />
|
||||||
</label>
|
</label>
|
||||||
@@ -131,6 +153,10 @@ PublicKey = {{ created.publicKey }}
|
|||||||
<input type="number" name="limit_ip" value="0" min="0" />
|
<input type="number" name="limit_ip" value="0" min="0" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<label class="check">
|
||||||
|
<input type="checkbox" name="start_after_first_use" value="1" checked />
|
||||||
|
Старт после первого использования
|
||||||
|
</label>
|
||||||
<label>Комментарий
|
<label>Комментарий
|
||||||
<input name="comment" />
|
<input name="comment" />
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user