Template
Add AWG/WireGuard connect-domain UI on server page and fix Traefik port for Dokploy.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -103,7 +103,7 @@ else:
|
|||||||
application_path = os.path.dirname(__file__)
|
application_path = os.path.dirname(__file__)
|
||||||
|
|
||||||
DATA_FILE = os.path.join(application_path, 'data.json') # legacy JSON; used only for one-shot import / export
|
DATA_FILE = os.path.join(application_path, 'data.json') # legacy JSON; used only for one-shot import / export
|
||||||
CURRENT_VERSION = "v2.6.4"
|
CURRENT_VERSION = "v2.6.5"
|
||||||
RELEASES_REPO_URL = repo_url()
|
RELEASES_REPO_URL = repo_url()
|
||||||
RELEASES_API_LATEST = api_latest_url()
|
RELEASES_API_LATEST = api_latest_url()
|
||||||
BIN_DIR = os.environ.get('TUNNEL_BIN_DIR', os.path.join(application_path, 'bin'))
|
BIN_DIR = os.environ.get('TUNNEL_BIN_DIR', os.path.join(application_path, 'bin'))
|
||||||
@@ -199,12 +199,26 @@ def get_ssh(server):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_server_connect_host(server: dict) -> str:
|
WG_AWG_PROTOCOLS = frozenset({'awg', 'awg2', 'awg_legacy', 'wireguard'})
|
||||||
|
|
||||||
|
|
||||||
|
def get_server_connect_host(server: dict, protocol: Optional[str] = None) -> str:
|
||||||
"""Hostname embedded in client VPN configs (Endpoint, vless://, etc.).
|
"""Hostname embedded in client VPN configs (Endpoint, vless://, etc.).
|
||||||
|
|
||||||
SSH still uses server['host']; this value can be a DNS name so clients
|
SSH still uses server['host']; this value can be a DNS name so clients
|
||||||
keep working after the server IP changes — update the A-record only.
|
keep working after the server IP changes — update the A-record only.
|
||||||
|
Per-protocol connect_domain (AWG / WireGuard) overrides the server default.
|
||||||
"""
|
"""
|
||||||
|
protocols = server.get('protocols') or {}
|
||||||
|
if protocol:
|
||||||
|
base = protocol_base(protocol)
|
||||||
|
if base in WG_AWG_PROTOCOLS:
|
||||||
|
for key in (protocol, base):
|
||||||
|
pinfo = protocols.get(key) if key else None
|
||||||
|
if isinstance(pinfo, dict):
|
||||||
|
domain = (pinfo.get('connect_domain') or '').strip()
|
||||||
|
if domain:
|
||||||
|
return domain.lower().rstrip('.')
|
||||||
info = server.get('server_info') or {}
|
info = server.get('server_info') or {}
|
||||||
for key in ('connect_domain', 'ssl_domain'):
|
for key in ('connect_domain', 'ssl_domain'):
|
||||||
val = (info.get(key) or '').strip()
|
val = (info.get(key) or '').strip()
|
||||||
@@ -1446,7 +1460,7 @@ def _create_remote_client(manager, protocol: str, server: dict, name: str, sourc
|
|||||||
base = protocol_base(protocol)
|
base = protocol_base(protocol)
|
||||||
if base == 'telemt':
|
if base == 'telemt':
|
||||||
user_data = (source_client or {}).get('userData') or {}
|
user_data = (source_client or {}).get('userData') or {}
|
||||||
connect_host = get_server_connect_host(server)
|
connect_host = get_server_connect_host(server, protocol)
|
||||||
return manager.add_client(
|
return manager.add_client(
|
||||||
protocol, name, connect_host, port,
|
protocol, name, connect_host, port,
|
||||||
telemt_quota=user_data.get('quota'),
|
telemt_quota=user_data.get('quota'),
|
||||||
@@ -1455,7 +1469,7 @@ def _create_remote_client(manager, protocol: str, server: dict, name: str, sourc
|
|||||||
user_ad_tag=user_data.get('user_ad_tag'),
|
user_ad_tag=user_data.get('user_ad_tag'),
|
||||||
max_tcp_conns=user_data.get('max_tcp_conns'),
|
max_tcp_conns=user_data.get('max_tcp_conns'),
|
||||||
)
|
)
|
||||||
connect_host = get_server_connect_host(server)
|
connect_host = get_server_connect_host(server, protocol)
|
||||||
if base == 'wireguard':
|
if base == 'wireguard':
|
||||||
return manager.add_client(name, connect_host)
|
return manager.add_client(name, connect_host)
|
||||||
return manager.add_client(protocol, name, connect_host, port)
|
return manager.add_client(protocol, name, connect_host, port)
|
||||||
@@ -1663,9 +1677,9 @@ async def perform_mass_operations(delete_uids: List[str] = None, toggle_uids: Li
|
|||||||
manager = get_protocol_manager(ssh, c_req['protocol'])
|
manager = get_protocol_manager(ssh, c_req['protocol'])
|
||||||
|
|
||||||
if c_req['protocol'] == 'wireguard':
|
if c_req['protocol'] == 'wireguard':
|
||||||
res = await asyncio.to_thread(manager.add_client, c_req['name'], get_server_connect_host(srv))
|
res = await asyncio.to_thread(manager.add_client, c_req['name'], get_server_connect_host(srv, 'wireguard'))
|
||||||
else:
|
else:
|
||||||
res = await asyncio.to_thread(_manager_call, manager, 'add_client', c_req['protocol'], c_req['name'], get_server_connect_host(srv), port)
|
res = await asyncio.to_thread(_manager_call, manager, 'add_client', c_req['protocol'], c_req['name'], get_server_connect_host(srv, c_req['protocol']), port)
|
||||||
|
|
||||||
if res.get('client_id'):
|
if res.get('client_id'):
|
||||||
new_conn = {
|
new_conn = {
|
||||||
@@ -2129,6 +2143,11 @@ class EditServerRequest(BaseModel):
|
|||||||
connect_domain: Optional[str] = None
|
connect_domain: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectDomainRequest(BaseModel):
|
||||||
|
connect_domain: str = ''
|
||||||
|
protocol: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class ReorderServersRequest(BaseModel):
|
class ReorderServersRequest(BaseModel):
|
||||||
# `order[i]` is the *old* server index now at position `i` in the new layout.
|
# `order[i]` is the *old* server index now at position `i` in the new layout.
|
||||||
order: List[int]
|
order: List[int]
|
||||||
@@ -3049,6 +3068,75 @@ async def api_edit_server(request: Request, server_id: int, req: EditServerReque
|
|||||||
return JSONResponse({'error': str(e)}, status_code=500)
|
return JSONResponse({'error': str(e)}, status_code=500)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get('/api/servers/{server_id}/connect-domain', tags=["Servers"])
|
||||||
|
async def api_get_connect_domain(request: Request, server_id: int, protocol: Optional[str] = None):
|
||||||
|
if not _check_admin(request):
|
||||||
|
return JSONResponse({'error': 'Forbidden'}, status_code=403)
|
||||||
|
data = load_data()
|
||||||
|
if server_id >= len(data['servers']):
|
||||||
|
return JSONResponse({'error': 'Server not found'}, status_code=404)
|
||||||
|
server = data['servers'][server_id]
|
||||||
|
proto = (protocol or '').strip()
|
||||||
|
info = server.get('server_info') or {}
|
||||||
|
proto_domain = ''
|
||||||
|
if proto:
|
||||||
|
pinfo = (server.get('protocols') or {}).get(proto) or {}
|
||||||
|
proto_domain = (pinfo.get('connect_domain') or '').strip()
|
||||||
|
return {
|
||||||
|
'connect_domain': (info.get('connect_domain') or '').strip(),
|
||||||
|
'protocol_connect_domain': proto_domain,
|
||||||
|
'protocol': proto,
|
||||||
|
'effective_host': get_server_connect_host(server, proto or None),
|
||||||
|
'ssh_host': server.get('host') or '',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post('/api/servers/{server_id}/connect-domain', tags=["Servers"])
|
||||||
|
async def api_set_connect_domain(request: Request, server_id: int, req: ConnectDomainRequest):
|
||||||
|
"""Set client Endpoint hostname for AWG / WireGuard without re-verifying SSH."""
|
||||||
|
if not _check_admin(request):
|
||||||
|
return JSONResponse({'error': 'Forbidden'}, status_code=403)
|
||||||
|
try:
|
||||||
|
data = load_data()
|
||||||
|
if server_id >= len(data['servers']):
|
||||||
|
return JSONResponse({'error': 'Server not found'}, status_code=404)
|
||||||
|
server = data['servers'][server_id]
|
||||||
|
domain = (req.connect_domain or '').strip().lower().rstrip('.')
|
||||||
|
proto = (req.protocol or '').strip()
|
||||||
|
|
||||||
|
if proto:
|
||||||
|
base = protocol_base(proto)
|
||||||
|
if base not in WG_AWG_PROTOCOLS:
|
||||||
|
return JSONResponse(
|
||||||
|
{'error': 'Per-protocol domain is only supported for AmneziaWG / WireGuard'},
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
protocols = server.setdefault('protocols', {})
|
||||||
|
proto_info = protocols.setdefault(proto, {})
|
||||||
|
if domain:
|
||||||
|
proto_info['connect_domain'] = domain
|
||||||
|
else:
|
||||||
|
proto_info.pop('connect_domain', None)
|
||||||
|
else:
|
||||||
|
info = dict(server.get('server_info') or {})
|
||||||
|
if domain:
|
||||||
|
info['connect_domain'] = domain
|
||||||
|
else:
|
||||||
|
info.pop('connect_domain', None)
|
||||||
|
server['server_info'] = info
|
||||||
|
|
||||||
|
save_data(data)
|
||||||
|
return {
|
||||||
|
'status': 'success',
|
||||||
|
'connect_domain': domain,
|
||||||
|
'protocol': proto,
|
||||||
|
'effective_host': get_server_connect_host(server, proto or None),
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception('Error saving connect domain')
|
||||||
|
return JSONResponse({'error': str(e)}, status_code=500)
|
||||||
|
|
||||||
|
|
||||||
@app.get('/api/servers/{server_id}/ping', tags=["Servers"])
|
@app.get('/api/servers/{server_id}/ping', tags=["Servers"])
|
||||||
async def api_server_ping(request: Request, server_id: int):
|
async def api_server_ping(request: Request, server_id: int):
|
||||||
"""Cheap reachability check: opens a TCP connection to the SSH port,
|
"""Cheap reachability check: opens a TCP connection to the SSH port,
|
||||||
@@ -3983,7 +4071,7 @@ async def api_protocol_export_clients(request: Request, server_id: int, req: Pro
|
|||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
config = _manager_call(
|
config = _manager_call(
|
||||||
manager, 'get_client_config', req.protocol, client_id, get_server_connect_host(server), port
|
manager, 'get_client_config', req.protocol, client_id, get_server_connect_host(server, req.protocol), port
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
skipped.append(f'{client_name}: {exc}')
|
skipped.append(f'{client_name}: {exc}')
|
||||||
@@ -4467,7 +4555,7 @@ async def api_add_connection(request: Request, server_id: int, req: AddConnectio
|
|||||||
|
|
||||||
if protocol_base(req.protocol) == 'telemt':
|
if protocol_base(req.protocol) == 'telemt':
|
||||||
result = manager.add_client(
|
result = manager.add_client(
|
||||||
req.protocol, req.name, get_server_connect_host(server), port,
|
req.protocol, req.name, get_server_connect_host(server, req.protocol), port,
|
||||||
telemt_quota=req.telemt_quota,
|
telemt_quota=req.telemt_quota,
|
||||||
telemt_max_ips=req.telemt_max_ips,
|
telemt_max_ips=req.telemt_max_ips,
|
||||||
telemt_expiry=req.telemt_expiry,
|
telemt_expiry=req.telemt_expiry,
|
||||||
@@ -4476,9 +4564,9 @@ async def api_add_connection(request: Request, server_id: int, req: AddConnectio
|
|||||||
max_tcp_conns=req.telemt_max_conns
|
max_tcp_conns=req.telemt_max_conns
|
||||||
)
|
)
|
||||||
elif protocol_base(req.protocol) == 'wireguard':
|
elif protocol_base(req.protocol) == 'wireguard':
|
||||||
result = manager.add_client(req.name, get_server_connect_host(server))
|
result = manager.add_client(req.name, get_server_connect_host(server, req.protocol))
|
||||||
else:
|
else:
|
||||||
result = manager.add_client(req.protocol, req.name, get_server_connect_host(server), port)
|
result = manager.add_client(req.protocol, req.name, get_server_connect_host(server, req.protocol), port)
|
||||||
ssh.disconnect()
|
ssh.disconnect()
|
||||||
|
|
||||||
if result.get('config'):
|
if result.get('config'):
|
||||||
@@ -4601,7 +4689,7 @@ async def api_get_connection_config(request: Request, server_id: int, req: Conne
|
|||||||
ssh = get_ssh(server)
|
ssh = get_ssh(server)
|
||||||
ssh.connect()
|
ssh.connect()
|
||||||
manager = get_protocol_manager(ssh, req.protocol)
|
manager = get_protocol_manager(ssh, req.protocol)
|
||||||
config = _manager_call(manager, 'get_client_config', req.protocol, req.client_id, get_server_connect_host(server), port)
|
config = _manager_call(manager, 'get_client_config', req.protocol, req.client_id, get_server_connect_host(server, req.protocol), port)
|
||||||
ssh.disconnect()
|
ssh.disconnect()
|
||||||
vpn_link = generate_vpn_link(config) if config else ''
|
vpn_link = generate_vpn_link(config) if config else ''
|
||||||
return {'config': config, 'vpn_link': vpn_link}
|
return {'config': config, 'vpn_link': vpn_link}
|
||||||
@@ -4784,7 +4872,7 @@ async def api_add_user(request: Request, req: AddUserRequest):
|
|||||||
manager = get_protocol_manager(ssh, req.protocol)
|
manager = get_protocol_manager(ssh, req.protocol)
|
||||||
if protocol_base(req.protocol) == 'telemt':
|
if protocol_base(req.protocol) == 'telemt':
|
||||||
conn_result = manager.add_client(
|
conn_result = manager.add_client(
|
||||||
req.protocol, conn_name, get_server_connect_host(server), port,
|
req.protocol, conn_name, get_server_connect_host(server, req.protocol), port,
|
||||||
telemt_quota=req.telemt_quota,
|
telemt_quota=req.telemt_quota,
|
||||||
telemt_max_ips=req.telemt_max_ips,
|
telemt_max_ips=req.telemt_max_ips,
|
||||||
telemt_expiry=req.telemt_expiry,
|
telemt_expiry=req.telemt_expiry,
|
||||||
@@ -4793,7 +4881,7 @@ async def api_add_user(request: Request, req: AddUserRequest):
|
|||||||
max_tcp_conns=req.telemt_max_conns
|
max_tcp_conns=req.telemt_max_conns
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
conn_result = manager.add_client(req.protocol, conn_name, get_server_connect_host(server), port)
|
conn_result = manager.add_client(req.protocol, conn_name, get_server_connect_host(server, req.protocol), port)
|
||||||
ssh.disconnect()
|
ssh.disconnect()
|
||||||
|
|
||||||
if conn_result.get('client_id'):
|
if conn_result.get('client_id'):
|
||||||
@@ -5001,12 +5089,12 @@ async def api_add_user_connection(request: Request, user_id: str, req: AddUserCo
|
|||||||
# Link existing client
|
# Link existing client
|
||||||
config = await asyncio.to_thread(
|
config = await asyncio.to_thread(
|
||||||
_manager_call, manager, 'get_client_config',
|
_manager_call, manager, 'get_client_config',
|
||||||
req.protocol, req.client_id, get_server_connect_host(server), port,
|
req.protocol, req.client_id, get_server_connect_host(server, req.protocol), port,
|
||||||
)
|
)
|
||||||
result = {'client_id': req.client_id, 'config': config}
|
result = {'client_id': req.client_id, 'config': config}
|
||||||
elif protocol_base(req.protocol) == 'telemt':
|
elif protocol_base(req.protocol) == 'telemt':
|
||||||
result = await asyncio.to_thread(
|
result = await asyncio.to_thread(
|
||||||
manager.add_client, req.protocol, req.name, get_server_connect_host(server), port,
|
manager.add_client, req.protocol, req.name, get_server_connect_host(server, req.protocol), port,
|
||||||
telemt_quota=req.telemt_quota,
|
telemt_quota=req.telemt_quota,
|
||||||
telemt_max_ips=req.telemt_max_ips,
|
telemt_max_ips=req.telemt_max_ips,
|
||||||
telemt_expiry=req.telemt_expiry,
|
telemt_expiry=req.telemt_expiry,
|
||||||
@@ -5015,11 +5103,11 @@ async def api_add_user_connection(request: Request, user_id: str, req: AddUserCo
|
|||||||
max_tcp_conns=req.telemt_max_conns,
|
max_tcp_conns=req.telemt_max_conns,
|
||||||
)
|
)
|
||||||
elif protocol_base(req.protocol) == 'wireguard':
|
elif protocol_base(req.protocol) == 'wireguard':
|
||||||
result = await asyncio.to_thread(manager.add_client, req.name, get_server_connect_host(server))
|
result = await asyncio.to_thread(manager.add_client, req.name, get_server_connect_host(server, req.protocol))
|
||||||
else:
|
else:
|
||||||
result = await asyncio.to_thread(
|
result = await asyncio.to_thread(
|
||||||
_manager_call, manager, 'add_client',
|
_manager_call, manager, 'add_client',
|
||||||
req.protocol, req.name, get_server_connect_host(server), port,
|
req.protocol, req.name, get_server_connect_host(server, req.protocol), port,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
await asyncio.to_thread(ssh.disconnect)
|
await asyncio.to_thread(ssh.disconnect)
|
||||||
@@ -5210,7 +5298,7 @@ async def api_share_config(token: str, connection_id: str, request: Request):
|
|||||||
ssh.connect()
|
ssh.connect()
|
||||||
# Use appropriate manager for the protocol
|
# Use appropriate manager for the protocol
|
||||||
manager = get_protocol_manager(ssh, conn['protocol'])
|
manager = get_protocol_manager(ssh, conn['protocol'])
|
||||||
config = _manager_call(manager, 'get_client_config', conn['protocol'], conn['client_id'], get_server_connect_host(server), port)
|
config = _manager_call(manager, 'get_client_config', conn['protocol'], conn['client_id'], get_server_connect_host(server, conn['protocol']), port)
|
||||||
ssh.disconnect()
|
ssh.disconnect()
|
||||||
vpn_link = generate_vpn_link(config) if config else ''
|
vpn_link = generate_vpn_link(config) if config else ''
|
||||||
return {'config': config, 'vpn_link': vpn_link, 'expires_at': user.get('expiration_date')}
|
return {'config': config, 'vpn_link': vpn_link, 'expires_at': user.get('expiration_date')}
|
||||||
@@ -5370,7 +5458,7 @@ async def api_guest_config(token: str, connection_id: str, request: Request):
|
|||||||
ssh = get_ssh(server)
|
ssh = get_ssh(server)
|
||||||
ssh.connect()
|
ssh.connect()
|
||||||
manager = get_protocol_manager(ssh, conn['protocol'])
|
manager = get_protocol_manager(ssh, conn['protocol'])
|
||||||
config = _manager_call(manager, 'get_client_config', conn['protocol'], conn['client_id'], get_server_connect_host(server), port)
|
config = _manager_call(manager, 'get_client_config', conn['protocol'], conn['client_id'], get_server_connect_host(server, conn['protocol']), port)
|
||||||
ssh.disconnect()
|
ssh.disconnect()
|
||||||
vpn_link = generate_vpn_link(config) if config else ''
|
vpn_link = generate_vpn_link(config) if config else ''
|
||||||
return {'config': config, 'vpn_link': vpn_link, 'expires_at': holder.get('expiration_date')}
|
return {'config': config, 'vpn_link': vpn_link, 'expires_at': holder.get('expiration_date')}
|
||||||
@@ -5434,11 +5522,11 @@ async def api_guest_create(token: str, req: GuestCreateRequest, request: Request
|
|||||||
try:
|
try:
|
||||||
manager = get_protocol_manager(ssh, protocol)
|
manager = get_protocol_manager(ssh, protocol)
|
||||||
if protocol_base(protocol) == 'wireguard':
|
if protocol_base(protocol) == 'wireguard':
|
||||||
result = await asyncio.to_thread(manager.add_client, name, get_server_connect_host(server))
|
result = await asyncio.to_thread(manager.add_client, name, get_server_connect_host(server, protocol))
|
||||||
else:
|
else:
|
||||||
result = await asyncio.to_thread(
|
result = await asyncio.to_thread(
|
||||||
_manager_call, manager, 'add_client',
|
_manager_call, manager, 'add_client',
|
||||||
protocol, name, get_server_connect_host(server), port,
|
protocol, name, get_server_connect_host(server, protocol), port,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
await asyncio.to_thread(ssh.disconnect)
|
await asyncio.to_thread(ssh.disconnect)
|
||||||
@@ -5596,11 +5684,11 @@ async def _create_config_for_protocol(
|
|||||||
try:
|
try:
|
||||||
manager = get_protocol_manager(ssh, protocol)
|
manager = get_protocol_manager(ssh, protocol)
|
||||||
if protocol_base(protocol) == 'wireguard':
|
if protocol_base(protocol) == 'wireguard':
|
||||||
result = await asyncio.to_thread(manager.add_client, name, get_server_connect_host(server))
|
result = await asyncio.to_thread(manager.add_client, name, get_server_connect_host(server, protocol))
|
||||||
else:
|
else:
|
||||||
result = await asyncio.to_thread(
|
result = await asyncio.to_thread(
|
||||||
_manager_call, manager, 'add_client',
|
_manager_call, manager, 'add_client',
|
||||||
protocol, name, get_server_connect_host(server), port,
|
protocol, name, get_server_connect_host(server, protocol), port,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
await asyncio.to_thread(ssh.disconnect)
|
await asyncio.to_thread(ssh.disconnect)
|
||||||
@@ -5971,7 +6059,7 @@ async def api_my_connection_config(request: Request, connection_id: str):
|
|||||||
ssh.connect()
|
ssh.connect()
|
||||||
# Use appropriate manager for the protocol (fixes Telemt/Xray not working for users)
|
# Use appropriate manager for the protocol (fixes Telemt/Xray not working for users)
|
||||||
manager = get_protocol_manager(ssh, conn['protocol'])
|
manager = get_protocol_manager(ssh, conn['protocol'])
|
||||||
config = _manager_call(manager, 'get_client_config', conn['protocol'], conn['client_id'], get_server_connect_host(server), port)
|
config = _manager_call(manager, 'get_client_config', conn['protocol'], conn['client_id'], get_server_connect_host(server, conn['protocol']), port)
|
||||||
ssh.disconnect()
|
ssh.disconnect()
|
||||||
vpn_link = generate_vpn_link(config) if config else ''
|
vpn_link = generate_vpn_link(config) if config else ''
|
||||||
expires_at = None
|
expires_at = None
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ services:
|
|||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.docker.network=dokploy-network
|
- traefik.docker.network=dokploy-network
|
||||||
|
- traefik.http.services.amnezia_panel.loadbalancer.server.port=5000
|
||||||
expose:
|
expose:
|
||||||
- "5000"
|
- "5000"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|||||||
@@ -155,6 +155,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="connectDomainSection" class="hidden" style="margin-top: var(--space-md);">
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
|
<label class="form-label">{{ _('server_connect_domain') }}</label>
|
||||||
|
<div class="flex gap-sm" style="flex-wrap: wrap; align-items: flex-end;">
|
||||||
|
<div style="flex: 0 0 200px; min-width: 160px;">
|
||||||
|
<select class="form-input" id="connectDomainProto" onchange="refreshConnectDomainFields()"></select>
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1; min-width: 200px;">
|
||||||
|
<input class="form-input" type="text" id="connectDomainInput" placeholder="vpn.example.com">
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-primary btn-sm" id="connectDomainSaveBtn" onclick="saveConnectDomain()">
|
||||||
|
{{ _('save') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-hint" style="margin-top: var(--space-xs);">{{ _('server_connect_domain_awg_hint') }}</div>
|
||||||
|
<div class="text-muted text-sm" id="connectDomainEffective" style="margin-top: var(--space-xs);"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Installed Applications Section -->
|
<!-- Installed Applications Section -->
|
||||||
@@ -1132,6 +1151,7 @@
|
|||||||
const SERVER_ID = {{ server_id }};
|
const SERVER_ID = {{ server_id }};
|
||||||
const ALL_SERVERS = {{ servers_for_move | tojson }};
|
const ALL_SERVERS = {{ servers_for_move | tojson }};
|
||||||
const SERVER_HOST = "{{ server.host }}";
|
const SERVER_HOST = "{{ server.host }}";
|
||||||
|
const SERVER_CONNECT_DOMAIN = {{ ((server.server_info or {}).get('connect_domain') or '') | tojson }};
|
||||||
const SERVER_SSL_DOMAIN = {{ ((server.server_info or {}).get('ssl_domain') or '') | tojson }};
|
const SERVER_SSL_DOMAIN = {{ ((server.server_info or {}).get('ssl_domain') or '') | tojson }};
|
||||||
const SERVER_SSL_EMAIL = {{ ((server.server_info or {}).get('ssl_email') or '') | tojson }};
|
const SERVER_SSL_EMAIL = {{ ((server.server_info or {}).get('ssl_email') or '') | tojson }};
|
||||||
const MARKETPLACE_APPS = [
|
const MARKETPLACE_APPS = [
|
||||||
@@ -1153,6 +1173,8 @@
|
|||||||
{ id: 'services', icon: 'wrench', titleKey: 'services' },
|
{ id: 'services', icon: 'wrench', titleKey: 'services' },
|
||||||
{ id: 'web_servers', icon: 'globe', titleKey: 'web_servers' },
|
{ id: 'web_servers', icon: 'globe', titleKey: 'web_servers' },
|
||||||
];
|
];
|
||||||
|
const WG_AWG_BASES = new Set(['awg', 'awg2', 'awg_legacy', 'wireguard']);
|
||||||
|
let connectDomainEffective = {};
|
||||||
let currentInstallProto = 'awg';
|
let currentInstallProto = 'awg';
|
||||||
let currentInstallAnother = false;
|
let currentInstallAnother = false;
|
||||||
let currentConfig = '';
|
let currentConfig = '';
|
||||||
@@ -1212,6 +1234,94 @@
|
|||||||
return Object.keys(installedProtocols).some(key => protoBase(key) === base && installedProtocols[key]);
|
return Object.keys(installedProtocols).some(key => protoBase(key) === base && installedProtocols[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasWgAwgInstalled() {
|
||||||
|
return Object.entries(installedProtocols || {}).some(([key, installed]) => installed && WG_AWG_BASES.has(protoBase(key)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function listInstalledWgAwgProtocols() {
|
||||||
|
const protos = new Set();
|
||||||
|
Object.entries(installedProtocols || {}).forEach(([key, installed]) => {
|
||||||
|
if (installed && WG_AWG_BASES.has(protoBase(key))) protos.add(key);
|
||||||
|
});
|
||||||
|
return Array.from(protos).sort((a, b) => {
|
||||||
|
const ao = MARKETPLACE_APPS.findIndex(app => app.proto === protoBase(a));
|
||||||
|
const bo = MARKETPLACE_APPS.findIndex(app => app.proto === protoBase(b));
|
||||||
|
if (ao !== bo) return ao - bo;
|
||||||
|
return protoInstance(a) - protoInstance(b);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateConnectDomainSection() {
|
||||||
|
const section = document.getElementById('connectDomainSection');
|
||||||
|
if (!section) return;
|
||||||
|
const show = hasWgAwgInstalled();
|
||||||
|
section.classList.toggle('hidden', !show);
|
||||||
|
if (!show) return;
|
||||||
|
const select = document.getElementById('connectDomainProto');
|
||||||
|
if (!select) return;
|
||||||
|
const prev = select.value;
|
||||||
|
const installed = listInstalledWgAwgProtocols();
|
||||||
|
select.innerHTML = `<option value="">${_('server_connect_domain_all')}</option>` +
|
||||||
|
installed.map(p => `<option value="${p}">${getProtoTitle(p)}</option>`).join('');
|
||||||
|
if (prev && [...select.options].some(opt => opt.value === prev)) {
|
||||||
|
select.value = prev;
|
||||||
|
}
|
||||||
|
refreshConnectDomainFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshConnectDomainFields() {
|
||||||
|
const proto = document.getElementById('connectDomainProto')?.value || '';
|
||||||
|
const input = document.getElementById('connectDomainInput');
|
||||||
|
const eff = document.getElementById('connectDomainEffective');
|
||||||
|
try {
|
||||||
|
const query = proto ? `?protocol=${encodeURIComponent(proto)}` : '';
|
||||||
|
const data = await apiCall(`/api/servers/${SERVER_ID}/connect-domain${query}`, 'GET');
|
||||||
|
connectDomainEffective = data;
|
||||||
|
if (input) {
|
||||||
|
input.value = proto ? (data.protocol_connect_domain || '') : (data.connect_domain || '');
|
||||||
|
}
|
||||||
|
if (eff) {
|
||||||
|
if (data.effective_host && data.effective_host !== data.ssh_host) {
|
||||||
|
eff.textContent = `${_('server_connect_domain_current')}: ${data.effective_host} (${_('server_connect_domain_ssh')}: ${data.ssh_host})`;
|
||||||
|
} else {
|
||||||
|
eff.textContent = `${_('server_connect_domain_current')}: ${data.ssh_host || SERVER_HOST}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Object.entries(currentProtocolStatus || {}).forEach(([p, info]) => {
|
||||||
|
if (WG_AWG_BASES.has(protoBase(p)) && isAppInstalled(info)) {
|
||||||
|
updateProtocolCard(p, info);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (input && !proto) input.value = SERVER_CONNECT_DOMAIN || '';
|
||||||
|
if (eff) eff.textContent = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveConnectDomain() {
|
||||||
|
const btn = document.getElementById('connectDomainSaveBtn');
|
||||||
|
const proto = document.getElementById('connectDomainProto')?.value || '';
|
||||||
|
const domain = document.getElementById('connectDomainInput')?.value.trim() || '';
|
||||||
|
if (btn) btn.disabled = true;
|
||||||
|
try {
|
||||||
|
await apiCall(`/api/servers/${SERVER_ID}/connect-domain`, 'POST', {
|
||||||
|
connect_domain: domain,
|
||||||
|
protocol: proto || null,
|
||||||
|
});
|
||||||
|
showToast(_('server_connect_domain_saved'), 'success');
|
||||||
|
await refreshConnectDomainFields();
|
||||||
|
Object.entries(currentProtocolStatus || {}).forEach(([p, info]) => {
|
||||||
|
if (WG_AWG_BASES.has(protoBase(p)) && isAppInstalled(info)) {
|
||||||
|
updateProtocolCard(p, info);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
showToast(err.message, 'error');
|
||||||
|
} finally {
|
||||||
|
if (btn) btn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function switchCategory(cat) {
|
function switchCategory(cat) {
|
||||||
if (cat === 'management') {
|
if (cat === 'management') {
|
||||||
openManagementModal();
|
openManagementModal();
|
||||||
@@ -1370,6 +1480,7 @@
|
|||||||
if (empty) empty.classList.toggle('hidden', installedAppsLoading || installedCount > 0);
|
if (empty) empty.classList.toggle('hidden', installedAppsLoading || installedCount > 0);
|
||||||
const loading = document.getElementById('installedAppsLoading');
|
const loading = document.getElementById('installedAppsLoading');
|
||||||
if (loading) loading.classList.toggle('hidden', !installedAppsLoading);
|
if (loading) loading.classList.toggle('hidden', !installedAppsLoading);
|
||||||
|
updateConnectDomainSection();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProtoTitle(proto) {
|
function getProtoTitle(proto) {
|
||||||
@@ -1672,6 +1783,12 @@
|
|||||||
grid = buildServiceInfoGrid(proto, info);
|
grid = buildServiceInfoGrid(proto, info);
|
||||||
} else if (info.port) {
|
} else if (info.port) {
|
||||||
grid += `<div class="protocol-info-item"><span class="protocol-info-label">${_('port')}</span><span class="protocol-info-value">${info.port}/${(['xray', 'telemt', 'naiveproxy'].includes(protoBase(proto))) ? 'TCP' : 'UDP'}</span></div>`;
|
grid += `<div class="protocol-info-item"><span class="protocol-info-label">${_('port')}</span><span class="protocol-info-value">${info.port}/${(['xray', 'telemt', 'naiveproxy'].includes(protoBase(proto))) ? 'TCP' : 'UDP'}</span></div>`;
|
||||||
|
if (WG_AWG_BASES.has(protoBase(proto))) {
|
||||||
|
const endpoint = (connectDomainEffective.effective_host && connectDomainEffective.effective_host !== SERVER_HOST)
|
||||||
|
? connectDomainEffective.effective_host
|
||||||
|
: SERVER_HOST;
|
||||||
|
grid += `<div class="protocol-info-item"><span class="protocol-info-label">${_('server_endpoint_label')}</span><span class="protocol-info-value">${endpoint}</span></div>`;
|
||||||
|
}
|
||||||
if (protoBase(proto) === 'hysteria' && info.domain) {
|
if (protoBase(proto) === 'hysteria' && info.domain) {
|
||||||
grid += `<div class="protocol-info-item"><span class="protocol-info-label">${_('hysteria_domain')}</span><span class="protocol-info-value">${info.domain}</span></div>`;
|
grid += `<div class="protocol-info-item"><span class="protocol-info-label">${_('hysteria_domain')}</span><span class="protocol-info-value">${info.domain}</span></div>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -399,6 +399,12 @@
|
|||||||
"server_ssl_hint": "Used when installing Hysteria / NGINX on this server (Let's Encrypt).",
|
"server_ssl_hint": "Used when installing Hysteria / NGINX on this server (Let's Encrypt).",
|
||||||
"server_connect_domain": "Client connection domain",
|
"server_connect_domain": "Client connection domain",
|
||||||
"server_connect_domain_hint": "Used in VPN configs instead of IP (Endpoint, vless://, etc.). When moving the server, update the DNS A-record — clients keep working.",
|
"server_connect_domain_hint": "Used in VPN configs instead of IP (Endpoint, vless://, etc.). When moving the server, update the DNS A-record — clients keep working.",
|
||||||
|
"server_connect_domain_awg_hint": "Used in AmneziaWG / WireGuard Endpoint instead of server IP. Leave empty to use SSH host IP.",
|
||||||
|
"server_connect_domain_all": "All AWG / WireGuard (default)",
|
||||||
|
"server_connect_domain_current": "Endpoint in configs",
|
||||||
|
"server_connect_domain_ssh": "SSH",
|
||||||
|
"server_connect_domain_saved": "Connection domain saved",
|
||||||
|
"server_endpoint_label": "Endpoint",
|
||||||
"container_logs": "Container logs",
|
"container_logs": "Container logs",
|
||||||
"logs_btn": "📋 Logs",
|
"logs_btn": "📋 Logs",
|
||||||
"logs_live": "Live",
|
"logs_live": "Live",
|
||||||
|
|||||||
@@ -399,6 +399,12 @@
|
|||||||
"server_ssl_hint": "Подставляется при установке Hysteria / NGINX на этом сервере (Let's Encrypt).",
|
"server_ssl_hint": "Подставляется при установке Hysteria / NGINX на этом сервере (Let's Encrypt).",
|
||||||
"server_connect_domain": "Домен для подключения клиентов",
|
"server_connect_domain": "Домен для подключения клиентов",
|
||||||
"server_connect_domain_hint": "Подставляется в конфиги VPN вместо IP (Endpoint, vless:// и т.д.). При переносе сервера достаточно обновить A-запись DNS — клиенты продолжат работать.",
|
"server_connect_domain_hint": "Подставляется в конфиги VPN вместо IP (Endpoint, vless:// и т.д.). При переносе сервера достаточно обновить A-запись DNS — клиенты продолжат работать.",
|
||||||
|
"server_connect_domain_awg_hint": "Подставляется в Endpoint конфигов AmneziaWG / WireGuard вместо IP сервера. Пусто — использовать IP из SSH.",
|
||||||
|
"server_connect_domain_all": "Все AWG / WireGuard (по умолчанию)",
|
||||||
|
"server_connect_domain_current": "Endpoint в конфигах",
|
||||||
|
"server_connect_domain_ssh": "SSH",
|
||||||
|
"server_connect_domain_saved": "Домен подключения сохранён",
|
||||||
|
"server_endpoint_label": "Endpoint",
|
||||||
"container_logs": "Логи контейнера",
|
"container_logs": "Логи контейнера",
|
||||||
"logs_btn": "📋 Логи",
|
"logs_btn": "📋 Логи",
|
||||||
"logs_live": "В реальном времени",
|
"logs_live": "В реальном времени",
|
||||||
|
|||||||
Reference in New Issue
Block a user