279 lines
10 KiB
HTML
279 lines
10 KiB
HTML
{% 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 == '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.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' %}
|
|
<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 %}
|
|
|
|
{% 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>Лимит 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>Комментарий
|
|
<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>Лимит 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>Комментарий
|
|
<input name="comment" />
|
|
</label>
|
|
<button class="btn btn-accent" type="submit" {% if not data.wireguard_inbounds %}disabled{% endif %}>Создать WireGuard</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel" style="margin-bottom:1rem">
|
|
<div class="panel-head">
|
|
<strong>Доступные inbound’ы (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>Clients</strong></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 %}
|