Template
Add 3x-ui user sync via panel API (login or Bearer token).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+188
-4
@@ -249,9 +249,9 @@
|
||||
style="display: flex; align-items: center; gap: var(--space-sm); cursor: not-allowed; color: var(--text-muted);">
|
||||
<input type="checkbox" disabled> Marzban
|
||||
</label>
|
||||
<label
|
||||
style="display: flex; align-items: center; gap: var(--space-sm); cursor: not-allowed; color: var(--text-muted);">
|
||||
<input type="checkbox" disabled> 3x-ui
|
||||
<label style="display: flex; align-items: center; gap: var(--space-sm); cursor: pointer;">
|
||||
<input type="checkbox" name="xui_sync" {% if settings.sync.xui_sync %}checked{% endif %}>
|
||||
3x-ui
|
||||
</label>
|
||||
<label
|
||||
style="display: flex; align-items: center; gap: var(--space-sm); cursor: not-allowed; color: var(--text-muted);">
|
||||
@@ -336,6 +336,85 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="xuiFields"
|
||||
style="{% if not settings.sync.xui_sync %}display:none;{% endif %} border-top: 1px solid var(--border-color); padding-top: var(--space-md); margin-top: var(--space-md);">
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ _('xui_url_label') }}</label>
|
||||
<input type="url" class="form-input" name="xui_url"
|
||||
value="{{ settings.sync.xui_url }}" placeholder="https://panel.example.com:2053/path">
|
||||
<div class="form-hint">{{ _('xui_url_hint') }}</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ _('xui_api_token_label') }}</label>
|
||||
<input type="password" class="form-input" name="xui_api_token"
|
||||
value="{{ settings.sync.xui_api_token }}" placeholder="{{ _('xui_api_token_placeholder') }}">
|
||||
<div class="form-hint">{{ _('xui_api_token_hint') }}</div>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-sm);">
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ _('xui_username_label') }}</label>
|
||||
<input type="text" class="form-input" name="xui_username"
|
||||
value="{{ settings.sync.xui_username }}" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ _('xui_password_label') }}</label>
|
||||
<input type="password" class="form-input" name="xui_password"
|
||||
value="{{ settings.sync.xui_password }}" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label
|
||||
style="display: flex; align-items: center; gap: var(--space-sm); cursor: pointer; margin-bottom: var(--space-xs);">
|
||||
<input type="checkbox" name="xui_sync_users" {% if settings.sync.xui_sync_users %}checked{% endif %}>
|
||||
{{ _('enable_sync') }}
|
||||
</label>
|
||||
<div class="form-hint"
|
||||
style="margin-left: var(--space-lg); line-height: 1.4; margin-bottom: var(--space-sm);">
|
||||
{{ _('xui_sync_hint') }}
|
||||
</div>
|
||||
<div style="display: flex; gap: var(--space-sm); margin-left: var(--space-lg);">
|
||||
<button type="button" class="btn btn-secondary btn-sm" onclick="syncXuiNow()" id="xuiSyncNowBtn">
|
||||
<span id="xuiSyncNowBtnText">🔄 {{ _('sync_now_btn') }}</span>
|
||||
<div class="spinner hidden" id="xuiSyncNowSpinner" style="width:14px; height:14px;"></div>
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger btn-sm" onclick="deleteSyncXui()" id="xuiSyncDelBtn">
|
||||
<span id="xuiSyncDelBtnText">🗑 {{ _('delete_sync_btn') }}</span>
|
||||
<div class="spinner hidden" id="xuiSyncDelSpinner" style="width:14px; height:14px;"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: var(--space-sm); cursor: pointer;">
|
||||
<input type="checkbox" name="xui_create_conns" id="xuiSyncCreateConns" {% if
|
||||
settings.sync.xui_create_conns %}checked{% endif %}>
|
||||
{{ _('auto_create_conns') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="xuiAutoConnFields"
|
||||
style="{% if not settings.sync.xui_create_conns %}display:none;{% endif %} background: var(--bg-primary); padding: var(--space-md); border-radius: var(--radius-md); margin-top: var(--space-sm);">
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ _('sync_server_label') }}</label>
|
||||
<select class="form-select" name="xui_server_id" onchange="updateProtocolsForXuiSync()">
|
||||
{% for s in servers %}
|
||||
<option value="{{ loop.index0 }}" {% if settings.sync.xui_server_id==loop.index0
|
||||
%}selected{% endif %}>{{ s.name or s.host }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ _('protocol_label') }}</label>
|
||||
<select class="form-select" name="xui_protocol" id="xuiSyncProtocolSelect">
|
||||
<option value="xray" {% if settings.sync.xui_protocol=='xray' %}selected{% endif %}>Xray</option>
|
||||
<option value="awg" {% if settings.sync.xui_protocol=='awg' %}selected{% endif %}>AmneziaWG</option>
|
||||
<option value="awg2" {% if settings.sync.xui_protocol=='awg2' %}selected{% endif %}>AmneziaWG 2.0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -502,15 +581,53 @@
|
||||
}
|
||||
}
|
||||
|
||||
function updateProtocolsForXuiSync() {
|
||||
const serverIdx = document.querySelector('[name="xui_server_id"]').value;
|
||||
const protoSelect = document.getElementById('xuiSyncProtocolSelect');
|
||||
protoSelect.innerHTML = '';
|
||||
|
||||
if (serverIdx === '' || !serversData[serverIdx]) return;
|
||||
|
||||
const protocols = serversData[serverIdx].protocols || {};
|
||||
let count = 0;
|
||||
|
||||
for (const [key, info] of Object.entries(protocols)) {
|
||||
if (info.installed) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = key;
|
||||
opt.textContent = key === 'awg' ? 'AmneziaWG' : (key === 'awg2' ? 'AmneziaWG 2.0' : (key === 'awg_legacy' ? 'AWG Legacy' : (key === 'xray' ? 'Xray' : key.toUpperCase())));
|
||||
if (key === "{{ settings.sync.xui_protocol }}") opt.selected = true;
|
||||
protoSelect.appendChild(opt);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (count === 0) {
|
||||
const opt = document.createElement('option');
|
||||
opt.textContent = _('no_protocols');
|
||||
opt.disabled = true;
|
||||
protoSelect.appendChild(opt);
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelector('[name="remnawave_sync"]').addEventListener('change', (e) => {
|
||||
document.getElementById('remnawaveFields').style.display = e.target.checked ? 'block' : 'none';
|
||||
});
|
||||
|
||||
document.querySelector('[name="xui_sync"]').addEventListener('change', (e) => {
|
||||
document.getElementById('xuiFields').style.display = e.target.checked ? 'block' : 'none';
|
||||
});
|
||||
|
||||
document.getElementById('syncCreateConns').addEventListener('change', (e) => {
|
||||
document.getElementById('autoConnFields').style.display = e.target.checked ? 'block' : 'none';
|
||||
if (e.target.checked) updateProtocolsForSync();
|
||||
});
|
||||
|
||||
document.getElementById('xuiSyncCreateConns').addEventListener('change', (e) => {
|
||||
document.getElementById('xuiAutoConnFields').style.display = e.target.checked ? 'block' : 'none';
|
||||
if (e.target.checked) updateProtocolsForXuiSync();
|
||||
});
|
||||
|
||||
document.getElementById('ssl_enabled').addEventListener('change', (e) => {
|
||||
document.getElementById('sslFields').style.display = e.target.checked ? 'block' : 'none';
|
||||
});
|
||||
@@ -760,6 +877,64 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function syncXuiNow() {
|
||||
const btn = document.getElementById('xuiSyncNowBtn');
|
||||
const text = document.getElementById('xuiSyncNowBtnText');
|
||||
const spinner = document.getElementById('xuiSyncNowSpinner');
|
||||
|
||||
btn.disabled = true;
|
||||
text.textContent = _('sync_running');
|
||||
spinner.classList.remove('hidden');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/settings/xui_sync_now', { method: 'POST' });
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
showToast(_('sync_success').replace('{}', data.count), 'success');
|
||||
if (data.message && data.count === 0) {
|
||||
showToast(data.message, 'error');
|
||||
} else {
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
}
|
||||
} else {
|
||||
throw new Error(data.message || 'Error occurred');
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(`${_('error')}: ` + err.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
text.textContent = `🔄 ${_('sync_now_btn')}`;
|
||||
spinner.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSyncXui() {
|
||||
if (!confirm(_('delete_sync_confirm'))) return;
|
||||
|
||||
const btn = document.getElementById('xuiSyncDelBtn');
|
||||
const text = document.getElementById('xuiSyncDelBtnText');
|
||||
const spinner = document.getElementById('xuiSyncDelSpinner');
|
||||
|
||||
btn.disabled = true;
|
||||
text.textContent = _('deleting');
|
||||
spinner.classList.remove('hidden');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/settings/xui_sync_delete', { method: 'POST' });
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
showToast(_('sync_deleted').replace('{}', data.count), 'success');
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(`${_('error')}: ` + err.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
text.textContent = `🗑 ${_('delete_sync_btn')}`;
|
||||
spinner.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
const btn = document.getElementById('saveBtn');
|
||||
const spinner = document.getElementById('saveSpinner');
|
||||
@@ -782,7 +957,16 @@
|
||||
remnawave_sync_users: syncForm.remnawave_sync_users.checked,
|
||||
remnawave_create_conns: syncForm.remnawave_create_conns.checked,
|
||||
remnawave_server_id: parseInt(syncForm.remnawave_server_id.value || '0'),
|
||||
remnawave_protocol: syncForm.remnawave_protocol.value
|
||||
remnawave_protocol: syncForm.remnawave_protocol.value,
|
||||
xui_sync: syncForm.xui_sync.checked,
|
||||
xui_url: syncForm.xui_url.value,
|
||||
xui_username: syncForm.xui_username.value,
|
||||
xui_password: syncForm.xui_password.value,
|
||||
xui_api_token: syncForm.xui_api_token.value,
|
||||
xui_sync_users: syncForm.xui_sync_users.checked,
|
||||
xui_create_conns: syncForm.xui_create_conns.checked,
|
||||
xui_server_id: parseInt(syncForm.xui_server_id.value || '0'),
|
||||
xui_protocol: syncForm.xui_protocol.value
|
||||
};
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user