Add 3x-ui VLESS configs and guest access without registration.

Guests can open a public link to view or create VPN configs; panel creates VLESS clients via the 3x-ui API.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
orohi
2026-07-26 01:15:24 +03:00
co-authored by Cursor
parent 2196d127b7
commit 5386c4d40e
12 changed files with 1512 additions and 39 deletions
+438 -26
View File
@@ -964,7 +964,7 @@ def _manager_call(manager, method, protocol, *args, **kwargs):
# Protocols that own VPN clients (can list/link connections)
CLIENT_VPN_BASES = {'awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'wireguard'}
CLIENT_VPN_BASES = {'awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'wireguard', 'xui'}
def generate_vpn_link(config_text):
@@ -1050,10 +1050,14 @@ async def perform_delete_user(data: dict, user_id: str):
user = next((u for u in data['users'] if u['id'] == user_id), None)
if not user:
return False
# Remove user's connections from servers
# Remove user's connections from servers / 3x-ui
user_conns = [c for c in data.get('user_connections', []) if c['user_id'] == user_id]
for uc in user_conns:
try:
if protocol_base(uc.get('protocol', '')) == 'xui':
from managers.xui_api import xui_delete_client
await xui_delete_client(data.get('settings', {}), uc['client_id'])
continue
sid = uc['server_id']
if sid < len(data['servers']):
server = data['servers'][sid]
@@ -1080,6 +1084,10 @@ async def perform_toggle_user(data: dict, user_id: str, enable: bool) -> bool:
user_conns = [c for c in data.get('user_connections', []) if c['user_id'] == user_id]
for uc in user_conns:
try:
if protocol_base(uc.get('protocol', '')) == 'xui':
from managers.xui_api import xui_toggle_client
await xui_toggle_client(data.get('settings', {}), uc['client_id'], enable)
continue
sid = uc['server_id']
if sid >= len(data['servers']):
continue
@@ -1699,6 +1707,7 @@ class AddConnectionRequest(BaseModel):
telemt_secret: Optional[str] = None
telemt_ad_tag: Optional[str] = None
telemt_max_conns: Optional[int] = None
xui_inbound_id: Optional[int] = None
class EditConnectionRequest(BaseModel):
@@ -1783,6 +1792,7 @@ class SyncSettings(BaseModel):
xui_create_conns: bool = False
xui_server_id: int = 0
xui_protocol: str = 'xray'
xui_inbound_id: int = 0
class CaptchaSettings(BaseModel):
enabled: bool = False
@@ -1802,6 +1812,16 @@ class TelegramSettings(BaseModel):
enabled: bool = False
class GuestSettings(BaseModel):
enabled: bool = False
token: str = ''
password: str = '' # plaintext from form; empty = keep existing hash
clear_password: bool = False
user_id: str = ''
allow_create: bool = False
create_protocol: str = 'xui'
create_server_id: int = 0
create_inbound_id: int = 0
class UpdateUserRequest(BaseModel):
@@ -1821,6 +1841,7 @@ class SaveSettingsRequest(BaseModel):
captcha: CaptchaSettings
telegram: TelegramSettings
ssl: SSLSettings
guest: GuestSettings = GuestSettings()
class ToggleUserRequest(BaseModel):
@@ -1828,7 +1849,7 @@ class ToggleUserRequest(BaseModel):
class AddUserConnectionRequest(BaseModel):
server_id: int
server_id: int = 0
protocol: str = 'awg'
name: str = 'VPN Connection'
client_id: Optional[str] = None
@@ -1838,6 +1859,7 @@ class AddUserConnectionRequest(BaseModel):
telemt_secret: Optional[str] = None
telemt_ad_tag: Optional[str] = None
telemt_max_conns: Optional[int] = None
xui_inbound_id: Optional[int] = None
class CreateApiTokenRequest(BaseModel):
@@ -1853,6 +1875,10 @@ class ShareAuthRequest(BaseModel):
password: str
class GuestCreateRequest(BaseModel):
name: str = 'Guest VPN'
class TunnelStartRequest(BaseModel):
authtoken: Optional[str] = None
@@ -2107,7 +2133,12 @@ async def periodic_background_tasks():
async def login_page(request: Request):
if get_current_user(request):
return RedirectResponse(url='/', status_code=302)
return tpl(request, 'login.html')
data = load_data()
guest = data.get('settings', {}).get('guest') or {}
guest_link = None
if guest.get('enabled') and guest.get('token'):
guest_link = f"/guest/{guest['token']}"
return tpl(request, 'login.html', guest_link=guest_link)
@app.get("/set_lang/{lang}", tags=["System Templates"])
@@ -2164,7 +2195,15 @@ async def users_page(request: Request):
for u in users_list:
u['connections_count'] = sum(1 for c in conns if c['user_id'] == u['id'])
servers = data['servers']
return tpl(request, 'users.html', users=users_list, servers=servers)
sync = data.get('settings', {}).get('sync', {})
xui_configured = bool((sync.get('xui_url') or '').strip())
return tpl(
request, 'users.html',
users=users_list,
servers=servers,
xui_configured=xui_configured,
xui_inbound_id=sync.get('xui_inbound_id') or 0,
)
@app.get('/my', response_class=HTMLResponse, tags=["System Templates"])
@@ -2176,6 +2215,9 @@ async def my_connections_page(request: Request):
conns = [c for c in data.get('user_connections', []) if c['user_id'] == user['id']]
# Enrich with server names
for c in conns:
if protocol_base(c.get('protocol', '')) == 'xui':
c['server_name'] = '3x-ui'
continue
sid = c.get('server_id', 0)
if sid < len(data['servers']):
c['server_name'] = data['servers'][sid].get('name', data['servers'][sid].get('host', ''))
@@ -3235,6 +3277,31 @@ async def api_add_connection(request: Request, server_id: int, req: AddConnectio
data = load_data()
if server_id >= len(data['servers']):
return JSONResponse({'error': 'Server not found'}, status_code=404)
if protocol_base(req.protocol) == 'xui':
from managers.xui_api import xui_create_vless_config
created = await xui_create_vless_config(
data.get('settings', {}),
name=req.name,
inbound_id=req.xui_inbound_id,
)
result = {'client_id': created['client_id'], 'config': created.get('config') or ''}
if result.get('config'):
result['vpn_link'] = generate_vpn_link(result['config'])
if req.user_id and result.get('client_id'):
conn = {
'id': str(uuid.uuid4()),
'user_id': req.user_id,
'server_id': server_id,
'protocol': 'xui',
'client_id': result['client_id'],
'name': req.name,
'created_at': datetime.now().isoformat(),
}
data['user_connections'].append(conn)
save_data(data)
return result
server = data['servers'][server_id]
proto_info = server.get('protocols', {}).get(req.protocol, {})
port = proto_info.get('port', '55424')
@@ -3287,20 +3354,26 @@ async def api_remove_connection(request: Request, server_id: int, req: Connectio
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]
if not req.client_id:
return JSONResponse({'error': 'Client ID is required'}, status_code=400)
ssh = get_ssh(server)
ssh.connect()
manager = get_protocol_manager(ssh, req.protocol)
_manager_call(manager, 'remove_client', req.protocol, req.client_id)
ssh.disconnect()
if protocol_base(req.protocol) == 'xui':
from managers.xui_api import xui_delete_client
await xui_delete_client(data.get('settings', {}), req.client_id)
else:
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, req.protocol)
_manager_call(manager, 'remove_client', req.protocol, req.client_id)
ssh.disconnect()
# Remove from user_connections
data['user_connections'] = [
c for c in data.get('user_connections', [])
if not (c.get('client_id') == req.client_id and c.get('server_id') == server_id)
if not (c.get('client_id') == req.client_id and c.get('server_id') == server_id
and c.get('protocol') == req.protocol)
]
save_data(data)
return {'status': 'success'}
@@ -3347,8 +3420,8 @@ async def api_get_connection_config(request: Request, server_id: int, req: Conne
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)
if not req.client_id:
return JSONResponse({'error': 'Client ID is required'}, status_code=400)
# Users can only view their own connections
if user['role'] == 'user':
owned = any(
@@ -3357,9 +3430,16 @@ async def api_get_connection_config(request: Request, server_id: int, req: Conne
)
if not owned:
return JSONResponse({'error': 'Forbidden'}, status_code=403)
if protocol_base(req.protocol) == 'xui':
from managers.xui_api import xui_get_config
config = await xui_get_config(data.get('settings', {}), req.client_id)
vpn_link = generate_vpn_link(config) if config else ''
return {'config': config, 'vpn_link': vpn_link}
if server_id >= len(data['servers']):
return JSONResponse({'error': 'Server not found'}, status_code=404)
server = data['servers'][server_id]
if not req.client_id:
return JSONResponse({'error': 'Client ID is required'}, status_code=400)
proto_info = server.get('protocols', {}).get(req.protocol, {})
port = proto_info.get('port', '55424')
ssh = get_ssh(server)
@@ -3380,18 +3460,21 @@ async def api_toggle_connection(request: Request, server_id: int, req: ToggleCon
return JSONResponse({'error': 'Forbidden'}, status_code=403)
try:
data = load_data()
if not req.client_id:
return JSONResponse({'error': 'Client ID is required'}, status_code=400)
if protocol_base(req.protocol) == 'xui':
from managers.xui_api import xui_toggle_client
await xui_toggle_client(data.get('settings', {}), req.client_id, req.enable)
return {'status': 'success'}
if server_id >= len(data['servers']):
return JSONResponse({'error': 'Server not found'}, status_code=404)
server = data['servers'][server_id]
if not req.client_id:
return JSONResponse({'error': 'Client ID is required'}, status_code=400)
ssh = get_ssh(server)
ssh.connect()
manager = get_protocol_manager(ssh, req.protocol)
_manager_call(manager, 'toggle_client', req.protocol, req.client_id, req.enable)
ssh.disconnect()
status = 'enabled' if req.enable else 'disabled'
return {'status': 'success', 'enabled': req.enable, 'message': f'Connection {status}'}
return {'status': 'success'}
except Exception as e:
logger.exception("Error toggling connection")
return JSONResponse({'error': str(e)}, status_code=500)
@@ -3632,13 +3715,49 @@ async def api_add_user_connection(request: Request, user_id: str, req: AddUserCo
user = next((u for u in data['users'] if u['id'] == user_id), None)
if not user:
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,
)
# 3x-ui VLESS — no SSH; uses panel settings credentials
if protocol_base(req.protocol) == 'xui':
if req.client_id:
from managers.xui_api import xui_get_config
config = await xui_get_config(data.get('settings', {}), req.client_id)
result = {'client_id': req.client_id, 'config': config}
else:
from managers.xui_api import xui_create_vless_config
created = await xui_create_vless_config(
data.get('settings', {}),
name=req.name or user.get('username') or 'user',
inbound_id=req.xui_inbound_id,
)
result = {'client_id': created['client_id'], 'config': created.get('config') or ''}
sid = req.server_id if data['servers'] and req.server_id < len(data['servers']) else 0
if result.get('client_id'):
conn = {
'id': str(uuid.uuid4()),
'user_id': user_id,
'server_id': sid,
'protocol': 'xui',
'client_id': result['client_id'],
'name': req.name,
'created_at': datetime.now().isoformat(),
}
async with DATA_LOCK:
data = load_data()
data['user_connections'].append(conn)
save_data(data)
resp = {'status': 'success'}
if result.get('config'):
resp['config'] = result['config']
resp['vpn_link'] = generate_vpn_link(result['config'])
return resp
if req.server_id >= len(data['servers']):
return JSONResponse({'error': 'Server not found'}, status_code=404)
server = data['servers'][req.server_id]
proto_info = server.get('protocols', {}).get(req.protocol, {})
port = proto_info.get('port', '55424')
@@ -3710,6 +3829,9 @@ async def api_get_user_connections(request: Request, user_id: str):
data = load_data()
conns = [c for c in data.get('user_connections', []) if c['user_id'] == user_id]
for c in conns:
if protocol_base(c.get('protocol', '')) == 'xui':
c['server_name'] = '3x-ui'
continue
sid = c.get('server_id', 0)
if sid < len(data['servers']):
c['server_name'] = data['servers'][sid].get('name', '')
@@ -3726,6 +3848,9 @@ async def api_my_connections(request: Request):
data = load_data()
conns = [c for c in data.get('user_connections', []) if c['user_id'] == user['id']]
for c in conns:
if protocol_base(c.get('protocol', '')) == 'xui':
c['server_name'] = '3x-ui'
continue
sid = c.get('server_id', 0)
if sid < len(data['servers']):
c['server_name'] = data['servers'][sid].get('name', '')
@@ -3825,6 +3950,12 @@ async def api_share_config(token: str, connection_id: str, request: Request):
return JSONResponse({'error': 'Not found'}, status_code=404)
try:
if protocol_base(conn.get('protocol', '')) == 'xui':
from managers.xui_api import xui_get_config
config = await xui_get_config(data.get('settings', {}), conn['client_id'])
vpn_link = generate_vpn_link(config) if config else ''
return {'config': config, 'vpn_link': vpn_link}
sid = conn['server_id']
server = data['servers'][sid]
proto_info = server.get('protocols', {}).get(conn['protocol'], {})
@@ -3842,6 +3973,240 @@ async def api_share_config(token: str, connection_id: str, request: Request):
return JSONResponse({'error': str(e)}, status_code=500)
# ======================== Guest access (no registration) ========================
def _guest_settings(data: Optional[dict] = None) -> dict:
if data is None:
data = load_data()
guest = dict((data.get('settings') or {}).get('guest') or {})
defaults = {
'enabled': False,
'token': '',
'password_hash': None,
'user_id': '',
'allow_create': False,
'create_protocol': 'xui',
'create_server_id': 0,
'create_inbound_id': 0,
}
defaults.update(guest)
return defaults
def _resolve_guest(token: str, request: Request):
"""Return (data, guest_cfg, holder_user, error_response)."""
data = load_data()
guest = _guest_settings(data)
if not guest.get('enabled') or not guest.get('token') or guest.get('token') != token:
return data, guest, None, JSONResponse({'error': 'Forbidden'}, status_code=403)
if guest.get('password_hash') and not request.session.get(f'guest_auth_{token}'):
return data, guest, None, JSONResponse({'error': 'Unauthorized'}, status_code=401)
holder = None
uid = guest.get('user_id') or ''
if uid:
holder = next((u for u in data['users'] if u['id'] == uid), None)
return data, guest, holder, None
def _enrich_guest_conn(c: dict, data: dict) -> dict:
out = dict(c)
if protocol_base(out.get('protocol', '')) == 'xui':
out['server_name'] = '3x-ui'
else:
sid = out.get('server_id', 0)
if sid < len(data['servers']):
out['server_name'] = data['servers'][sid].get('name') or data['servers'][sid]['host']
else:
out['server_name'] = 'Unknown'
return out
@app.get('/guest/{token}', response_class=HTMLResponse, tags=["System Templates"])
async def guest_page(token: str, request: Request):
data = load_data()
guest = _guest_settings(data)
lang = request.cookies.get('lang', 'ru')
if not guest.get('enabled') or guest.get('token') != token:
return HTMLResponse(
f"<h1>{_t('guest_not_found', lang)}</h1><p>{_t('guest_not_found_desc', lang)}</p>",
status_code=404,
)
need_password = bool(guest.get('password_hash')) and not request.session.get(f'guest_auth_{token}')
return tpl(
request,
'guest.html',
need_password=need_password,
token=token,
allow_create=bool(guest.get('allow_create')),
create_protocol=guest.get('create_protocol') or 'xui',
)
@app.post('/api/guest/{token}/auth', tags=["Guest"])
async def api_guest_auth(token: str, req: ShareAuthRequest, request: Request):
data = load_data()
guest = _guest_settings(data)
if not guest.get('enabled') or guest.get('token') != token:
return JSONResponse({'error': 'Link expired or disabled'}, status_code=404)
if not guest.get('password_hash'):
request.session[f'guest_auth_{token}'] = True
return {'status': 'success'}
if verify_password(req.password, guest.get('password_hash') or ''):
request.session[f'guest_auth_{token}'] = True
return {'status': 'success'}
lang = request.cookies.get('lang', 'ru')
return JSONResponse({'error': _t('wrong_guest_password', lang)}, status_code=401)
@app.get('/api/guest/{token}/connections', tags=["Guest"])
async def api_guest_connections(token: str, request: Request):
data, guest, holder, err = _resolve_guest(token, request)
if err:
return err
if not holder:
return {
'connections': [],
'allow_create': bool(guest.get('allow_create')),
'create_protocol': guest.get('create_protocol') or 'xui',
}
conns = [
_enrich_guest_conn(c, data)
for c in data.get('user_connections', [])
if c['user_id'] == holder['id']
]
return {
'connections': conns,
'allow_create': bool(guest.get('allow_create')),
'create_protocol': guest.get('create_protocol') or 'xui',
}
@app.post('/api/guest/{token}/config/{connection_id}', tags=["Guest"])
async def api_guest_config(token: str, connection_id: str, request: Request):
data, guest, holder, err = _resolve_guest(token, request)
if err:
return err
if not holder:
return JSONResponse({'error': 'Not found'}, status_code=404)
conn = next(
(c for c in data.get('user_connections', []) if c['id'] == connection_id and c['user_id'] == holder['id']),
None,
)
if not conn:
return JSONResponse({'error': 'Not found'}, status_code=404)
try:
if protocol_base(conn.get('protocol', '')) == 'xui':
from managers.xui_api import xui_get_config
config = await xui_get_config(data.get('settings', {}), conn['client_id'])
vpn_link = generate_vpn_link(config) if config else ''
return {'config': config, 'vpn_link': vpn_link}
sid = conn['server_id']
server = data['servers'][sid]
proto_info = server.get('protocols', {}).get(conn['protocol'], {})
port = proto_info.get('port', '55424')
ssh = get_ssh(server)
ssh.connect()
manager = get_protocol_manager(ssh, conn['protocol'])
config = _manager_call(manager, 'get_client_config', conn['protocol'], conn['client_id'], server['host'], port)
ssh.disconnect()
vpn_link = generate_vpn_link(config) if config else ''
return {'config': config, 'vpn_link': vpn_link}
except Exception as e:
logger.exception("Error getting guest config")
return JSONResponse({'error': str(e)}, status_code=500)
@app.post('/api/guest/{token}/create', tags=["Guest"])
async def api_guest_create(token: str, req: GuestCreateRequest, request: Request):
"""Create a new VPN config for a guest (no registration)."""
data, guest, holder, err = _resolve_guest(token, request)
if err:
return err
if not guest.get('allow_create'):
return JSONResponse({'error': 'Guest config creation is disabled'}, status_code=403)
if not holder:
return JSONResponse({'error': 'Guest user is not configured in settings'}, status_code=400)
protocol = guest.get('create_protocol') or 'xui'
name = (req.name or 'Guest VPN').strip() or 'Guest VPN'
# Unique-ish name to avoid collisions
name = f"{name}_{secrets.token_hex(3)}"
try:
if protocol_base(protocol) == 'xui':
from managers.xui_api import xui_create_vless_config
created = await xui_create_vless_config(
data.get('settings', {}),
name=name,
inbound_id=guest.get('create_inbound_id') or None,
)
result = {'client_id': created['client_id'], 'config': created.get('config') or ''}
sid = 0
protocol = 'xui'
else:
sid = int(guest.get('create_server_id') or 0)
if sid >= len(data['servers']):
return JSONResponse({'error': 'Guest server not found'}, status_code=400)
server = data['servers'][sid]
proto_info = server.get('protocols', {}).get(protocol, {})
port = proto_info.get('port', '55424')
ssh = get_ssh(server)
await asyncio.to_thread(ssh.connect)
try:
manager = get_protocol_manager(ssh, protocol)
if protocol_base(protocol) == 'wireguard':
result = await asyncio.to_thread(manager.add_client, name, server['host'])
else:
result = await asyncio.to_thread(
_manager_call, manager, 'add_client',
protocol, name, server['host'], port,
)
finally:
await asyncio.to_thread(ssh.disconnect)
if not result.get('client_id'):
return JSONResponse({'error': 'Failed to create config'}, status_code=500)
conn = {
'id': str(uuid.uuid4()),
'user_id': holder['id'],
'server_id': sid,
'protocol': protocol,
'client_id': result['client_id'],
'name': name,
'created_at': datetime.now().isoformat(),
}
async with DATA_LOCK:
data = load_data()
data['user_connections'].append(conn)
save_data(data)
config = result.get('config') or ''
vpn_link = generate_vpn_link(config) if config else ''
return {
'status': 'success',
'connection': _enrich_guest_conn(conn, data),
'config': config,
'vpn_link': vpn_link,
}
except Exception as e:
logger.exception("Error creating guest config")
return JSONResponse({'error': str(e)}, status_code=500)
@app.post('/api/settings/guest/regenerate_token', tags=["Settings"])
async def api_guest_regenerate_token(request: Request):
if not _check_admin(request):
return JSONResponse({'error': 'Forbidden'}, status_code=403)
data = load_data()
guest = _guest_settings(data)
guest['token'] = secrets.token_urlsafe(16)
data.setdefault('settings', {})['guest'] = guest
save_data(data)
return {'status': 'success', 'token': guest['token']}
@app.post('/api/my/connections/{connection_id}/config', tags=["Self-service"])
async def api_my_connection_config(request: Request, connection_id: str):
user = get_current_user(request)
@@ -3855,6 +4220,13 @@ async def api_my_connection_config(request: Request, connection_id: str):
)
if not conn:
return JSONResponse({'error': 'Connection not found'}, status_code=404)
if protocol_base(conn.get('protocol', '')) == 'xui':
from managers.xui_api import xui_get_config
config = await xui_get_config(data.get('settings', {}), conn['client_id'])
vpn_link = generate_vpn_link(config) if config else ''
return {'config': config, 'vpn_link': vpn_link}
sid = conn['server_id']
if sid >= len(data['servers']):
return JSONResponse({'error': 'Server not found'}, status_code=404)
@@ -3880,7 +4252,14 @@ async def settings_page(request: Request):
if not user:
return RedirectResponse('/login')
data = load_data()
return tpl(request, 'settings.html', settings=data.get('settings', {}), servers=data.get('servers', []), current_version=CURRENT_VERSION)
return tpl(
request,
'settings.html',
settings=data.get('settings', {}),
servers=data.get('servers', []),
users=data.get('users', []),
current_version=CURRENT_VERSION,
)
@app.get('/api/settings', tags=["Settings"])
@@ -4009,6 +4388,23 @@ async def save_settings(request: Request, payload: SaveSettingsRequest):
data['settings']['captcha'] = payload.captcha.dict()
data['settings']['telegram'] = payload.telegram.dict()
data['settings']['ssl'] = payload.ssl.dict()
existing_guest = dict(data.get('settings', {}).get('guest') or {})
guest = payload.guest.dict()
password = (guest.pop('password', None) or '').strip()
clear_password = bool(guest.pop('clear_password', False))
token = (guest.get('token') or existing_guest.get('token') or '').strip()
if guest.get('enabled') and not token:
token = secrets.token_urlsafe(16)
guest['token'] = token
if clear_password:
guest['password_hash'] = None
elif password:
guest['password_hash'] = hash_password(password)
else:
guest['password_hash'] = existing_guest.get('password_hash')
data['settings']['guest'] = guest
save_data(data)
logger.info("Settings saved (including captcha and telegram)")
@@ -4023,7 +4419,7 @@ async def save_settings(request: Request, payload: SaveSettingsRequest):
logger.info("Stopping Telegram bot (settings save)...")
asyncio.create_task(tg_bot.stop_bot())
return {"status": "success", "bot_running": tg_bot.is_running()}
return {"status": "success", "bot_running": tg_bot.is_running(), "guest_token": token}
@app.post('/api/settings/telegram/toggle', tags=["Settings"])
@@ -4086,6 +4482,7 @@ async def api_xui_sync_now(request: Request):
for key in (
'xui_url', 'xui_username', 'xui_password', 'xui_api_token',
'xui_create_conns', 'xui_server_id', 'xui_protocol', 'xui_sync_users', 'xui_sync',
'xui_inbound_id',
):
if key in body and body[key] is not None:
sync_cfg[key] = body[key]
@@ -4116,6 +4513,21 @@ async def api_xui_sync_delete(request: Request):
return {'status': 'success', 'count': len(to_delete_ids)}
@app.get('/api/settings/xui/inbounds', tags=["Settings"])
async def api_xui_list_inbounds(request: Request):
"""List VLESS inbounds from the configured 3x-ui panel."""
if not _check_admin(request):
return JSONResponse({'error': 'Forbidden'}, status_code=403)
try:
data = load_data()
from managers.xui_api import xui_list_vless_inbounds
inbounds = await xui_list_vless_inbounds(data.get('settings', {}))
return {'inbounds': inbounds}
except Exception as e:
logger.exception("Error listing 3x-ui inbounds")
return JSONResponse({'error': str(e)}, status_code=500)
@app.get('/api/servers/{server_id}/{protocol}/clients', tags=["Connections"])
async def api_get_server_clients(request: Request, server_id: int, protocol: str):
if not _check_admin(request):