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