Use client connect domain in VPN configs and fix legacy JSON import to PostgreSQL.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
orohi
2026-07-28 13:00:37 +03:00
co-authored by Cursor
parent 78e69d899a
commit 9b4ef0f9f0
9 changed files with 248 additions and 36 deletions
+4
View File
@@ -7,8 +7,10 @@ from .store import (
ensure_db_ready,
export_data_dict,
import_from_json_file,
invalidate_data_cache,
load_data,
load_tunnel_state,
normalize_import_data,
save_data,
save_tunnel_state,
update_tunnel_state,
@@ -24,8 +26,10 @@ __all__ = [
'get_database_url',
'import_from_json_file',
'init_schema',
'invalidate_data_cache',
'load_data',
'load_tunnel_state',
'normalize_import_data',
'restore_database_sql',
'save_data',
'save_tunnel_state',
+150 -7
View File
@@ -10,8 +10,10 @@ import copy
import json
import logging
import os
import secrets
import shutil
import threading
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional
@@ -83,6 +85,147 @@ def _as_uuid(value: Any) -> UUID:
return UUID(str(value))
def _is_valid_uuid(value: Any) -> bool:
if value is None:
return False
try:
UUID(str(value))
return True
except (ValueError, AttributeError, TypeError):
return False
def _coerce_uuid(value: Any, *, default: Optional[str] = None) -> str:
if _is_valid_uuid(value):
return str(UUID(str(value)))
if default is not None:
return default
return str(uuid.uuid4())
def _as_list(value: Any) -> list:
return list(value) if isinstance(value, list) else []
def normalize_import_data(data: dict) -> dict:
"""Prepare legacy data.json (or partial exports) for PostgreSQL constraints."""
data = copy.deepcopy(data or {})
data['servers'] = [s for s in _as_list(data.get('servers')) if isinstance(s, dict)]
data['users'] = [u for u in _as_list(data.get('users')) if isinstance(u, dict)]
data['user_connections'] = [c for c in _as_list(data.get('user_connections')) if isinstance(c, dict)]
data['api_tokens'] = [t for t in _as_list(data.get('api_tokens')) if isinstance(t, dict)]
data['invite_links'] = [l for l in _as_list(data.get('invite_links')) if isinstance(l, dict)]
if not isinstance(data.get('settings'), dict):
data['settings'] = {}
id_map: dict[str, str] = {}
def map_user_id(value: Any) -> Optional[str]:
if value is None or value == '':
return None
key = str(value)
if _is_valid_uuid(key):
resolved = str(UUID(key))
id_map.setdefault(key, resolved)
return resolved
if key not in id_map:
id_map[key] = str(uuid.uuid4())
return id_map[key]
used_usernames: set[str] = set()
for index, user in enumerate(data['users']):
if not isinstance(user, dict):
continue
old_id = user.get('id')
if old_id is None or old_id == '':
user['id'] = str(uuid.uuid4())
else:
key = str(old_id)
if _is_valid_uuid(key):
user['id'] = str(UUID(key))
id_map.setdefault(key, user['id'])
else:
user['id'] = map_user_id(key) or str(uuid.uuid4())
username = (user.get('username') or '').strip() or f'user{index + 1}'
base = username
suffix = 2
while username.lower() in used_usernames:
username = f'{base}_{suffix}'
suffix += 1
user['username'] = username
used_usernames.add(username.lower())
user.setdefault('password_hash', '')
user.setdefault('role', 'user')
user.setdefault('enabled', True)
valid_user_ids = {str(u['id']) for u in data['users'] if isinstance(u, dict) and u.get('id')}
for server in data['servers']:
if not isinstance(server, dict):
continue
server['server_info'] = _as_dict(server.get('server_info'))
server['protocols'] = _as_dict(server.get('protocols'))
for conn in data['user_connections']:
if not isinstance(conn, dict):
continue
conn['id'] = _coerce_uuid(conn.get('id'))
mapped_uid = map_user_id(conn.get('user_id'))
if mapped_uid:
conn['user_id'] = mapped_uid
data['user_connections'] = [
c for c in data['user_connections']
if isinstance(c, dict) and str(c.get('user_id')) in valid_user_ids
]
seen_hashes: set[str] = set()
normalized_tokens = []
for token in data['api_tokens']:
if not isinstance(token, dict):
continue
mapped_uid = map_user_id(token.get('user_id'))
if not mapped_uid or mapped_uid not in valid_user_ids:
continue
token['id'] = _coerce_uuid(token.get('id'))
token['user_id'] = mapped_uid
token_hash = (token.get('token_hash') or '').strip()
if not token_hash or token_hash in seen_hashes:
token_hash = secrets.token_hex(32)
token['token_hash'] = token_hash
seen_hashes.add(token_hash)
token.setdefault('token_prefix', (token_hash[:8] if token_hash else ''))
normalized_tokens.append(token)
data['api_tokens'] = normalized_tokens
seen_invite_tokens: set[str] = set()
for link in data['invite_links']:
if not isinstance(link, dict):
continue
link['id'] = _coerce_uuid(link.get('id'))
mapped_uid = map_user_id(link.get('user_id'))
link['user_id'] = mapped_uid if mapped_uid in valid_user_ids else ''
token = (link.get('token') or '').strip()
if not token or token in seen_invite_tokens:
token = secrets.token_urlsafe(16)
while token in seen_invite_tokens:
token = secrets.token_urlsafe(16)
link['token'] = token
seen_invite_tokens.add(token)
guest = data['settings'].get('guest')
if isinstance(guest, dict):
mapped_guest = map_user_id(guest.get('user_id'))
if mapped_guest in valid_user_ids:
guest['user_id'] = mapped_guest
elif guest.get('user_id'):
guest['user_id'] = ''
return data
def _merge_settings(raw: Optional[dict]) -> dict:
settings = json.loads(json.dumps(DEFAULT_SETTINGS))
if not isinstance(raw, dict):
@@ -293,6 +436,7 @@ def load_data() -> dict:
def save_data(data: dict) -> None:
"""Replace panel state in a single transaction (same semantics as rewriting data.json)."""
global _DATA_CACHE
data = normalize_import_data(data)
init_schema()
servers = data.get('servers') or []
users = data.get('users') or []
@@ -484,12 +628,7 @@ def import_from_json_file(path: str | os.PathLike, *, backup: bool = True) -> bo
if not isinstance(data, dict):
raise ValueError(f'Invalid data.json: expected object, got {type(data).__name__}')
data.setdefault('servers', [])
data.setdefault('users', [])
data.setdefault('user_connections', [])
data.setdefault('api_tokens', [])
data.setdefault('invite_links', [])
data.setdefault('settings', {})
data = normalize_import_data(data)
save_data(data)
@@ -555,4 +694,8 @@ def ensure_db_ready(legacy_data_file: Optional[str] = None) -> None:
init_schema()
if legacy_data_file and is_database_empty() and os.path.exists(legacy_data_file):
logger.info('Empty database — importing legacy %s', legacy_data_file)
import_from_json_file(legacy_data_file)
try:
import_from_json_file(legacy_data_file)
except Exception:
logger.exception('Legacy data.json import failed — panel will start with empty DB')
invalidate_data_cache()