From e612a7bc340b2f4f1770e63a9a08e11c5fb4a05e Mon Sep 17 00:00:00 2001 From: orohi Date: Sun, 26 Jul 2026 00:31:57 +0300 Subject: [PATCH] Fix linking existing VPN clients (WireGuard/service protocols and API errors). Co-authored-by: Cursor --- app.py | 107 +++++++++++++++++++++++++++++-------------- templates/base.html | 10 +++- templates/users.html | 33 +++++++++---- translations/en.json | 1 + translations/fa.json | 1 + translations/fr.json | 1 + translations/ru.json | 1 + translations/zh.json | 1 + 8 files changed, 109 insertions(+), 46 deletions(-) diff --git a/app.py b/app.py index a080753..51e1de4 100644 --- a/app.py +++ b/app.py @@ -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) - - 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': + try: + manager = get_protocol_manager(ssh, req.protocol) + + 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"]) diff --git a/templates/base.html b/templates/base.html index 695c326..8f5e7f4 100644 --- a/templates/base.html +++ b/templates/base.html @@ -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; } diff --git a/templates/users.html b/templates/users.html index 034c8ad..6eb2a5a 100644 --- a/templates/users.html +++ b/templates/users.html @@ -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 = ``; + if (!protocol) { + select.innerHTML = ``; + 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 = ``; return; } - select.innerHTML = ''; + select.innerHTML = ''; data.clients.forEach(c => { - select.innerHTML += ``; + 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 = ``; diff --git a/translations/en.json b/translations/en.json index 70643a7..05eae47 100644 --- a/translations/en.json +++ b/translations/en.json @@ -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", diff --git a/translations/fa.json b/translations/fa.json index 4fcfef6..5058171 100644 --- a/translations/fa.json +++ b/translations/fa.json @@ -156,6 +156,7 @@ "conn_name_panel": "نام اتصال (در پنل)", "user_conns_title": "اتصال‌های کاربر", "no_free_conns": "اتصال آزاد موجود نیست", + "select_connection": "لطفا یک اتصال انتخاب کنید", "linking": "در حال اتصال...", "conn_linked": "اتصال برقرار شد", "enabling": "در حال فعال‌سازی", diff --git a/translations/fr.json b/translations/fr.json index 0a74ec4..6f4afec 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -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", diff --git a/translations/ru.json b/translations/ru.json index fc239f1..20322a9 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -158,6 +158,7 @@ "conn_name_panel": "Имя подключения (в панели)", "user_conns_title": "Подключения пользователя", "no_free_conns": "Нет свободных подключений", + "select_connection": "Выберите подключение", "linking": "Привязка...", "conn_linked": "Подключение привязано", "enabling": "Включение", diff --git a/translations/zh.json b/translations/zh.json index ef69fd2..d0a1a35 100644 --- a/translations/zh.json +++ b/translations/zh.json @@ -156,6 +156,7 @@ "conn_name_panel": "连接名称 (面板显示)", "user_conns_title": "用户连接列表", "no_free_conns": "无可用空闲连接", + "select_connection": "请选择一个连接", "linking": "关联中...", "conn_linked": "连接关联成功", "enabling": "启用中",