graphxr-database-proxy 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- graphxr_database_proxy/__init__.py +16 -0
- graphxr_database_proxy/api/__init__.py +1 -0
- graphxr_database_proxy/api/database.py +266 -0
- graphxr_database_proxy/api/google.py +437 -0
- graphxr_database_proxy/api/projects.py +99 -0
- graphxr_database_proxy/common/util.py +38 -0
- graphxr_database_proxy/drivers/__init__.py +1 -0
- graphxr_database_proxy/drivers/base.py +56 -0
- graphxr_database_proxy/drivers/factory.py +36 -0
- graphxr_database_proxy/drivers/spanner.py +814 -0
- graphxr_database_proxy/main.py +110 -0
- graphxr_database_proxy/models/__init__.py +1 -0
- graphxr_database_proxy/models/google.py +50 -0
- graphxr_database_proxy/models/project.py +170 -0
- graphxr_database_proxy/proxy.py +495 -0
- graphxr_database_proxy/proxyForDev.py +290 -0
- graphxr_database_proxy/services/__init__.py +1 -0
- graphxr_database_proxy/services/project_service.py +150 -0
- graphxr_database_proxy/static/favicon.ico +0 -0
- graphxr_database_proxy/static/index.html +1 -0
- graphxr_database_proxy/static/main.7391ee9773c403483393.css +175 -0
- graphxr_database_proxy/static/main.7391ee9773c403483393.css.map +1 -0
- graphxr_database_proxy/static/main.ce3fbb85a7bc9452edb9.js +2 -0
- graphxr_database_proxy/static/main.ce3fbb85a7bc9452edb9.js.map +1 -0
- graphxr_database_proxy/static/vendors.70542a99f336a8021013.js +3 -0
- graphxr_database_proxy/static/vendors.70542a99f336a8021013.js.LICENSE.txt +95 -0
- graphxr_database_proxy/static/vendors.70542a99f336a8021013.js.map +1 -0
- graphxr_database_proxy-1.0.0.dist-info/METADATA +180 -0
- graphxr_database_proxy-1.0.0.dist-info/RECORD +33 -0
- graphxr_database_proxy-1.0.0.dist-info/WHEEL +5 -0
- graphxr_database_proxy-1.0.0.dist-info/entry_points.txt +2 -0
- graphxr_database_proxy-1.0.0.dist-info/licenses/LICENSE +21 -0
- graphxr_database_proxy-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Development Proxy Configuration Module
|
|
3
|
+
Auto-detect proxy settings and intercept all Python network requests
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import socket
|
|
8
|
+
import urllib.parse
|
|
9
|
+
import urllib.request
|
|
10
|
+
import urllib.error
|
|
11
|
+
from typing import List, Optional, Dict, Any
|
|
12
|
+
import logging
|
|
13
|
+
|
|
14
|
+
# Configure logging
|
|
15
|
+
logging.basicConfig(level=logging.INFO)
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
class ProxyConfig:
|
|
19
|
+
"""Proxy configuration class"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.http_proxy = os.environ.get('http_proxy') or os.environ.get('HTTP_PROXY')
|
|
23
|
+
self.https_proxy = os.environ.get('https_proxy') or os.environ.get('HTTPS_PROXY')
|
|
24
|
+
self.no_proxy = os.environ.get('no_proxy') or os.environ.get('NO_PROXY')
|
|
25
|
+
|
|
26
|
+
# List of domains that need proxy (supports wildcards)
|
|
27
|
+
self.proxy_domains = [
|
|
28
|
+
'googleapis.com',
|
|
29
|
+
'*.googleapis.com',
|
|
30
|
+
'google.com',
|
|
31
|
+
'*.google.com',
|
|
32
|
+
'accounts.google.com',
|
|
33
|
+
'oauth2.googleapis.com',
|
|
34
|
+
'www.googleapis.com',
|
|
35
|
+
'cloudresourcemanager.googleapis.com',
|
|
36
|
+
'spanner.googleapis.com',
|
|
37
|
+
# Add more domains that need proxy
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
# List of domains that don't need proxy
|
|
41
|
+
self.no_proxy_domains = []
|
|
42
|
+
if self.no_proxy:
|
|
43
|
+
self.no_proxy_domains = [domain.strip() for domain in self.no_proxy.split(',')]
|
|
44
|
+
|
|
45
|
+
# Default domains that don't need proxy
|
|
46
|
+
self.no_proxy_domains.extend([
|
|
47
|
+
'localhost',
|
|
48
|
+
'127.0.0.1',
|
|
49
|
+
'::1',
|
|
50
|
+
'.local'
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
def should_use_proxy(self, url: str) -> bool:
|
|
54
|
+
"""Check if URL needs to use proxy"""
|
|
55
|
+
if not (self.http_proxy or self.https_proxy):
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
parsed = urllib.parse.urlparse(url)
|
|
59
|
+
hostname = parsed.hostname or parsed.netloc
|
|
60
|
+
|
|
61
|
+
# Check if it's in the no-proxy domain list
|
|
62
|
+
for no_proxy_domain in self.no_proxy_domains:
|
|
63
|
+
if self._match_domain(hostname, no_proxy_domain):
|
|
64
|
+
logger.debug(f"Skip proxy (no_proxy): {hostname}")
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
# Check if it's in the proxy domain list
|
|
68
|
+
for proxy_domain in self.proxy_domains:
|
|
69
|
+
if self._match_domain(hostname, proxy_domain):
|
|
70
|
+
logger.debug(f"Use proxy: {hostname}")
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
# Default: don't use proxy
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
def _match_domain(self, hostname: str, pattern: str) -> bool:
|
|
77
|
+
"""Match domain pattern with wildcard support"""
|
|
78
|
+
if not hostname:
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
if pattern.startswith('*.'):
|
|
82
|
+
# Wildcard matching
|
|
83
|
+
suffix = pattern[2:]
|
|
84
|
+
return hostname.endswith(suffix) or hostname == suffix
|
|
85
|
+
else:
|
|
86
|
+
# Exact matching
|
|
87
|
+
return hostname == pattern
|
|
88
|
+
|
|
89
|
+
def get_proxy_url(self, scheme: str) -> Optional[str]:
|
|
90
|
+
"""Get proxy URL for corresponding protocol"""
|
|
91
|
+
if scheme.lower() == 'https':
|
|
92
|
+
return self.https_proxy or self.http_proxy
|
|
93
|
+
elif scheme.lower() == 'http':
|
|
94
|
+
return self.http_proxy
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
# Global proxy configuration instance
|
|
98
|
+
proxy_config = ProxyConfig()
|
|
99
|
+
|
|
100
|
+
# Backup of original network functions
|
|
101
|
+
_original_urlopen = urllib.request.urlopen
|
|
102
|
+
_original_socket_create_connection = socket.create_connection
|
|
103
|
+
|
|
104
|
+
def _patched_urlopen(url, data=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, **kwargs):
|
|
105
|
+
"""Proxy-intercepted urlopen function"""
|
|
106
|
+
|
|
107
|
+
# If it's a string URL, check if proxy is needed
|
|
108
|
+
if isinstance(url, str):
|
|
109
|
+
if proxy_config.should_use_proxy(url):
|
|
110
|
+
parsed = urllib.parse.urlparse(url)
|
|
111
|
+
proxy_url = proxy_config.get_proxy_url(parsed.scheme)
|
|
112
|
+
|
|
113
|
+
if proxy_url:
|
|
114
|
+
logger.info(f"Accessing via proxy: {url}")
|
|
115
|
+
logger.debug(f"Using proxy: {proxy_url}")
|
|
116
|
+
|
|
117
|
+
# Create proxy handler
|
|
118
|
+
proxy_handler = urllib.request.ProxyHandler({
|
|
119
|
+
'http': proxy_url,
|
|
120
|
+
'https': proxy_url
|
|
121
|
+
})
|
|
122
|
+
opener = urllib.request.build_opener(proxy_handler)
|
|
123
|
+
|
|
124
|
+
# Create request
|
|
125
|
+
if isinstance(url, str):
|
|
126
|
+
request = urllib.request.Request(url, data=data)
|
|
127
|
+
else:
|
|
128
|
+
request = url
|
|
129
|
+
|
|
130
|
+
return opener.open(request, timeout=timeout)
|
|
131
|
+
|
|
132
|
+
# For other cases, use original function
|
|
133
|
+
return _original_urlopen(url, data=data, timeout=timeout, **kwargs)
|
|
134
|
+
|
|
135
|
+
def _patched_socket_create_connection(address, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, source_address=None):
|
|
136
|
+
"""Proxy-intercepted socket connection function"""
|
|
137
|
+
|
|
138
|
+
# Check if proxy is needed
|
|
139
|
+
if len(address) >= 2:
|
|
140
|
+
hostname, port = address[0], address[1]
|
|
141
|
+
|
|
142
|
+
# Construct URL for checking
|
|
143
|
+
scheme = 'https' if port == 443 else 'http'
|
|
144
|
+
url = f"{scheme}://{hostname}:{port}"
|
|
145
|
+
|
|
146
|
+
if proxy_config.should_use_proxy(url):
|
|
147
|
+
proxy_url = proxy_config.get_proxy_url(scheme)
|
|
148
|
+
if proxy_url:
|
|
149
|
+
logger.debug(f"Socket connection via proxy: {hostname}:{port}")
|
|
150
|
+
# Note: This may require more complex proxy socket implementation
|
|
151
|
+
# For simple cases, we let it fall back to original connection
|
|
152
|
+
|
|
153
|
+
return _original_socket_create_connection(address, timeout, source_address)
|
|
154
|
+
|
|
155
|
+
def setup_requests_proxy():
|
|
156
|
+
"""Set up proxy for requests library"""
|
|
157
|
+
try:
|
|
158
|
+
import requests
|
|
159
|
+
|
|
160
|
+
# Get or create default session
|
|
161
|
+
if not hasattr(requests, '_original_get'):
|
|
162
|
+
requests._original_get = requests.get
|
|
163
|
+
requests._original_post = requests.post
|
|
164
|
+
requests._original_put = requests.put
|
|
165
|
+
requests._original_delete = requests.delete
|
|
166
|
+
requests._original_patch = requests.patch
|
|
167
|
+
|
|
168
|
+
def _proxy_request(method, url, **kwargs):
|
|
169
|
+
"""Proxy request wrapper"""
|
|
170
|
+
if proxy_config.should_use_proxy(url):
|
|
171
|
+
proxy_url = proxy_config.get_proxy_url(urllib.parse.urlparse(url).scheme)
|
|
172
|
+
if proxy_url:
|
|
173
|
+
if 'proxies' not in kwargs:
|
|
174
|
+
kwargs['proxies'] = {}
|
|
175
|
+
kwargs['proxies'].update({
|
|
176
|
+
'http': proxy_url,
|
|
177
|
+
'https': proxy_url
|
|
178
|
+
})
|
|
179
|
+
logger.info(f"Requests accessing via proxy: {url}")
|
|
180
|
+
|
|
181
|
+
# Call original method
|
|
182
|
+
original_method = getattr(requests, f'_original_{method}')
|
|
183
|
+
return original_method(url, **kwargs)
|
|
184
|
+
|
|
185
|
+
# Wrap requests methods
|
|
186
|
+
requests.get = lambda url, **kwargs: _proxy_request('get', url, **kwargs)
|
|
187
|
+
requests.post = lambda url, **kwargs: _proxy_request('post', url, **kwargs)
|
|
188
|
+
requests.put = lambda url, **kwargs: _proxy_request('put', url, **kwargs)
|
|
189
|
+
requests.delete = lambda url, **kwargs: _proxy_request('delete', url, **kwargs)
|
|
190
|
+
requests.patch = lambda url, **kwargs: _proxy_request('patch', url, **kwargs)
|
|
191
|
+
|
|
192
|
+
logger.info("Proxy interception set up for requests library")
|
|
193
|
+
|
|
194
|
+
except ImportError:
|
|
195
|
+
logger.debug("requests library not installed, skipping setup")
|
|
196
|
+
|
|
197
|
+
def setup_google_cloud_proxy():
|
|
198
|
+
"""Set up proxy for Google Cloud client libraries"""
|
|
199
|
+
try:
|
|
200
|
+
# Set Google Cloud related environment variables
|
|
201
|
+
if proxy_config.http_proxy:
|
|
202
|
+
os.environ.setdefault('GOOGLE_CLOUD_PROXY', proxy_config.http_proxy)
|
|
203
|
+
os.environ.setdefault('grpc_proxy', proxy_config.http_proxy)
|
|
204
|
+
os.environ.setdefault('GRPC_PROXY', proxy_config.http_proxy)
|
|
205
|
+
|
|
206
|
+
logger.info("Proxy environment variables set for Google Cloud clients")
|
|
207
|
+
|
|
208
|
+
except Exception as e:
|
|
209
|
+
logger.warning(f"Error setting up Google Cloud proxy: {e}")
|
|
210
|
+
|
|
211
|
+
def install_proxy_interceptor():
|
|
212
|
+
"""Install proxy interceptor"""
|
|
213
|
+
if not (proxy_config.http_proxy or proxy_config.https_proxy):
|
|
214
|
+
logger.info("No proxy settings detected, skipping proxy interceptor installation")
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
logger.info("Proxy settings detected, installing proxy interceptor...")
|
|
218
|
+
logger.info(f"HTTP proxy: {proxy_config.http_proxy}")
|
|
219
|
+
logger.info(f"HTTPS proxy: {proxy_config.https_proxy}")
|
|
220
|
+
logger.info(f"Domains requiring proxy: {proxy_config.proxy_domains}")
|
|
221
|
+
logger.info(f"Domains not requiring proxy: {proxy_config.no_proxy_domains}")
|
|
222
|
+
|
|
223
|
+
# Install urllib interceptor
|
|
224
|
+
urllib.request.urlopen = _patched_urlopen
|
|
225
|
+
|
|
226
|
+
# Install socket interceptor (optional, may affect performance)
|
|
227
|
+
# socket.create_connection = _patched_socket_create_connection
|
|
228
|
+
|
|
229
|
+
# Install requests interceptor
|
|
230
|
+
setup_requests_proxy()
|
|
231
|
+
|
|
232
|
+
# Set up Google Cloud proxy
|
|
233
|
+
setup_google_cloud_proxy()
|
|
234
|
+
|
|
235
|
+
logger.info("Proxy interceptor installation complete!")
|
|
236
|
+
return True
|
|
237
|
+
|
|
238
|
+
def uninstall_proxy_interceptor():
|
|
239
|
+
"""Uninstall proxy interceptor"""
|
|
240
|
+
logger.info("Uninstalling proxy interceptor...")
|
|
241
|
+
|
|
242
|
+
# Restore original functions
|
|
243
|
+
urllib.request.urlopen = _original_urlopen
|
|
244
|
+
socket.create_connection = _original_socket_create_connection
|
|
245
|
+
|
|
246
|
+
# Restore requests original methods
|
|
247
|
+
try:
|
|
248
|
+
import requests
|
|
249
|
+
if hasattr(requests, '_original_get'):
|
|
250
|
+
requests.get = requests._original_get
|
|
251
|
+
requests.post = requests._original_post
|
|
252
|
+
requests.put = requests._original_put
|
|
253
|
+
requests.delete = requests._original_delete
|
|
254
|
+
requests.patch = requests._original_patch
|
|
255
|
+
|
|
256
|
+
except ImportError:
|
|
257
|
+
pass
|
|
258
|
+
|
|
259
|
+
logger.info("Proxy interceptor uninstallation complete!")
|
|
260
|
+
|
|
261
|
+
def test_proxy_connection():
|
|
262
|
+
"""Test proxy connection"""
|
|
263
|
+
test_urls = [
|
|
264
|
+
'https://www.googleapis.com',
|
|
265
|
+
'https://accounts.google.com',
|
|
266
|
+
'https://www.google.com'
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
for url in test_urls:
|
|
270
|
+
try:
|
|
271
|
+
logger.info(f"Testing connection: {url}")
|
|
272
|
+
|
|
273
|
+
if proxy_config.should_use_proxy(url):
|
|
274
|
+
logger.info(f" -> Will use proxy")
|
|
275
|
+
else:
|
|
276
|
+
logger.info(f" -> Direct connection")
|
|
277
|
+
|
|
278
|
+
response = urllib.request.urlopen(url, timeout=10)
|
|
279
|
+
logger.info(f" -> Success (status code: {response.getcode()})")
|
|
280
|
+
|
|
281
|
+
except Exception as e:
|
|
282
|
+
logger.error(f" -> Failed: {e}")
|
|
283
|
+
|
|
284
|
+
# Auto-install proxy interceptor
|
|
285
|
+
if __name__ == "__main__":
|
|
286
|
+
install_proxy_interceptor()
|
|
287
|
+
test_proxy_connection()
|
|
288
|
+
else:
|
|
289
|
+
# Auto-install when imported as module
|
|
290
|
+
install_proxy_interceptor()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Services package
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project service for managing projects and their configurations
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import uuid
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from ..models.project import Project, ProjectCreate, ProjectUpdate
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ProjectService:
|
|
17
|
+
"""Service for managing projects"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, config_dir: str = "config"):
|
|
20
|
+
self.config_dir = Path(config_dir)
|
|
21
|
+
self.projects_file = self.config_dir / "projects.json"
|
|
22
|
+
self._ensure_config_dir()
|
|
23
|
+
|
|
24
|
+
def _ensure_config_dir(self) -> None:
|
|
25
|
+
"""Ensure config directory exists"""
|
|
26
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
if not self.projects_file.exists():
|
|
28
|
+
self._save_projects({})
|
|
29
|
+
|
|
30
|
+
def _load_projects(self) -> Dict[str, Dict]:
|
|
31
|
+
"""Load projects from config file"""
|
|
32
|
+
try:
|
|
33
|
+
with open(self.projects_file, 'r', encoding='utf-8') as f:
|
|
34
|
+
return json.load(f)
|
|
35
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
36
|
+
return {}
|
|
37
|
+
|
|
38
|
+
def _save_projects(self, projects: Dict[str, Dict]) -> None:
|
|
39
|
+
"""Save projects to config file"""
|
|
40
|
+
with open(self.projects_file, 'w', encoding='utf-8') as f:
|
|
41
|
+
json.dump(projects, f, indent=2, ensure_ascii=False, default=str)
|
|
42
|
+
|
|
43
|
+
async def create_project(self, project_data: ProjectCreate) -> Project:
|
|
44
|
+
"""Create a new project"""
|
|
45
|
+
name = project_data.name or str(uuid.uuid4())
|
|
46
|
+
project_id = str(uuid.uuid4())
|
|
47
|
+
now = datetime.utcnow()
|
|
48
|
+
|
|
49
|
+
## name should be unique and only allow alphanumeric, underscore, hyphen
|
|
50
|
+
## Auto remove invalid characters
|
|
51
|
+
name = re.sub(r'[^a-zA-Z0-9_-]', '', name)
|
|
52
|
+
exist_project = await self.get_project_by_name(name)
|
|
53
|
+
if exist_project:
|
|
54
|
+
#update existing project
|
|
55
|
+
await self.update_project(exist_project.id, ProjectUpdate(
|
|
56
|
+
name=project_data.name,
|
|
57
|
+
database_type=project_data.database_type,
|
|
58
|
+
database_config=project_data.database_config
|
|
59
|
+
))
|
|
60
|
+
return exist_project
|
|
61
|
+
|
|
62
|
+
project = Project(
|
|
63
|
+
id=project_id,
|
|
64
|
+
name=name,
|
|
65
|
+
database_type=project_data.database_type,
|
|
66
|
+
database_config=project_data.database_config,
|
|
67
|
+
create_time=now,
|
|
68
|
+
update_time=now
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
projects = self._load_projects()
|
|
72
|
+
projects[project_id] = project.model_dump()
|
|
73
|
+
self._save_projects(projects)
|
|
74
|
+
|
|
75
|
+
return project
|
|
76
|
+
|
|
77
|
+
async def get_project(self, project_id: str) -> Optional[Project]:
|
|
78
|
+
"""Get a project by ID"""
|
|
79
|
+
projects = self._load_projects()
|
|
80
|
+
project_data = projects.get(project_id)
|
|
81
|
+
|
|
82
|
+
if project_data:
|
|
83
|
+
return Project(**project_data)
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
async def get_project_by_name(self, name: str) -> Optional[Project]:
|
|
87
|
+
"""Get a project by name"""
|
|
88
|
+
projects = self._load_projects()
|
|
89
|
+
for project_data in projects.values():
|
|
90
|
+
if project_data.get('name') == name:
|
|
91
|
+
return Project(**project_data)
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
async def list_projects(self) -> List[Project]:
|
|
95
|
+
"""List all projects"""
|
|
96
|
+
projects = self._load_projects()
|
|
97
|
+
return [Project(**project_data) for project_data in projects.values()]
|
|
98
|
+
|
|
99
|
+
async def update_project(self, project_id: str, update_data: ProjectUpdate) -> Optional[Project]:
|
|
100
|
+
"""Update a project"""
|
|
101
|
+
projects = self._load_projects()
|
|
102
|
+
project_data = projects.get(project_id)
|
|
103
|
+
|
|
104
|
+
if not project_data:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
# Update fields
|
|
108
|
+
if update_data.name is not None:
|
|
109
|
+
project_data['name'] = update_data.name
|
|
110
|
+
if update_data.database_config is not None:
|
|
111
|
+
project_data['database_config'] = update_data.database_config.model_dump()
|
|
112
|
+
|
|
113
|
+
project_data['update_time'] = datetime.utcnow().isoformat()
|
|
114
|
+
|
|
115
|
+
projects[project_id] = project_data
|
|
116
|
+
self._save_projects(projects)
|
|
117
|
+
|
|
118
|
+
return Project(**project_data)
|
|
119
|
+
|
|
120
|
+
async def update_project_token(self, project_id: str, token: str, last_refreshed: float, expires_in: Optional[int] = None) -> Optional[Project]:
|
|
121
|
+
"""Update project OAuth token and last refreshed timestamp"""
|
|
122
|
+
projects = self._load_projects()
|
|
123
|
+
project_data = projects.get(project_id)
|
|
124
|
+
|
|
125
|
+
if not project_data:
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
# Update OAuth config token fields
|
|
129
|
+
if 'database_config' in project_data and 'oauth_config' in project_data['database_config']:
|
|
130
|
+
oauth_config = project_data['database_config']['oauth_config']
|
|
131
|
+
oauth_config['token'] = token
|
|
132
|
+
oauth_config['last_refreshed'] = last_refreshed
|
|
133
|
+
if expires_in is not None:
|
|
134
|
+
oauth_config['expires_in'] = expires_in
|
|
135
|
+
|
|
136
|
+
project_data['update_time'] = datetime.utcnow().isoformat()
|
|
137
|
+
|
|
138
|
+
projects[project_id] = project_data
|
|
139
|
+
self._save_projects(projects)
|
|
140
|
+
|
|
141
|
+
return Project(**project_data)
|
|
142
|
+
|
|
143
|
+
async def delete_project(self, project_id: str) -> bool:
|
|
144
|
+
"""Delete a project"""
|
|
145
|
+
projects = self._load_projects()
|
|
146
|
+
if project_id in projects:
|
|
147
|
+
del projects[project_id]
|
|
148
|
+
self._save_projects(projects)
|
|
149
|
+
return True
|
|
150
|
+
return False
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/static/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="GraphXR Database Proxy - Secure middleware for connecting GraphXR to databases"/><title>GraphXR Database Proxy</title><link rel="icon" href="/static/favicon.ico"><script defer="defer" src="/static/vendors.70542a99f336a8021013.js"></script><script defer="defer" src="/static/main.ce3fbb85a7bc9452edb9.js"></script><link href="/static/main.7391ee9773c403483393.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/* App layout styles */
|
|
2
|
+
.app-layout {
|
|
3
|
+
min-height: 100vh;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.app-content {
|
|
7
|
+
padding: 0;
|
|
8
|
+
background-color: #f5f5f5;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.content-wrapper {
|
|
12
|
+
padding: 24px;
|
|
13
|
+
max-width: 1200px;
|
|
14
|
+
margin: 0 auto;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* Header styles */
|
|
18
|
+
.app-header {
|
|
19
|
+
background: #001529;
|
|
20
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
21
|
+
padding: 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.header-content {
|
|
25
|
+
display: flex;
|
|
26
|
+
justify-content: space-between;
|
|
27
|
+
align-items: center;
|
|
28
|
+
padding: 0 24px;
|
|
29
|
+
max-width: 1200px;
|
|
30
|
+
margin: 0 auto;
|
|
31
|
+
height: 64px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.logo-icon {
|
|
35
|
+
font-size: 24px;
|
|
36
|
+
color: #1890ff;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.app-title {
|
|
40
|
+
margin: 0 !important;
|
|
41
|
+
color: #fff;
|
|
42
|
+
font-weight: 600;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.header-button {
|
|
46
|
+
color: rgba(255, 255, 255, 0.65);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.header-button:hover {
|
|
50
|
+
color: #fff;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Project management styles */
|
|
54
|
+
.project-management {
|
|
55
|
+
width: 100%;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.page-header {
|
|
59
|
+
display: flex;
|
|
60
|
+
justify-content: space-between;
|
|
61
|
+
align-items: center;
|
|
62
|
+
margin-bottom: 24px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.page-header h2 {
|
|
66
|
+
margin: 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Project form styles */
|
|
70
|
+
.ant-form-item-label > label.ant-form-item-required:not(.ant-form-item-required-mark-optional) {
|
|
71
|
+
font-weight: 500;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.ant-card .ant-card {
|
|
75
|
+
border: 1px solid #d9d9d9;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.ant-card-head {
|
|
79
|
+
border-bottom: 1px solid #d9d9d9;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* API URLs modal styles */
|
|
83
|
+
.ant-input-group {
|
|
84
|
+
display: flex;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.ant-input-group .ant-input {
|
|
88
|
+
border-radius: 6px 0 0 6px;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.ant-input-group .ant-btn {
|
|
92
|
+
border-radius: 0 6px 6px 0;
|
|
93
|
+
border-left: 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Responsive design */
|
|
97
|
+
@media (max-width: 768px) {
|
|
98
|
+
.content-wrapper {
|
|
99
|
+
padding: 16px;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.header-content {
|
|
103
|
+
padding: 0 16px;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.page-header {
|
|
107
|
+
flex-direction: column;
|
|
108
|
+
align-items: flex-start;
|
|
109
|
+
gap: 16px;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.ant-table {
|
|
113
|
+
font-size: 12px;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.ant-table-tbody > tr > td {
|
|
117
|
+
padding: 8px;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/* Global styles */
|
|
121
|
+
* {
|
|
122
|
+
box-sizing: border-box;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
body {
|
|
126
|
+
margin: 0;
|
|
127
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
128
|
+
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
129
|
+
sans-serif;
|
|
130
|
+
-webkit-font-smoothing: antialiased;
|
|
131
|
+
-moz-osx-font-smoothing: grayscale;
|
|
132
|
+
background-color: #f5f5f5;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
code {
|
|
136
|
+
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
|
137
|
+
monospace;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* Ant Design customizations */
|
|
141
|
+
.ant-layout {
|
|
142
|
+
min-height: 100vh;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.ant-table-thead > tr > th {
|
|
146
|
+
background-color: #fafafa;
|
|
147
|
+
font-weight: 600;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.ant-btn {
|
|
151
|
+
border-radius: 6px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.ant-card {
|
|
155
|
+
border-radius: 8px;
|
|
156
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px rgba(0, 0, 0, 0.02);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.ant-modal {
|
|
160
|
+
border-radius: 8px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.ant-modal-header {
|
|
164
|
+
border-radius: 8px 8px 0 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.ant-input, .ant-select-selector {
|
|
168
|
+
border-radius: 6px;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.ant-form-item-label > label {
|
|
172
|
+
font-weight: 500;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/*# sourceMappingURL=main.7391ee9773c403483393.css.map*/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.7391ee9773c403483393.css","mappings":"AAAA,sBAAsB;AACtB;EACE,iBAAiB;AACnB;;AAEA;EACE,UAAU;EACV,yBAAyB;AAC3B;;AAEA;EACE,aAAa;EACb,iBAAiB;EACjB,cAAc;AAChB;;AAEA,kBAAkB;AAClB;EACE,mBAAmB;EACnB,yCAAyC;EACzC,UAAU;AACZ;;AAEA;EACE,aAAa;EACb,8BAA8B;EAC9B,mBAAmB;EACnB,eAAe;EACf,iBAAiB;EACjB,cAAc;EACd,YAAY;AACd;;AAEA;EACE,eAAe;EACf,cAAc;AAChB;;AAEA;EACE,oBAAoB;EACpB,WAAW;EACX,gBAAgB;AAClB;;AAEA;EACE,gCAAgC;AAClC;;AAEA;EACE,WAAW;AACb;;AAEA,8BAA8B;AAC9B;EACE,WAAW;AACb;;AAEA;EACE,aAAa;EACb,8BAA8B;EAC9B,mBAAmB;EACnB,mBAAmB;AACrB;;AAEA;EACE,SAAS;AACX;;AAEA,wBAAwB;AACxB;EACE,gBAAgB;AAClB;;AAEA;EACE,yBAAyB;AAC3B;;AAEA;EACE,gCAAgC;AAClC;;AAEA,0BAA0B;AAC1B;EACE,aAAa;AACf;;AAEA;EACE,0BAA0B;AAC5B;;AAEA;EACE,0BAA0B;EAC1B,cAAc;AAChB;;AAEA,sBAAsB;AACtB;EACE;IACE,aAAa;EACf;;EAEA;IACE,eAAe;EACjB;;EAEA;IACE,sBAAsB;IACtB,uBAAuB;IACvB,SAAS;EACX;;EAEA;IACE,eAAe;EACjB;;EAEA;IACE,YAAY;EACd;AACF,C;ACtHA,kBAAkB;AAClB;EACE,sBAAsB;AACxB;;AAEA;EACE,SAAS;EACT;;cAEY;EACZ,mCAAmC;EACnC,kCAAkC;EAClC,yBAAyB;AAC3B;;AAEA;EACE;aACW;AACb;;AAEA,8BAA8B;AAC9B;EACE,iBAAiB;AACnB;;AAEA;EACE,yBAAyB;EACzB,gBAAgB;AAClB;;AAEA;EACE,kBAAkB;AACpB;;AAEA;EACE,kBAAkB;EAClB,4GAA4G;AAC9G;;AAEA;EACE,kBAAkB;AACpB;;AAEA;EACE,0BAA0B;AAC5B;;AAEA;EACE,kBAAkB;AACpB;;AAEA;EACE,gBAAgB;AAClB,C","sources":["webpack://graphxr-database-proxy-frontend/./src/styles/App.css","webpack://graphxr-database-proxy-frontend/./src/styles/index.css"],"sourcesContent":["/* App layout styles */\r\n.app-layout {\r\n min-height: 100vh;\r\n}\r\n\r\n.app-content {\r\n padding: 0;\r\n background-color: #f5f5f5;\r\n}\r\n\r\n.content-wrapper {\r\n padding: 24px;\r\n max-width: 1200px;\r\n margin: 0 auto;\r\n}\r\n\r\n/* Header styles */\r\n.app-header {\r\n background: #001529;\r\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\r\n padding: 0;\r\n}\r\n\r\n.header-content {\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n padding: 0 24px;\r\n max-width: 1200px;\r\n margin: 0 auto;\r\n height: 64px;\r\n}\r\n\r\n.logo-icon {\r\n font-size: 24px;\r\n color: #1890ff;\r\n}\r\n\r\n.app-title {\r\n margin: 0 !important;\r\n color: #fff;\r\n font-weight: 600;\r\n}\r\n\r\n.header-button {\r\n color: rgba(255, 255, 255, 0.65);\r\n}\r\n\r\n.header-button:hover {\r\n color: #fff;\r\n}\r\n\r\n/* Project management styles */\r\n.project-management {\r\n width: 100%;\r\n}\r\n\r\n.page-header {\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n margin-bottom: 24px;\r\n}\r\n\r\n.page-header h2 {\r\n margin: 0;\r\n}\r\n\r\n/* Project form styles */\r\n.ant-form-item-label > label.ant-form-item-required:not(.ant-form-item-required-mark-optional) {\r\n font-weight: 500;\r\n}\r\n\r\n.ant-card .ant-card {\r\n border: 1px solid #d9d9d9;\r\n}\r\n\r\n.ant-card-head {\r\n border-bottom: 1px solid #d9d9d9;\r\n}\r\n\r\n/* API URLs modal styles */\r\n.ant-input-group {\r\n display: flex;\r\n}\r\n\r\n.ant-input-group .ant-input {\r\n border-radius: 6px 0 0 6px;\r\n}\r\n\r\n.ant-input-group .ant-btn {\r\n border-radius: 0 6px 6px 0;\r\n border-left: 0;\r\n}\r\n\r\n/* Responsive design */\r\n@media (max-width: 768px) {\r\n .content-wrapper {\r\n padding: 16px;\r\n }\r\n \r\n .header-content {\r\n padding: 0 16px;\r\n }\r\n \r\n .page-header {\r\n flex-direction: column;\r\n align-items: flex-start;\r\n gap: 16px;\r\n }\r\n \r\n .ant-table {\r\n font-size: 12px;\r\n }\r\n \r\n .ant-table-tbody > tr > td {\r\n padding: 8px;\r\n }\r\n}","/* Global styles */\r\n* {\r\n box-sizing: border-box;\r\n}\r\n\r\nbody {\r\n margin: 0;\r\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\r\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\r\n sans-serif;\r\n -webkit-font-smoothing: antialiased;\r\n -moz-osx-font-smoothing: grayscale;\r\n background-color: #f5f5f5;\r\n}\r\n\r\ncode {\r\n font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\r\n monospace;\r\n}\r\n\r\n/* Ant Design customizations */\r\n.ant-layout {\r\n min-height: 100vh;\r\n}\r\n\r\n.ant-table-thead > tr > th {\r\n background-color: #fafafa;\r\n font-weight: 600;\r\n}\r\n\r\n.ant-btn {\r\n border-radius: 6px;\r\n}\r\n\r\n.ant-card {\r\n border-radius: 8px;\r\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px rgba(0, 0, 0, 0.02);\r\n}\r\n\r\n.ant-modal {\r\n border-radius: 8px;\r\n}\r\n\r\n.ant-modal-header {\r\n border-radius: 8px 8px 0 0;\r\n}\r\n\r\n.ant-input, .ant-select-selector {\r\n border-radius: 6px;\r\n}\r\n\r\n.ant-form-item-label > label {\r\n font-weight: 500;\r\n}"],"names":[],"sourceRoot":""}
|