Fix linking existing VPN clients (WireGuard/service protocols and API errors).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -956,10 +956,17 @@ def _manager_call(manager, method, protocol, *args, **kwargs):
|
||||
"""Unified call: WireGuard manager methods don't take protocol_type argument."""
|
||||
fn = getattr(manager, method)
|
||||
if isinstance(manager, WireGuardManager):
|
||||
# WireGuard signatures omit protocol and often omit port.
|
||||
if method in ('get_client_config', 'add_client') and len(args) >= 2:
|
||||
return fn(args[0], args[1])
|
||||
return fn(*args, **kwargs)
|
||||
return fn(protocol, *args, **kwargs)
|
||||
|
||||
|
||||
# Protocols that own VPN clients (can list/link connections)
|
||||
CLIENT_VPN_BASES = {'awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'wireguard'}
|
||||
|
||||
|
||||
def generate_vpn_link(config_text):
|
||||
b64 = base64.b64encode(config_text.strip().encode('utf-8')).decode('utf-8')
|
||||
return f"vpn://{b64}"
|
||||
@@ -3616,22 +3623,27 @@ async def api_add_user_connection(request: Request, user_id: str, req: AddUserCo
|
||||
return JSONResponse({'error': 'User not found'}, status_code=404)
|
||||
if req.server_id >= len(data['servers']):
|
||||
return JSONResponse({'error': 'Server not found'}, status_code=404)
|
||||
if protocol_base(req.protocol) not in CLIENT_VPN_BASES:
|
||||
return JSONResponse(
|
||||
{'error': f'Protocol "{req.protocol}" does not support user connections'},
|
||||
status_code=400,
|
||||
)
|
||||
server = data['servers'][req.server_id]
|
||||
proto_info = server.get('protocols', {}).get(req.protocol, {})
|
||||
port = proto_info.get('port', '55424')
|
||||
ssh = get_ssh(server)
|
||||
await asyncio.to_thread(ssh.connect)
|
||||
manager = get_protocol_manager(ssh, req.protocol)
|
||||
try:
|
||||
manager = get_protocol_manager(ssh, req.protocol)
|
||||
|
||||
if req.client_id:
|
||||
# Use existing client
|
||||
target_client_id = req.client_id
|
||||
# Retrieve config for existing client
|
||||
config = await asyncio.to_thread(manager.get_client_config, req.protocol, req.client_id, server['host'], port)
|
||||
result = {'client_id': target_client_id, 'config': config}
|
||||
else:
|
||||
# Create new client
|
||||
if protocol_base(req.protocol) == 'telemt':
|
||||
if req.client_id:
|
||||
# Link existing client
|
||||
config = await asyncio.to_thread(
|
||||
_manager_call, manager, 'get_client_config',
|
||||
req.protocol, req.client_id, server['host'], port,
|
||||
)
|
||||
result = {'client_id': req.client_id, 'config': config}
|
||||
elif protocol_base(req.protocol) == 'telemt':
|
||||
result = await asyncio.to_thread(
|
||||
manager.add_client, req.protocol, req.name, server['host'], port,
|
||||
telemt_quota=req.telemt_quota,
|
||||
@@ -3639,12 +3651,17 @@ async def api_add_user_connection(request: Request, user_id: str, req: AddUserCo
|
||||
telemt_expiry=req.telemt_expiry,
|
||||
secret=req.telemt_secret,
|
||||
user_ad_tag=req.telemt_ad_tag,
|
||||
max_tcp_conns=req.telemt_max_conns
|
||||
max_tcp_conns=req.telemt_max_conns,
|
||||
)
|
||||
elif protocol_base(req.protocol) == 'wireguard':
|
||||
result = await asyncio.to_thread(manager.add_client, req.name, server['host'])
|
||||
else:
|
||||
result = await asyncio.to_thread(manager.add_client, req.protocol, req.name, server['host'], port)
|
||||
|
||||
await asyncio.to_thread(ssh.disconnect)
|
||||
result = await asyncio.to_thread(
|
||||
_manager_call, manager, 'add_client',
|
||||
req.protocol, req.name, server['host'], port,
|
||||
)
|
||||
finally:
|
||||
await asyncio.to_thread(ssh.disconnect)
|
||||
|
||||
if result.get('client_id'):
|
||||
conn = {
|
||||
@@ -3656,9 +3673,10 @@ async def api_add_user_connection(request: Request, user_id: str, req: AddUserCo
|
||||
'name': req.name,
|
||||
'created_at': datetime.now().isoformat(),
|
||||
}
|
||||
data = load_data()
|
||||
data['user_connections'].append(conn)
|
||||
save_data(data)
|
||||
async with DATA_LOCK:
|
||||
data = load_data()
|
||||
data['user_connections'].append(conn)
|
||||
save_data(data)
|
||||
|
||||
resp = {'status': 'success'}
|
||||
if result.get('config'):
|
||||
@@ -3667,7 +3685,7 @@ async def api_add_user_connection(request: Request, user_id: str, req: AddUserCo
|
||||
return resp
|
||||
except Exception as e:
|
||||
logger.exception("Error adding user connection")
|
||||
return JSONResponse({'error': str(e)}, status_code=500)
|
||||
return JSONResponse({'error': str(e) or e.__class__.__name__}, status_code=500)
|
||||
|
||||
|
||||
@app.get('/api/users/{user_id}/connections', tags=["Users"])
|
||||
@@ -4092,31 +4110,52 @@ async def api_get_server_clients(request: Request, server_id: int, protocol: str
|
||||
if not _check_admin(request):
|
||||
return JSONResponse({'error': 'Forbidden'}, status_code=403)
|
||||
try:
|
||||
if not protocol or protocol_base(protocol) not in CLIENT_VPN_BASES:
|
||||
return JSONResponse(
|
||||
{'error': f'Protocol "{protocol}" does not support client listing'},
|
||||
status_code=400,
|
||||
)
|
||||
data = load_data()
|
||||
if server_id >= len(data['servers']):
|
||||
return JSONResponse({'error': 'Server not found'}, status_code=404)
|
||||
server = data['servers'][server_id]
|
||||
ssh = get_ssh(server)
|
||||
ssh.connect()
|
||||
manager = get_protocol_manager(ssh, protocol)
|
||||
clients = manager.get_clients(protocol)
|
||||
ssh.disconnect()
|
||||
await asyncio.to_thread(ssh.connect)
|
||||
try:
|
||||
manager = get_protocol_manager(ssh, protocol)
|
||||
if not hasattr(manager, 'get_clients'):
|
||||
return JSONResponse(
|
||||
{'error': f'Manager for "{protocol}" has no get_clients'},
|
||||
status_code=400,
|
||||
)
|
||||
clients = await asyncio.to_thread(_manager_call, manager, 'get_clients', protocol)
|
||||
finally:
|
||||
await asyncio.to_thread(ssh.disconnect)
|
||||
|
||||
# Filter: only show clients that are not assigned to anyone in the panel
|
||||
assigned_ids = {c['client_id'] for c in data.get('user_connections', []) if c['server_id'] == server_id and c['protocol'] == protocol}
|
||||
assigned_ids = {
|
||||
c['client_id']
|
||||
for c in data.get('user_connections', [])
|
||||
if c.get('server_id') == server_id and c.get('protocol') == protocol
|
||||
}
|
||||
|
||||
filtered = []
|
||||
for c in clients:
|
||||
if c['clientId'] not in assigned_ids:
|
||||
filtered.append({
|
||||
'id': c['clientId'],
|
||||
'name': c.get('userData', {}).get('clientName', 'Unnamed')
|
||||
})
|
||||
for c in clients or []:
|
||||
cid = c.get('clientId') or c.get('client_id') or c.get('id')
|
||||
if not cid or cid in assigned_ids:
|
||||
continue
|
||||
filtered.append({
|
||||
'id': cid,
|
||||
'name': (c.get('userData') or {}).get('clientName')
|
||||
or c.get('name')
|
||||
or c.get('email')
|
||||
or 'Unnamed',
|
||||
})
|
||||
|
||||
return {'clients': filtered}
|
||||
except Exception as e:
|
||||
logger.exception("Error getting server clients")
|
||||
return JSONResponse({'error': str(e)}, status_code=500)
|
||||
return JSONResponse({'error': str(e) or e.__class__.__name__}, status_code=500)
|
||||
|
||||
|
||||
@app.get('/api/settings/tokens', tags=["API Tokens"])
|
||||
|
||||
+8
-2
@@ -227,10 +227,16 @@
|
||||
throw new Error('Session expired');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const data = await res.json().catch(() => ({}));
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(data.error || 'Unknown error');
|
||||
const detail = data.error || data.message || data.detail;
|
||||
const msg = typeof detail === 'string'
|
||||
? detail
|
||||
: (Array.isArray(detail)
|
||||
? detail.map(d => d.msg || JSON.stringify(d)).join('; ')
|
||||
: (detail ? JSON.stringify(detail) : `HTTP ${res.status}`));
|
||||
throw new Error(msg || 'Unknown error');
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
+23
-10
@@ -586,6 +586,7 @@
|
||||
const select = document.getElementById(selectId);
|
||||
const group = groupId ? document.getElementById(groupId) : null;
|
||||
select.innerHTML = '';
|
||||
const vpnBases = new Set(['awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'wireguard']);
|
||||
|
||||
if (serverId === '') {
|
||||
if (group) group.style.display = 'none';
|
||||
@@ -597,13 +598,14 @@
|
||||
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())));
|
||||
select.appendChild(opt);
|
||||
count++;
|
||||
}
|
||||
if (!info.installed) continue;
|
||||
const base = key.split('__')[0];
|
||||
if (!vpnBases.has(base)) continue;
|
||||
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 === 'wireguard' ? 'WireGuard' : (key === 'telemt' ? 'Telemt' : key.toUpperCase())))));
|
||||
select.appendChild(opt);
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
@@ -765,15 +767,26 @@
|
||||
const select = document.getElementById('ucExistingClient');
|
||||
select.innerHTML = `<option value="">${_('loading')}</option>`;
|
||||
|
||||
if (!protocol) {
|
||||
select.innerHTML = `<option value="">${_('no_protocols')}</option>`;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await apiCall(`/api/servers/${serverId}/${protocol}/clients`);
|
||||
const data = await apiCall(`/api/servers/${serverId}/${encodeURIComponent(protocol)}/clients`);
|
||||
if (!data.clients || data.clients.length === 0) {
|
||||
select.innerHTML = `<option value="">${_('no_free_conns')}</option>`;
|
||||
return;
|
||||
}
|
||||
select.innerHTML = '';
|
||||
select.innerHTML = '<option value="">' + _('select_existing_conn') + '</option>';
|
||||
data.clients.forEach(c => {
|
||||
select.innerHTML += `<option value="${c.id}">${c.name || 'Unnamed'} (${c.id.substring(0, 8)}...)</option>`;
|
||||
const id = c.id || '';
|
||||
const short = id ? (id.length > 8 ? id.substring(0, 8) + '...' : id) : '';
|
||||
const label = `${c.name || 'Unnamed'}${short ? ' (' + short + ')' : ''}`;
|
||||
const opt = document.createElement('option');
|
||||
opt.value = id;
|
||||
opt.textContent = label;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
} catch (err) {
|
||||
select.innerHTML = `<option value="">${_('error')}: ${err.message}</option>`;
|
||||
|
||||
@@ -158,6 +158,7 @@
|
||||
"conn_name_panel": "Connection name (in panel)",
|
||||
"user_conns_title": "User connections",
|
||||
"no_free_conns": "No free connections",
|
||||
"select_connection": "Please select a connection",
|
||||
"linking": "Linking...",
|
||||
"conn_linked": "Connection linked",
|
||||
"enabling": "Enabling",
|
||||
|
||||
@@ -156,6 +156,7 @@
|
||||
"conn_name_panel": "نام اتصال (در پنل)",
|
||||
"user_conns_title": "اتصالهای کاربر",
|
||||
"no_free_conns": "اتصال آزاد موجود نیست",
|
||||
"select_connection": "لطفا یک اتصال انتخاب کنید",
|
||||
"linking": "در حال اتصال...",
|
||||
"conn_linked": "اتصال برقرار شد",
|
||||
"enabling": "در حال فعالسازی",
|
||||
|
||||
@@ -156,6 +156,7 @@
|
||||
"conn_name_panel": "Nom (dans le panneau)",
|
||||
"user_conns_title": "Connexions utilisateur",
|
||||
"no_free_conns": "Aucune connexion libre",
|
||||
"select_connection": "Veuillez sélectionner une connexion",
|
||||
"linking": "Liaison...",
|
||||
"conn_linked": "Connexion liée",
|
||||
"enabling": "Activation",
|
||||
|
||||
@@ -158,6 +158,7 @@
|
||||
"conn_name_panel": "Имя подключения (в панели)",
|
||||
"user_conns_title": "Подключения пользователя",
|
||||
"no_free_conns": "Нет свободных подключений",
|
||||
"select_connection": "Выберите подключение",
|
||||
"linking": "Привязка...",
|
||||
"conn_linked": "Подключение привязано",
|
||||
"enabling": "Включение",
|
||||
|
||||
@@ -156,6 +156,7 @@
|
||||
"conn_name_panel": "连接名称 (面板显示)",
|
||||
"user_conns_title": "用户连接列表",
|
||||
"no_free_conns": "无可用空闲连接",
|
||||
"select_connection": "请选择一个连接",
|
||||
"linking": "关联中...",
|
||||
"conn_linked": "连接关联成功",
|
||||
"enabling": "启用中",
|
||||
|
||||
Reference in New Issue
Block a user