Files
vpn/app/templates/admin/xui_detail.html
T

411 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "admin/layout.html" %}
{% block admin_title %}{{ panel.name }}{% endblock %}
{% block admin_content %}
<div class="admin-top">
<div>
<h1>{{ panel.name }}</h1>
<div class="muted"><a href="{{ panel.base_url }}" target="_blank" rel="noopener">{{ panel.base_url }}</a></div>
</div>
<div class="actions">
<a class="btn btn-sm btn-ghost" href="/admin/xui">Назад</a>
<button class="btn btn-sm btn-ghost" type="button" id="btn-reload-inbounds">Загрузить inbound’ы</button>
<form method="post" action="/admin/xui/{{ panel.id }}/check" class="inline-form">
<button class="btn btn-sm btn-accent" type="submit">Обновить API</button>
</form>
</div>
</div>
{% if flash %}
{% if flash.startswith('error:') %}
<div class="flash flash-error">{{ flash[6:] }}</div>
{% elif flash == 'client_created' %}
<div class="flash">Клиент создан в 3x-ui</div>
{% elif flash == 'inbounds_synced' %}
<div class="flash">Inbound’ы синхронизированы с 3x-ui</div>
{% elif flash == 'inbounds_saved' %}
<div class="flash">Доступные пользователям inbound’ы сохранены</div>
{% elif flash == 'ok' %}
<div class="flash">Данные обновлены</div>
{% endif %}
{% endif %}
{% if error %}
<div class="flash flash-error">{{ error }}</div>
{% endif %}
{% if created %}
<div class="panel" style="margin-bottom:1rem">
<div class="panel-head"><strong>Новый клиент ({{ created.protocol }})</strong></div>
<div class="panel-body stack">
<p style="margin:0"><strong>{{ created.email }}</strong> → inbound #{{ created.inbound_id }}</p>
{% 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.expiry_days %}
<p class="muted" style="margin:0">
Срок: {{ created.expiry_days }} дн.
{% if created.start_after_first_use %}· старт после первого использования{% else %}· с момента создания{% endif %}
</p>
{% endif %}
{% if created.links %}
{% for link in created.links %}
<pre class="mono">{{ link }}</pre>
{% endfor %}
{% else %}
<p class="muted">Ссылка vless:// появится в 3x-ui (Clients → Copy URL).</p>
{% endif %}
{% 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>
<pre class="mono">PrivateKey = {{ created.privateKey }}
PublicKey = {{ created.publicKey }}
{% if created.inbound %}Endpoint port = {{ created.inbound.port }}{% endif %}</pre>
{% endif %}
</div>
</div>
{% endif %}
<div class="panel">
<div class="panel-head">
<strong>Создано через сайт</strong>
<span class="muted">только клиенты, которых вы создали в этой панели</span>
</div>
<table>
<thead>
<tr>
<th>Email</th>
<th>Протокол</th>
<th>Inbound</th>
<th>Срок</th>
<th>Лимиты</th>
<th>Ссылка / ключи</th>
<th>Когда</th>
</tr>
</thead>
<tbody>
{% for c in site_clients %}
<tr>
<td>
<strong>{{ c.email }}</strong>
{% if c.comment %}<div class="muted">{{ c.comment }}</div>{% endif %}
</td>
<td><span class="badge badge-proto">{{ c.protocol }}</span></td>
<td class="muted">#{{ c.inbound_id }} {% if c.inbound_remark %}{{ c.inbound_remark }}{% endif %}</td>
<td class="muted">
{% if c.expiry_days and c.expiry_days > 0 %}
{{ c.expiry_days }} дн.
{% if c.start_after_first_use %}<br><span class="badge badge-ok">после 1-го входа</span>{% endif %}
{% else %}
без срока
{% endif %}
</td>
<td class="muted">
{{ c.total_gb or 0 }} GB
{% if c.limit_ip %}<br>IP ≤ {{ c.limit_ip }}{% endif %}
</td>
<td>
{% if c.link %}
<div class="link-box" style="margin:0;max-width:280px;font-size:.78rem;word-break:break-all;">{{ c.link }}</div>
{% elif c.private_key %}
<div class="muted" style="font-size:.78rem;word-break:break-all;">priv: {{ c.private_key[:24] }}…</div>
{% else %}—{% endif %}
</td>
<td class="muted">{{ c.created_at }}</td>
</tr>
{% else %}
<tr><td colspan="7" class="muted">Пока никого не создавали через сайт</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% if data %}
<div class="stats">
<div class="stat">
<div class="label">Все inbound’ы</div>
<div class="value">{{ data.inbounds|length }}</div>
</div>
<div class="stat">
<div class="label">VLESS</div>
<div class="value">{{ data.vless_inbounds|length }}</div>
</div>
<div class="stat">
<div class="label">WireGuard</div>
<div class="value">{{ data.wireguard_inbounds|length }}</div>
</div>
</div>
<div class="detail-grid" style="margin-bottom:1rem">
<div class="panel">
<div class="panel-head"><strong>Создать VLESS клиента</strong></div>
<div class="panel-body">
<form method="post" action="/admin/xui/{{ panel.id }}/clients" class="stack">
<input type="hidden" name="protocol" value="vless" />
<label>Email / имя
<input name="email" placeholder="user-vless" required />
</label>
<label>Inbound VLESS
<select name="inbound_id" id="select-vless" required>
{% for ib in data.vless_inbounds %}
<option value="{{ ib.id }}">#{{ ib.id }} {{ ib.remark or 'vless' }} :{{ ib.port }}{% if ib.enable is defined and not ib.enable %} (off){% endif %}</option>
{% else %}
<option value="" disabled selected>Нет VLESS inbound’ов — нажмите «Загрузить»</option>
{% endfor %}
</select>
</label>
<label>Flow (пусто = авто vision для Reality/TLS)
<input name="flow" placeholder="xtls-rprx-vision" />
</label>
<div class="form-grid-2">
<label>Срок действия (дни, 0 = безлимит)
<input type="number" name="expiry_days" value="30" min="0" />
</label>
<label>Лимит GB
<input type="number" name="total_gb" value="0" min="0" />
</label>
<label>Limit IP
<input type="number" name="limit_ip" value="0" min="0" />
</label>
</div>
<label class="check">
<input type="checkbox" name="start_after_first_use" value="1" checked />
Старт после первого использования
</label>
<label>Комментарий
<input name="comment" />
</label>
<button class="btn btn-accent" type="submit" {% if not data.vless_inbounds %}disabled{% endif %}>Создать VLESS</button>
</form>
</div>
</div>
<div class="panel">
<div class="panel-head"><strong>Создать WireGuard клиента</strong></div>
<div class="panel-body">
<form method="post" action="/admin/xui/{{ panel.id }}/clients" class="stack">
<input type="hidden" name="protocol" value="wireguard" />
<label>Email / имя
<input name="email" placeholder="user-wg" required />
</label>
<label>Inbound WireGuard
<select name="inbound_id" id="select-wg" required>
{% for ib in data.wireguard_inbounds %}
<option value="{{ ib.id }}">#{{ ib.id }} {{ ib.remark or 'wireguard' }} :{{ ib.port }}{% if ib.enable is defined and not ib.enable %} (off){% endif %}</option>
{% else %}
<option value="" disabled selected>Нет WireGuard inbound’ов — нажмите «Загрузить»</option>
{% endfor %}
</select>
</label>
<div class="form-grid-2">
<label>Срок действия (дни, 0 = безлимит)
<input type="number" name="expiry_days" value="30" min="0" />
</label>
<label>Лимит GB
<input type="number" name="total_gb" value="0" min="0" />
</label>
<label>Limit IP
<input type="number" name="limit_ip" value="0" min="0" />
</label>
</div>
<label class="check">
<input type="checkbox" name="start_after_first_use" value="1" checked />
Старт после первого использования
</label>
<label>Комментарий
<input name="comment" />
</label>
<button class="btn btn-accent" type="submit" {% if not data.wireguard_inbounds %}disabled{% endif %}>Создать WireGuard</button>
</form>
</div>
</div>
</div>
{% endif %}
<div class="panel" style="margin-bottom:1rem">
<div class="panel-head">
<strong>Inbound’ы для пользователей</strong>
<span class="muted">отметьте, какие будут доступны при создании клиентов</span>
</div>
<form method="post" action="/admin/xui/{{ panel.id }}/inbounds/users">
<table>
<thead>
<tr>
<th>Для пользователей</th>
<th>ID</th>
<th>Remark</th>
<th>Protocol</th>
<th>Port</th>
<th>В 3x-ui</th>
</tr>
</thead>
<tbody>
{% for ib in panel_inbounds %}
<tr>
<td>
<label class="check" style="margin:0">
<input type="checkbox" name="inbound_ids" value="{{ ib.inbound_id }}" {% if ib.is_available_for_users %}checked{% endif %} />
</label>
</td>
<td>{{ ib.inbound_id }}</td>
<td>{{ ib.remark or '—' }}</td>
<td><span class="badge badge-proto">{{ ib.protocol }}</span></td>
<td>{{ ib.port or '—' }}</td>
<td>{% if ib.enable %}<span class="badge badge-ok">on</span>{% else %}<span class="badge badge-off">off</span>{% endif %}</td>
</tr>
{% else %}
<tr><td colspan="6" class="muted">Список пуст — нажмите «Синхронизировать inbound’ы»</td></tr>
{% endfor %}
</tbody>
</table>
<div class="actions" style="padding:1rem;gap:.5rem;display:flex;flex-wrap:wrap">
<button class="btn btn-accent" type="submit" {% if not panel_inbounds %}disabled{% endif %}>Сохранить для пользователей</button>
<button class="btn btn-ghost" type="submit" formaction="/admin/xui/{{ panel.id }}/inbounds/sync" formmethod="post">Синхронизировать с 3x-ui</button>
</div>
</form>
</div>
{% if data %}
<div class="panel" style="margin-bottom:1rem">
<div class="panel-head">
<strong>Все inbound’ы из API (VLESS / WireGuard)</strong>
<span class="muted" id="inbounds-status"></span>
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>Remark</th>
<th>Protocol</th>
<th>Port</th>
<th>Enable</th>
</tr>
</thead>
<tbody id="inbounds-table-body">
{% for ib in data.available_inbounds %}
<tr>
<td>{{ ib.id }}</td>
<td>{{ ib.remark or '—' }}</td>
<td><span class="badge badge-proto">{{ ib.protocol }}</span></td>
<td>{{ ib.port }}</td>
<td>{% if ib.enable is not defined or ib.enable %}<span class="badge badge-ok">on</span>{% else %}<span class="badge badge-off">off</span>{% endif %}</td>
</tr>
{% else %}
<tr><td colspan="5" class="muted">Нет VLESS/WireGuard inbound’ов</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="panel">
<div class="panel-head">
<strong>Все клиенты 3x-ui</strong>
<span class="muted">из API панели (включая созданных не через сайт)</span>
</div>
<table>
<thead>
<tr>
<th>Email</th>
<th>Enable</th>
<th>Inbounds</th>
<th>Traffic</th>
</tr>
</thead>
<tbody>
{% for c in data.clients %}
<tr>
<td>{{ c.email }}</td>
<td>{% if c.enable %}<span class="badge badge-ok">on</span>{% else %}<span class="badge badge-off">off</span>{% endif %}</td>
<td>{{ c.inboundIds or c.inbound_ids or '—' }}</td>
<td class="muted">
{% if c.traffic %}
↑ {{ c.traffic.up or 0 }} / ↓ {{ c.traffic.down or 0 }}
{% else %}—{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="4" class="muted">Нет клиентов</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<script>
(function () {
const panelId = {{ panel.id }};
const btn = document.getElementById('btn-reload-inbounds');
const statusEl = document.getElementById('inbounds-status');
const tbody = document.getElementById('inbounds-table-body');
const selectVless = document.getElementById('select-vless');
const selectWg = document.getElementById('select-wg');
function fillSelect(select, items, emptyLabel) {
if (!select) return;
select.innerHTML = '';
if (!items.length) {
const opt = document.createElement('option');
opt.disabled = true;
opt.selected = true;
opt.value = '';
opt.textContent = emptyLabel;
select.appendChild(opt);
return;
}
items.forEach((ib) => {
const opt = document.createElement('option');
opt.value = ib.id;
const off = (ib.enable === false) ? ' (off)' : '';
opt.textContent = `#${ib.id} ${ib.remark || ib.protocol} :${ib.port}${off}`;
select.appendChild(opt);
});
}
async function reloadInbounds() {
statusEl.textContent = 'загрузка…';
btn.disabled = true;
try {
const res = await fetch(`/admin/xui/${panelId}/inbounds`);
const data = await res.json();
if (!data.success) throw new Error(data.error || 'fail');
const items = data.obj || [];
const vless = items.filter((i) => String(i.protocol).toLowerCase() === 'vless');
const wg = items.filter((i) => String(i.protocol).toLowerCase() === 'wireguard');
fillSelect(selectVless, vless, 'Нет VLESS inbound’ов');
fillSelect(selectWg, wg, 'Нет WireGuard inbound’ов');
const vlessBtn = selectVless && selectVless.closest('form').querySelector('button[type=submit]');
const wgBtn = selectWg && selectWg.closest('form').querySelector('button[type=submit]');
if (vlessBtn) vlessBtn.disabled = vless.length === 0;
if (wgBtn) wgBtn.disabled = wg.length === 0;
tbody.innerHTML = '';
if (!items.length) {
tbody.innerHTML = '<tr><td colspan="5" class="muted">Нет VLESS/WireGuard inbound’ов</td></tr>';
} else {
items.forEach((ib) => {
const tr = document.createElement('tr');
const on = ib.enable === false
? '<span class="badge badge-off">off</span>'
: '<span class="badge badge-ok">on</span>';
tr.innerHTML = `<td>${ib.id}</td><td>${ib.remark || '—'}</td><td><span class="badge badge-proto">${ib.protocol}</span></td><td>${ib.port}</td><td>${on}</td>`;
tbody.appendChild(tr);
});
}
statusEl.textContent = `загружено: ${items.length} (VLESS ${vless.length}, WG ${wg.length})`;
} catch (e) {
statusEl.textContent = 'ошибка: ' + e.message;
} finally {
btn.disabled = false;
}
}
btn.addEventListener('click', reloadInbounds);
})();
</script>
{% endif %}
{% endblock %}