Template
Allow re-copying invite configs and end-user server choice (v3.0.1).
Invite pages keep created configs for repeat copy; guest/invite can pick a server when enabled. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -103,7 +103,7 @@ else:
|
||||
application_path = os.path.dirname(__file__)
|
||||
|
||||
DATA_FILE = os.path.join(application_path, 'data.json') # legacy JSON; used only for one-shot import / export
|
||||
CURRENT_VERSION = "v3.0.0"
|
||||
CURRENT_VERSION = "v3.0.1"
|
||||
RELEASES_REPO_URL = repo_url()
|
||||
RELEASES_API_LATEST = api_latest_url()
|
||||
BIN_DIR = os.environ.get('TUNNEL_BIN_DIR', os.path.join(application_path, 'bin'))
|
||||
@@ -2401,6 +2401,7 @@ class GuestSettings(BaseModel):
|
||||
create_server_id: int = 0
|
||||
create_inbound_id: int = 0
|
||||
create_xui_panel_id: str = ''
|
||||
create_allow_server_choice: bool = True
|
||||
|
||||
|
||||
class DonateMethodSettings(BaseModel):
|
||||
@@ -2474,6 +2475,7 @@ class ShareAuthRequest(BaseModel):
|
||||
|
||||
class GuestCreateRequest(BaseModel):
|
||||
name: str = 'Guest VPN'
|
||||
server_id: Optional[int] = None
|
||||
|
||||
|
||||
class InviteCreateRequest(BaseModel):
|
||||
@@ -2488,6 +2490,7 @@ class InviteCreateRequest(BaseModel):
|
||||
duration_days: int = 0 # client lifetime after redeem; 0 = no expiry
|
||||
note: str = ''
|
||||
enabled: bool = True
|
||||
allow_server_choice: bool = True
|
||||
|
||||
|
||||
class InviteUpdateRequest(BaseModel):
|
||||
@@ -2504,10 +2507,12 @@ class InviteUpdateRequest(BaseModel):
|
||||
note: Optional[str] = None
|
||||
enabled: Optional[bool] = None
|
||||
reset_used: bool = False
|
||||
allow_server_choice: Optional[bool] = None
|
||||
|
||||
|
||||
class InviteRedeemRequest(BaseModel):
|
||||
name: str = 'Invite VPN'
|
||||
server_id: Optional[int] = None
|
||||
|
||||
|
||||
class TunnelStartRequest(BaseModel):
|
||||
@@ -5705,6 +5710,7 @@ def _guest_settings(data: Optional[dict] = None) -> dict:
|
||||
'create_server_id': 0,
|
||||
'create_inbound_id': 0,
|
||||
'create_xui_panel_id': '',
|
||||
'create_allow_server_choice': True,
|
||||
}
|
||||
defaults.update(guest)
|
||||
return defaults
|
||||
@@ -5780,22 +5786,26 @@ async def api_guest_connections(token: str, request: Request):
|
||||
data, guest, holder, err = _resolve_guest(token, request)
|
||||
if err:
|
||||
return err
|
||||
allow_choice = bool(guest.get('create_allow_server_choice', True))
|
||||
protocol = guest.get('create_protocol') or 'xui'
|
||||
servers = _pickable_servers_for_protocol(data, protocol) if (
|
||||
guest.get('allow_create') and allow_choice and protocol_base(protocol) != 'xui'
|
||||
) else []
|
||||
meta = {
|
||||
'allow_create': bool(guest.get('allow_create')),
|
||||
'create_protocol': protocol,
|
||||
'allow_server_choice': bool(servers),
|
||||
'default_server_id': int(guest.get('create_server_id') or 0),
|
||||
'servers': servers,
|
||||
}
|
||||
if not holder:
|
||||
return {
|
||||
'connections': [],
|
||||
'allow_create': bool(guest.get('allow_create')),
|
||||
'create_protocol': guest.get('create_protocol') or 'xui',
|
||||
}
|
||||
return {'connections': [], **meta}
|
||||
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',
|
||||
}
|
||||
return {'connections': conns, **meta}
|
||||
|
||||
|
||||
@app.post('/api/guest/{token}/config/{connection_id}', tags=["Guest"])
|
||||
@@ -5817,33 +5827,7 @@ async def api_guest_config(token: str, connection_id: str, request: Request):
|
||||
return JSONResponse({'error': 'Subscription expired'}, status_code=403)
|
||||
if maybe_start_user_expiration(data, holder['id']):
|
||||
save_data(data)
|
||||
|
||||
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'],
|
||||
panel_id=conn.get('xui_panel_id') or None,
|
||||
)
|
||||
vpn_link = config if str(config).startswith('http') else (generate_vpn_link(config) if config else '')
|
||||
return {
|
||||
'config': config,
|
||||
'vpn_link': vpn_link,
|
||||
'subscription_url': config if str(config).startswith('http') else '',
|
||||
'expires_at': holder.get('expiration_date'),
|
||||
}
|
||||
|
||||
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'], get_server_connect_host(server, conn['protocol']), port)
|
||||
ssh.disconnect()
|
||||
vpn_link = generate_vpn_link(config) if config else ''
|
||||
return {'config': config, 'vpn_link': vpn_link, 'expires_at': holder.get('expiration_date')}
|
||||
return await _fetch_connection_config_payload(data, conn, expires_at=holder.get('expiration_date'))
|
||||
except Exception as e:
|
||||
logger.exception("Error getting guest config")
|
||||
return JSONResponse({'error': str(e)}, status_code=500)
|
||||
@@ -5864,18 +5848,32 @@ async def api_guest_create(token: str, req: GuestCreateRequest, request: Request
|
||||
name = (req.name or 'Guest VPN').strip() or 'Guest VPN'
|
||||
# Unique-ish name to avoid collisions
|
||||
name = f"{name}_{secrets.token_hex(3)}"
|
||||
allow_choice = bool(guest.get('create_allow_server_choice', True))
|
||||
|
||||
try:
|
||||
from managers.user_expiration import maybe_start_user_expiration, user_is_expired
|
||||
if user_is_expired(holder):
|
||||
return JSONResponse({'error': 'Subscription expired'}, status_code=403)
|
||||
|
||||
if protocol_base(protocol) == 'xui':
|
||||
sid = 0
|
||||
else:
|
||||
try:
|
||||
sid = _resolve_chosen_server_id(
|
||||
data,
|
||||
protocol=protocol,
|
||||
default_server_id=int(guest.get('create_server_id') or 0),
|
||||
requested_server_id=req.server_id,
|
||||
allow_choice=allow_choice,
|
||||
)
|
||||
except ValueError as ve:
|
||||
return JSONResponse({'error': str(ve)}, status_code=400)
|
||||
|
||||
if protocol_base(protocol) == 'aivpn':
|
||||
from managers.aivpn_manager import resolve_provision_protocol
|
||||
sid = int(guest.get('create_server_id') or 0)
|
||||
if sid >= len(data['servers']):
|
||||
return JSONResponse({'error': 'Guest server not found'}, status_code=400)
|
||||
protocol = resolve_provision_protocol(data['servers'][sid], 'aivpn')
|
||||
elif protocol_base(protocol) != 'xui':
|
||||
protocol = _match_protocol_on_server(data['servers'][sid], protocol)
|
||||
|
||||
if protocol_base(protocol) == 'xui':
|
||||
from managers.xui_api import xui_create_vless_config
|
||||
@@ -5900,9 +5898,6 @@ async def api_guest_create(token: str, req: GuestCreateRequest, request: Request
|
||||
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')
|
||||
@@ -5970,12 +5965,90 @@ async def api_guest_regenerate_token(request: Request):
|
||||
|
||||
# ======================== Invite links (limited config creation) ========================
|
||||
|
||||
def _invite_public_view(link: dict) -> dict:
|
||||
def _server_supports_protocol(server: dict, protocol: str) -> bool:
|
||||
"""True if server has an installed client VPN matching protocol (or any for aivpn)."""
|
||||
base = protocol_base(protocol)
|
||||
if base == 'xui':
|
||||
return False
|
||||
protocols = server.get('protocols') or {}
|
||||
for key, info in protocols.items():
|
||||
if not isinstance(info, dict) or not info.get('installed'):
|
||||
continue
|
||||
kb = protocol_base(key)
|
||||
if kb not in CLIENT_VPN_BASES or kb == 'xui':
|
||||
continue
|
||||
if base == 'aivpn' or kb == base or key == protocol:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _match_protocol_on_server(server: dict, protocol: str) -> str:
|
||||
"""Map requested protocol to an installed key on this server (prefer exact)."""
|
||||
protocols = server.get('protocols') or {}
|
||||
if protocol in protocols and isinstance(protocols.get(protocol), dict) and protocols[protocol].get('installed'):
|
||||
return protocol
|
||||
base = protocol_base(protocol)
|
||||
if base == 'aivpn':
|
||||
return protocol
|
||||
candidates = [
|
||||
key for key, info in protocols.items()
|
||||
if isinstance(info, dict) and info.get('installed') and protocol_base(key) == base
|
||||
]
|
||||
if not candidates:
|
||||
return protocol
|
||||
candidates.sort()
|
||||
return candidates[0]
|
||||
|
||||
|
||||
def _pickable_servers_for_protocol(data: dict, protocol: str) -> list:
|
||||
"""Safe server list for end-user pickers (name/host/id only)."""
|
||||
out = []
|
||||
for idx, server in enumerate(data.get('servers') or []):
|
||||
if not isinstance(server, dict):
|
||||
continue
|
||||
if not _server_supports_protocol(server, protocol):
|
||||
continue
|
||||
out.append({
|
||||
'id': idx,
|
||||
'name': server.get('name') or server.get('host') or f'Server {idx + 1}',
|
||||
'host': server.get('host') or '',
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def _resolve_chosen_server_id(
|
||||
data: dict,
|
||||
*,
|
||||
protocol: str,
|
||||
default_server_id: int,
|
||||
requested_server_id: Optional[int],
|
||||
allow_choice: bool,
|
||||
) -> int:
|
||||
"""Pick server_id for guest/invite create; validate against installed protocols."""
|
||||
servers = data.get('servers') or []
|
||||
default_sid = int(default_server_id or 0)
|
||||
if not allow_choice or requested_server_id is None:
|
||||
sid = default_sid
|
||||
else:
|
||||
sid = int(requested_server_id)
|
||||
if sid < 0 or sid >= len(servers):
|
||||
raise ValueError('Server not found')
|
||||
if protocol_base(protocol) != 'xui' and not _server_supports_protocol(servers[sid], protocol):
|
||||
raise ValueError('Selected server does not have the required protocol')
|
||||
return sid
|
||||
|
||||
|
||||
def _invite_public_view(link: dict, data: Optional[dict] = None) -> dict:
|
||||
max_uses = int(link.get('max_uses') or 0)
|
||||
used = int(link.get('used_count') or 0)
|
||||
remaining = None if max_uses <= 0 else max(0, max_uses - used)
|
||||
exhausted = remaining is not None and remaining <= 0
|
||||
duration_days = int(link.get('duration_days') or 0)
|
||||
protocol = link.get('protocol') or 'awg'
|
||||
allow_server_choice = bool(link.get('allow_server_choice', True)) and protocol_base(protocol) != 'xui'
|
||||
servers = []
|
||||
if data is not None and allow_server_choice:
|
||||
servers = _pickable_servers_for_protocol(data, protocol)
|
||||
return {
|
||||
'id': link.get('id'),
|
||||
'name': link.get('name') or 'Invite',
|
||||
@@ -5988,8 +6061,10 @@ def _invite_public_view(link: dict) -> dict:
|
||||
'expired': False,
|
||||
'exhausted': exhausted,
|
||||
'has_password': bool(link.get('password_hash')),
|
||||
'protocol': link.get('protocol') or 'awg',
|
||||
'protocol': protocol,
|
||||
'server_id': int(link.get('server_id') or 0),
|
||||
'allow_server_choice': allow_server_choice,
|
||||
'servers': servers,
|
||||
'duration_days': duration_days,
|
||||
'user_id': link.get('user_id') or '',
|
||||
'note': link.get('note') or '',
|
||||
@@ -6008,6 +6083,58 @@ def _invite_auth_ok(link: dict, request: Request) -> bool:
|
||||
return bool(request.session.get(f"invite_auth_{link.get('token')}"))
|
||||
|
||||
|
||||
def _enrich_invite_conn(c: dict, data: dict) -> dict:
|
||||
out = dict(c)
|
||||
if protocol_base(out.get('protocol', '')) == 'xui':
|
||||
out['server_name'] = '3x-ui'
|
||||
else:
|
||||
sid = int(out.get('server_id') or 0)
|
||||
if sid < len(data.get('servers') or []):
|
||||
srv = data['servers'][sid]
|
||||
out['server_name'] = srv.get('name') or srv.get('host') or ''
|
||||
else:
|
||||
out['server_name'] = 'Unknown'
|
||||
return out
|
||||
|
||||
|
||||
async def _fetch_connection_config_payload(data: dict, conn: dict, expires_at: Optional[str] = None) -> dict:
|
||||
"""Shared config fetch for guest/invite/share self-service surfaces."""
|
||||
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'],
|
||||
panel_id=conn.get('xui_panel_id') or None,
|
||||
)
|
||||
vpn_link = config if str(config).startswith('http') else (generate_vpn_link(config) if config else '')
|
||||
return {
|
||||
'config': config,
|
||||
'vpn_link': vpn_link,
|
||||
'subscription_url': config if str(config).startswith('http') else '',
|
||||
'expires_at': expires_at,
|
||||
}
|
||||
|
||||
sid = int(conn.get('server_id') or 0)
|
||||
if sid >= len(data.get('servers') or []):
|
||||
raise RuntimeError('Server not found')
|
||||
server = data['servers'][sid]
|
||||
protocol = conn['protocol']
|
||||
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)
|
||||
config = await asyncio.to_thread(
|
||||
_manager_call, manager, 'get_client_config',
|
||||
protocol, conn['client_id'], get_server_connect_host(server, protocol), port,
|
||||
)
|
||||
finally:
|
||||
await asyncio.to_thread(ssh.disconnect)
|
||||
vpn_link = generate_vpn_link(config) if config else ''
|
||||
return {'config': config, 'vpn_link': vpn_link, 'expires_at': expires_at}
|
||||
|
||||
|
||||
def _expiry_ms_from_duration_days(duration_days: int) -> int:
|
||||
"""3x-ui expiryTime is unix ms; 0 means no expiry. Starts now (on redeem)."""
|
||||
days = int(duration_days or 0)
|
||||
@@ -6176,11 +6303,12 @@ async def api_create_invite(request: Request, req: InviteCreateRequest):
|
||||
'expires_at': None,
|
||||
'duration_days': int(req.duration_days or 0),
|
||||
'note': req.note or '',
|
||||
'allow_server_choice': bool(req.allow_server_choice),
|
||||
'created_at': datetime.now().isoformat(),
|
||||
}
|
||||
data.setdefault('invite_links', []).append(link)
|
||||
save_data(data)
|
||||
view = _invite_public_view(link)
|
||||
view = _invite_public_view(link, data)
|
||||
return {'status': 'success', 'invite': view, 'url': f"/invite/{link['token']}"}
|
||||
|
||||
|
||||
@@ -6222,6 +6350,8 @@ async def api_update_invite(request: Request, invite_id: str, req: InviteUpdateR
|
||||
link['note'] = req.note
|
||||
if req.enabled is not None:
|
||||
link['enabled'] = bool(req.enabled)
|
||||
if req.allow_server_choice is not None:
|
||||
link['allow_server_choice'] = bool(req.allow_server_choice)
|
||||
if req.reset_used:
|
||||
link['used_count'] = 0
|
||||
if protocol_base(link.get('protocol') or 'xui') == 'xui':
|
||||
@@ -6237,7 +6367,7 @@ async def api_update_invite(request: Request, invite_id: str, req: InviteUpdateR
|
||||
if not int(link.get('xui_inbound_id') or 0):
|
||||
return JSONResponse({'error': 'Select a VLESS inbound from 3x-ui'}, status_code=400)
|
||||
save_data(data)
|
||||
return {'status': 'success', 'invite': _invite_public_view(link)}
|
||||
return {'status': 'success', 'invite': _invite_public_view(link, data)}
|
||||
|
||||
|
||||
@app.delete('/api/invites/{invite_id}', tags=["Invites"])
|
||||
@@ -6263,7 +6393,7 @@ async def invite_public_page(token: str, request: Request):
|
||||
f"<h1>{_t('invite_not_found', lang)}</h1><p>{_t('invite_not_found_desc', lang)}</p>",
|
||||
status_code=404,
|
||||
)
|
||||
view = _invite_public_view(link)
|
||||
view = _invite_public_view(link, data)
|
||||
need_password = bool(link.get('password_hash')) and not _invite_auth_ok(link, request)
|
||||
return tpl(
|
||||
request,
|
||||
@@ -6298,7 +6428,7 @@ async def api_invite_info(token: str, request: Request):
|
||||
return JSONResponse({'error': 'Not found'}, status_code=404)
|
||||
if not _invite_auth_ok(link, request):
|
||||
return JSONResponse({'error': 'Unauthorized'}, status_code=401)
|
||||
view = _invite_public_view(link)
|
||||
view = _invite_public_view(link, data)
|
||||
# Don't leak admin note / ids beyond what's needed
|
||||
return {
|
||||
'name': view['name'],
|
||||
@@ -6311,9 +6441,64 @@ async def api_invite_info(token: str, request: Request):
|
||||
'max_uses': view['max_uses'],
|
||||
'used_count': view['used_count'],
|
||||
'protocol': view['protocol'],
|
||||
'allow_server_choice': view['allow_server_choice'],
|
||||
'server_id': view['server_id'],
|
||||
'servers': view.get('servers') or [],
|
||||
'duration_days': view['duration_days'],
|
||||
}
|
||||
|
||||
|
||||
@app.get('/api/invite/{token}/connections', tags=["Invites"])
|
||||
async def api_invite_connections(token: str, request: Request):
|
||||
"""List configs created via this invite (re-copy without consuming another use)."""
|
||||
data = load_data()
|
||||
link = _find_invite(data, token)
|
||||
if not link:
|
||||
return JSONResponse({'error': 'Not found'}, status_code=404)
|
||||
if not _invite_auth_ok(link, request):
|
||||
return JSONResponse({'error': 'Unauthorized'}, status_code=401)
|
||||
invite_id = link.get('id')
|
||||
holder_id = link.get('user_id') or ''
|
||||
conns = [
|
||||
_enrich_invite_conn(c, data)
|
||||
for c in data.get('user_connections', [])
|
||||
if c.get('invite_id') == invite_id and (not holder_id or c.get('user_id') == holder_id)
|
||||
]
|
||||
conns.sort(key=lambda c: c.get('created_at') or '', reverse=True)
|
||||
return {
|
||||
'connections': conns,
|
||||
'invite': _invite_public_view(link, data),
|
||||
}
|
||||
|
||||
|
||||
@app.post('/api/invite/{token}/config/{connection_id}', tags=["Invites"])
|
||||
async def api_invite_config(token: str, connection_id: str, request: Request):
|
||||
data = load_data()
|
||||
link = _find_invite(data, token)
|
||||
if not link:
|
||||
return JSONResponse({'error': 'Not found'}, status_code=404)
|
||||
if not _invite_auth_ok(link, request):
|
||||
return JSONResponse({'error': 'Unauthorized'}, status_code=401)
|
||||
invite_id = link.get('id')
|
||||
holder_id = link.get('user_id') or ''
|
||||
conn = next(
|
||||
(
|
||||
c for c in data.get('user_connections', [])
|
||||
if c.get('id') == connection_id
|
||||
and c.get('invite_id') == invite_id
|
||||
and (not holder_id or c.get('user_id') == holder_id)
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not conn:
|
||||
return JSONResponse({'error': 'Not found'}, status_code=404)
|
||||
try:
|
||||
return await _fetch_connection_config_payload(data, conn, expires_at=conn.get('expires_at'))
|
||||
except Exception as e:
|
||||
logger.exception('Error getting invite config')
|
||||
return JSONResponse({'error': str(e)}, status_code=500)
|
||||
|
||||
|
||||
@app.post('/api/invite/{token}/create', tags=["Invites"])
|
||||
async def api_invite_create_config(token: str, req: InviteRedeemRequest, request: Request):
|
||||
data = load_data()
|
||||
@@ -6323,7 +6508,7 @@ async def api_invite_create_config(token: str, req: InviteRedeemRequest, request
|
||||
if not _invite_auth_ok(link, request):
|
||||
return JSONResponse({'error': 'Unauthorized'}, status_code=401)
|
||||
|
||||
view = _invite_public_view(link)
|
||||
view = _invite_public_view(link, data)
|
||||
if not view['available']:
|
||||
if view['expired']:
|
||||
return JSONResponse({'error': 'Invite link expired'}, status_code=403)
|
||||
@@ -6335,13 +6520,29 @@ async def api_invite_create_config(token: str, req: InviteRedeemRequest, request
|
||||
if not holder_id or not any(u['id'] == holder_id for u in data['users']):
|
||||
return JSONResponse({'error': 'Invite holder user is not configured'}, status_code=400)
|
||||
|
||||
protocol = link.get('protocol') or 'xui'
|
||||
allow_choice = bool(link.get('allow_server_choice', True))
|
||||
try:
|
||||
if protocol_base(protocol) == 'xui':
|
||||
sid = int(link.get('server_id') or 0)
|
||||
else:
|
||||
sid = _resolve_chosen_server_id(
|
||||
data,
|
||||
protocol=protocol,
|
||||
default_server_id=int(link.get('server_id') or 0),
|
||||
requested_server_id=req.server_id,
|
||||
allow_choice=allow_choice,
|
||||
)
|
||||
except ValueError as ve:
|
||||
return JSONResponse({'error': str(ve)}, status_code=400)
|
||||
|
||||
# Reserve a use slot under lock
|
||||
async with DATA_LOCK:
|
||||
data = load_data()
|
||||
link = _find_invite(data, token)
|
||||
if not link:
|
||||
return JSONResponse({'error': 'Not found'}, status_code=404)
|
||||
view = _invite_public_view(link)
|
||||
view = _invite_public_view(link, data)
|
||||
if not view['available']:
|
||||
return JSONResponse({'error': 'Invite link is no longer available'}, status_code=403)
|
||||
link['used_count'] = int(link.get('used_count') or 0) + 1
|
||||
@@ -6352,10 +6553,11 @@ async def api_invite_create_config(token: str, req: InviteRedeemRequest, request
|
||||
try:
|
||||
data = load_data()
|
||||
protocol = link.get('protocol') or 'xui'
|
||||
sid = int(link.get('server_id') or 0)
|
||||
if protocol_base(protocol) == 'aivpn' and sid < len(data.get('servers') or []):
|
||||
from managers.aivpn_manager import resolve_provision_protocol
|
||||
protocol = resolve_provision_protocol(data['servers'][sid], 'aivpn')
|
||||
elif protocol_base(protocol) != 'xui' and sid < len(data.get('servers') or []):
|
||||
protocol = _match_protocol_on_server(data['servers'][sid], protocol)
|
||||
created = await _create_config_for_protocol(
|
||||
data,
|
||||
protocol=protocol,
|
||||
@@ -6372,16 +6574,18 @@ async def api_invite_create_config(token: str, req: InviteRedeemRequest, request
|
||||
'protocol': created['protocol'],
|
||||
'client_id': created['client_id'],
|
||||
'name': name,
|
||||
'invite_id': link.get('id'),
|
||||
'xui_panel_id': created.get('xui_panel_id') or link.get('xui_panel_id') or '',
|
||||
'created_at': datetime.now().isoformat(),
|
||||
}
|
||||
if created.get('expires_at'):
|
||||
conn['expires_at'] = created['expires_at']
|
||||
async with DATA_LOCK:
|
||||
data = load_data()
|
||||
data.setdefault('user_connections', []).append(conn)
|
||||
save_data(data)
|
||||
config = created.get('config') or ''
|
||||
subscription_url = created.get('subscription_url') or ''
|
||||
# For subscription URLs, vpn_link is the same shareable string
|
||||
vpn_link = subscription_url or (generate_vpn_link(config) if config else '')
|
||||
data = load_data()
|
||||
link = _find_invite(data, token) or link
|
||||
@@ -6391,8 +6595,8 @@ async def api_invite_create_config(token: str, req: InviteRedeemRequest, request
|
||||
'subscription_url': subscription_url,
|
||||
'vpn_link': vpn_link,
|
||||
'expires_at': created.get('expires_at'),
|
||||
'connection': conn,
|
||||
'invite': _invite_public_view(link),
|
||||
'connection': _enrich_invite_conn(conn, data),
|
||||
'invite': _invite_public_view(link, data),
|
||||
}
|
||||
except Exception as e:
|
||||
# Roll back reserved use
|
||||
|
||||
Reference in New Issue
Block a user