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:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user