isa-model 0.3.9__py3-none-any.whl → 0.4.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.
- isa_model/__init__.py +1 -1
- isa_model/client.py +732 -565
- isa_model/core/cache/redis_cache.py +401 -0
- isa_model/core/config/config_manager.py +53 -10
- isa_model/core/config.py +1 -1
- isa_model/core/database/__init__.py +1 -0
- isa_model/core/database/migrations.py +277 -0
- isa_model/core/database/supabase_client.py +123 -0
- isa_model/core/models/__init__.py +37 -0
- isa_model/core/models/model_billing_tracker.py +60 -88
- isa_model/core/models/model_manager.py +36 -18
- isa_model/core/models/model_repo.py +44 -38
- isa_model/core/models/model_statistics_tracker.py +234 -0
- isa_model/core/models/model_storage.py +0 -1
- isa_model/core/models/model_version_manager.py +959 -0
- isa_model/core/pricing_manager.py +2 -249
- isa_model/core/resilience/circuit_breaker.py +366 -0
- isa_model/core/security/secrets.py +358 -0
- isa_model/core/services/__init__.py +2 -4
- isa_model/core/services/intelligent_model_selector.py +101 -370
- isa_model/core/storage/hf_storage.py +1 -1
- isa_model/core/types.py +7 -0
- isa_model/deployment/cloud/modal/isa_audio_chatTTS_service.py +520 -0
- isa_model/deployment/cloud/modal/isa_audio_fish_service.py +0 -0
- isa_model/deployment/cloud/modal/isa_audio_openvoice_service.py +758 -0
- isa_model/deployment/cloud/modal/isa_audio_service_v2.py +1044 -0
- isa_model/deployment/cloud/modal/isa_embed_rerank_service.py +296 -0
- isa_model/deployment/cloud/modal/isa_video_hunyuan_service.py +423 -0
- isa_model/deployment/cloud/modal/isa_vision_ocr_service.py +519 -0
- isa_model/deployment/cloud/modal/isa_vision_qwen25_service.py +709 -0
- isa_model/deployment/cloud/modal/isa_vision_table_service.py +467 -323
- isa_model/deployment/cloud/modal/isa_vision_ui_service.py +607 -180
- isa_model/deployment/cloud/modal/isa_vision_ui_service_optimized.py +660 -0
- isa_model/deployment/core/deployment_manager.py +6 -4
- isa_model/deployment/services/auto_hf_modal_deployer.py +894 -0
- isa_model/eval/benchmarks/__init__.py +27 -0
- isa_model/eval/benchmarks/multimodal_datasets.py +460 -0
- isa_model/eval/benchmarks.py +244 -12
- isa_model/eval/evaluators/__init__.py +8 -2
- isa_model/eval/evaluators/audio_evaluator.py +727 -0
- isa_model/eval/evaluators/embedding_evaluator.py +742 -0
- isa_model/eval/evaluators/vision_evaluator.py +564 -0
- isa_model/eval/example_evaluation.py +395 -0
- isa_model/eval/factory.py +272 -5
- isa_model/eval/isa_benchmarks.py +700 -0
- isa_model/eval/isa_integration.py +582 -0
- isa_model/eval/metrics.py +159 -6
- isa_model/eval/tests/unit/test_basic.py +396 -0
- isa_model/inference/ai_factory.py +44 -8
- isa_model/inference/services/audio/__init__.py +21 -0
- isa_model/inference/services/audio/base_realtime_service.py +225 -0
- isa_model/inference/services/audio/isa_tts_service.py +0 -0
- isa_model/inference/services/audio/openai_realtime_service.py +320 -124
- isa_model/inference/services/audio/openai_stt_service.py +32 -6
- isa_model/inference/services/base_service.py +17 -1
- isa_model/inference/services/embedding/__init__.py +13 -0
- isa_model/inference/services/embedding/base_embed_service.py +111 -8
- isa_model/inference/services/embedding/isa_embed_service.py +305 -0
- isa_model/inference/services/embedding/openai_embed_service.py +2 -4
- isa_model/inference/services/embedding/tests/test_embedding.py +222 -0
- isa_model/inference/services/img/__init__.py +2 -2
- isa_model/inference/services/img/base_image_gen_service.py +24 -7
- isa_model/inference/services/img/replicate_image_gen_service.py +84 -422
- isa_model/inference/services/img/services/replicate_face_swap.py +193 -0
- isa_model/inference/services/img/services/replicate_flux.py +226 -0
- isa_model/inference/services/img/services/replicate_flux_kontext.py +219 -0
- isa_model/inference/services/img/services/replicate_sticker_maker.py +249 -0
- isa_model/inference/services/img/tests/test_img_client.py +297 -0
- isa_model/inference/services/llm/base_llm_service.py +30 -6
- isa_model/inference/services/llm/helpers/llm_adapter.py +63 -9
- isa_model/inference/services/llm/ollama_llm_service.py +2 -1
- isa_model/inference/services/llm/openai_llm_service.py +652 -55
- isa_model/inference/services/llm/yyds_llm_service.py +2 -1
- isa_model/inference/services/vision/__init__.py +5 -5
- isa_model/inference/services/vision/base_vision_service.py +118 -185
- isa_model/inference/services/vision/helpers/image_utils.py +11 -5
- isa_model/inference/services/vision/isa_vision_service.py +573 -0
- isa_model/inference/services/vision/tests/test_ocr_client.py +284 -0
- isa_model/serving/api/fastapi_server.py +88 -16
- isa_model/serving/api/middleware/auth.py +311 -0
- isa_model/serving/api/middleware/security.py +278 -0
- isa_model/serving/api/routes/analytics.py +486 -0
- isa_model/serving/api/routes/deployments.py +339 -0
- isa_model/serving/api/routes/evaluations.py +579 -0
- isa_model/serving/api/routes/logs.py +430 -0
- isa_model/serving/api/routes/settings.py +582 -0
- isa_model/serving/api/routes/unified.py +324 -165
- isa_model/serving/api/startup.py +304 -0
- isa_model/serving/modal_proxy_server.py +249 -0
- isa_model/training/__init__.py +100 -6
- isa_model/training/core/__init__.py +4 -1
- isa_model/training/examples/intelligent_training_example.py +281 -0
- isa_model/training/intelligent/__init__.py +25 -0
- isa_model/training/intelligent/decision_engine.py +643 -0
- isa_model/training/intelligent/intelligent_factory.py +888 -0
- isa_model/training/intelligent/knowledge_base.py +751 -0
- isa_model/training/intelligent/resource_optimizer.py +839 -0
- isa_model/training/intelligent/task_classifier.py +576 -0
- isa_model/training/storage/__init__.py +24 -0
- isa_model/training/storage/core_integration.py +439 -0
- isa_model/training/storage/training_repository.py +552 -0
- isa_model/training/storage/training_storage.py +628 -0
- {isa_model-0.3.9.dist-info → isa_model-0.4.0.dist-info}/METADATA +13 -1
- isa_model-0.4.0.dist-info/RECORD +182 -0
- isa_model/deployment/cloud/modal/isa_vision_doc_service.py +0 -766
- isa_model/deployment/cloud/modal/register_models.py +0 -321
- isa_model/inference/adapter/unified_api.py +0 -248
- isa_model/inference/services/helpers/stacked_config.py +0 -148
- isa_model/inference/services/img/flux_professional_service.py +0 -603
- isa_model/inference/services/img/helpers/base_stacked_service.py +0 -274
- isa_model/inference/services/others/table_transformer_service.py +0 -61
- isa_model/inference/services/vision/doc_analysis_service.py +0 -640
- isa_model/inference/services/vision/helpers/base_stacked_service.py +0 -274
- isa_model/inference/services/vision/ui_analysis_service.py +0 -823
- isa_model/scripts/inference_tracker.py +0 -283
- isa_model/scripts/mlflow_manager.py +0 -379
- isa_model/scripts/model_registry.py +0 -465
- isa_model/scripts/register_models.py +0 -370
- isa_model/scripts/register_models_with_embeddings.py +0 -510
- isa_model/scripts/start_mlflow.py +0 -95
- isa_model/scripts/training_tracker.py +0 -257
- isa_model-0.3.9.dist-info/RECORD +0 -138
- {isa_model-0.3.9.dist-info → isa_model-0.4.0.dist-info}/WHEEL +0 -0
- {isa_model-0.3.9.dist-info → isa_model-0.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,582 @@
|
|
1
|
+
"""
|
2
|
+
Settings API Routes
|
3
|
+
|
4
|
+
Provides API key management and platform configuration endpoints.
|
5
|
+
This module handles sensitive operations safely without affecting running services.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from fastapi import APIRouter, HTTPException, Depends
|
9
|
+
from pydantic import BaseModel
|
10
|
+
from typing import Optional, List, Dict, Any
|
11
|
+
import logging
|
12
|
+
import os
|
13
|
+
import json
|
14
|
+
import hashlib
|
15
|
+
from pathlib import Path
|
16
|
+
from ..middleware.auth import api_key_manager, require_admin_access
|
17
|
+
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
router = APIRouter()
|
21
|
+
|
22
|
+
# Configuration file path
|
23
|
+
CONFIG_DIR = Path(os.path.dirname(__file__)).parent.parent.parent / "deployment" / "dev"
|
24
|
+
ENV_FILE = CONFIG_DIR / ".env"
|
25
|
+
CONFIG_BACKUP_FILE = CONFIG_DIR / ".env.backup"
|
26
|
+
|
27
|
+
class APIKeyEntry(BaseModel):
|
28
|
+
provider: str
|
29
|
+
key_name: str
|
30
|
+
masked_value: str
|
31
|
+
is_set: bool
|
32
|
+
last_updated: Optional[str] = None
|
33
|
+
|
34
|
+
class APIKeyUpdate(BaseModel):
|
35
|
+
provider: str
|
36
|
+
key_name: str
|
37
|
+
key_value: str
|
38
|
+
|
39
|
+
class GeneralSettings(BaseModel):
|
40
|
+
platform_name: Optional[str] = "ISA Model Platform"
|
41
|
+
default_provider: Optional[str] = "auto"
|
42
|
+
log_level: Optional[str] = "INFO"
|
43
|
+
max_workers: Optional[int] = 1
|
44
|
+
request_timeout: Optional[int] = 300
|
45
|
+
|
46
|
+
class PlatformAPIKey(BaseModel):
|
47
|
+
name: str
|
48
|
+
scopes: List[str] = ["read"]
|
49
|
+
|
50
|
+
class AuthSettings(BaseModel):
|
51
|
+
auth_enabled: bool
|
52
|
+
total_keys: int
|
53
|
+
active_keys: int
|
54
|
+
|
55
|
+
# Known API key mappings
|
56
|
+
API_KEY_PROVIDERS = {
|
57
|
+
"openai": {
|
58
|
+
"OPENAI_API_KEY": "OpenAI API Key",
|
59
|
+
"OPENAI_API_BASE": "OpenAI API Base URL"
|
60
|
+
},
|
61
|
+
"replicate": {
|
62
|
+
"REPLICATE_API_TOKEN": "Replicate API Token"
|
63
|
+
},
|
64
|
+
"yyds": {
|
65
|
+
"YYDS_API_KEY": "YYDS API Key",
|
66
|
+
"YYDS_API_BASE": "YYDS API Base URL"
|
67
|
+
},
|
68
|
+
"huggingface": {
|
69
|
+
"HF_TOKEN": "Hugging Face Token"
|
70
|
+
},
|
71
|
+
"runpod": {
|
72
|
+
"RUNPOD_API_KEY": "RunPod API Key"
|
73
|
+
},
|
74
|
+
"pypi": {
|
75
|
+
"PYPI_API_TOKEN": "PyPI API Token"
|
76
|
+
}
|
77
|
+
}
|
78
|
+
|
79
|
+
def mask_api_key(api_key: str) -> str:
|
80
|
+
"""Mask API key for display, showing only first 4 and last 4 characters"""
|
81
|
+
if not api_key or len(api_key) < 8:
|
82
|
+
return "••••••••"
|
83
|
+
return f"{api_key[:4]}{'•' * (len(api_key) - 8)}{api_key[-4:]}"
|
84
|
+
|
85
|
+
def read_env_file() -> Dict[str, str]:
|
86
|
+
"""Read environment variables from .env file"""
|
87
|
+
env_vars = {}
|
88
|
+
|
89
|
+
if not ENV_FILE.exists():
|
90
|
+
logger.warning(f"Environment file not found: {ENV_FILE}")
|
91
|
+
return env_vars
|
92
|
+
|
93
|
+
try:
|
94
|
+
with open(ENV_FILE, 'r') as f:
|
95
|
+
for line in f:
|
96
|
+
line = line.strip()
|
97
|
+
if line and not line.startswith('#') and '=' in line:
|
98
|
+
key, value = line.split('=', 1)
|
99
|
+
# Remove quotes if present
|
100
|
+
value = value.strip('"\'')
|
101
|
+
env_vars[key] = value
|
102
|
+
except Exception as e:
|
103
|
+
logger.error(f"Error reading environment file: {e}")
|
104
|
+
|
105
|
+
return env_vars
|
106
|
+
|
107
|
+
def write_env_file(env_vars: Dict[str, str]) -> bool:
|
108
|
+
"""Write environment variables to .env file with backup"""
|
109
|
+
try:
|
110
|
+
# Create backup
|
111
|
+
if ENV_FILE.exists():
|
112
|
+
import shutil
|
113
|
+
shutil.copy2(ENV_FILE, CONFIG_BACKUP_FILE)
|
114
|
+
logger.info("Created backup of environment file")
|
115
|
+
|
116
|
+
# Write new file
|
117
|
+
with open(ENV_FILE, 'w') as f:
|
118
|
+
f.write("# ISA Model Local Development Environment\n")
|
119
|
+
f.write("# Copy this to .env for local development\n\n")
|
120
|
+
|
121
|
+
# Organize by sections
|
122
|
+
sections = {
|
123
|
+
"Environment": ["ENVIRONMENT", "DEBUG", "LOG_LEVEL", "VERBOSE_LOGGING"],
|
124
|
+
"Server Configuration": ["PORT", "MAX_WORKERS", "REQUEST_TIMEOUT", "MAX_REQUEST_SIZE"],
|
125
|
+
"API Keys": [k for provider in API_KEY_PROVIDERS.values() for k in provider.keys()],
|
126
|
+
"Database Configuration": ["SUPABASE_LOCAL_URL", "SUPABASE_LOCAL_ANON_KEY", "SUPABASE_LOCAL_SERVICE_ROLE_KEY", "SUPABASE_PWD", "DATABASE_URL"],
|
127
|
+
"Local Services": ["OLLAMA_BASE_URL"],
|
128
|
+
"Model Defaults": [k for k in env_vars.keys() if k.startswith("DEFAULT_")],
|
129
|
+
"Development Configuration": ["RATE_LIMIT_REQUESTS_PER_MINUTE", "CORS_ORIGINS", "ENABLE_METRICS", "METRICS_PORT"],
|
130
|
+
"Storage Configuration": ["MODEL_STORAGE_PATH"]
|
131
|
+
}
|
132
|
+
|
133
|
+
for section, keys in sections.items():
|
134
|
+
section_vars = {k: v for k, v in env_vars.items() if k in keys}
|
135
|
+
if section_vars:
|
136
|
+
f.write(f"\n# ============= {section} =============\n")
|
137
|
+
for key, value in section_vars.items():
|
138
|
+
f.write(f"{key}={value}\n")
|
139
|
+
|
140
|
+
# Add any remaining variables
|
141
|
+
written_keys = set()
|
142
|
+
for keys in sections.values():
|
143
|
+
written_keys.update(keys)
|
144
|
+
|
145
|
+
remaining = {k: v for k, v in env_vars.items() if k not in written_keys}
|
146
|
+
if remaining:
|
147
|
+
f.write(f"\n# ============= Other =============\n")
|
148
|
+
for key, value in remaining.items():
|
149
|
+
f.write(f"{key}={value}\n")
|
150
|
+
|
151
|
+
logger.info("Successfully updated environment file")
|
152
|
+
return True
|
153
|
+
|
154
|
+
except Exception as e:
|
155
|
+
logger.error(f"Error writing environment file: {e}")
|
156
|
+
# Restore backup if write failed
|
157
|
+
if CONFIG_BACKUP_FILE.exists():
|
158
|
+
import shutil
|
159
|
+
shutil.copy2(CONFIG_BACKUP_FILE, ENV_FILE)
|
160
|
+
logger.info("Restored backup due to write failure")
|
161
|
+
return False
|
162
|
+
|
163
|
+
@router.get("/api-keys")
|
164
|
+
async def get_api_keys():
|
165
|
+
"""Get current API key configuration (masked for security)"""
|
166
|
+
try:
|
167
|
+
env_vars = read_env_file()
|
168
|
+
api_keys = []
|
169
|
+
|
170
|
+
for provider, keys in API_KEY_PROVIDERS.items():
|
171
|
+
for env_key, display_name in keys.items():
|
172
|
+
current_value = env_vars.get(env_key, "")
|
173
|
+
api_keys.append(APIKeyEntry(
|
174
|
+
provider=provider,
|
175
|
+
key_name=env_key,
|
176
|
+
masked_value=mask_api_key(current_value) if current_value else "",
|
177
|
+
is_set=bool(current_value)
|
178
|
+
))
|
179
|
+
|
180
|
+
return {
|
181
|
+
"api_keys": api_keys,
|
182
|
+
"total_keys": len(api_keys),
|
183
|
+
"configured_keys": sum(1 for key in api_keys if key.is_set)
|
184
|
+
}
|
185
|
+
|
186
|
+
except Exception as e:
|
187
|
+
logger.error(f"Error getting API keys: {e}")
|
188
|
+
raise HTTPException(status_code=500, detail="Failed to retrieve API key configuration")
|
189
|
+
|
190
|
+
@router.put("/api-keys")
|
191
|
+
async def update_api_key(api_key_update: APIKeyUpdate):
|
192
|
+
"""Update or add an API key"""
|
193
|
+
try:
|
194
|
+
# Validate provider and key name
|
195
|
+
if api_key_update.provider not in API_KEY_PROVIDERS:
|
196
|
+
raise HTTPException(status_code=400, detail="Invalid provider")
|
197
|
+
|
198
|
+
provider_keys = API_KEY_PROVIDERS[api_key_update.provider]
|
199
|
+
if api_key_update.key_name not in provider_keys:
|
200
|
+
raise HTTPException(status_code=400, detail="Invalid key name for provider")
|
201
|
+
|
202
|
+
# Read current environment
|
203
|
+
env_vars = read_env_file()
|
204
|
+
|
205
|
+
# Update the specific key
|
206
|
+
env_vars[api_key_update.key_name] = api_key_update.key_value
|
207
|
+
|
208
|
+
# Write back to file
|
209
|
+
if not write_env_file(env_vars):
|
210
|
+
raise HTTPException(status_code=500, detail="Failed to update configuration file")
|
211
|
+
|
212
|
+
return {
|
213
|
+
"success": True,
|
214
|
+
"message": f"Successfully updated {api_key_update.key_name}",
|
215
|
+
"restart_required": True # Note: Changes require restart to take effect
|
216
|
+
}
|
217
|
+
|
218
|
+
except HTTPException:
|
219
|
+
raise
|
220
|
+
except Exception as e:
|
221
|
+
logger.error(f"Error updating API key: {e}")
|
222
|
+
raise HTTPException(status_code=500, detail="Failed to update API key")
|
223
|
+
|
224
|
+
@router.delete("/api-keys/{provider}/{key_name}")
|
225
|
+
async def delete_api_key(provider: str, key_name: str):
|
226
|
+
"""Remove an API key"""
|
227
|
+
try:
|
228
|
+
# Validate provider and key name
|
229
|
+
if provider not in API_KEY_PROVIDERS:
|
230
|
+
raise HTTPException(status_code=400, detail="Invalid provider")
|
231
|
+
|
232
|
+
provider_keys = API_KEY_PROVIDERS[provider]
|
233
|
+
if key_name not in provider_keys:
|
234
|
+
raise HTTPException(status_code=400, detail="Invalid key name for provider")
|
235
|
+
|
236
|
+
# Read current environment
|
237
|
+
env_vars = read_env_file()
|
238
|
+
|
239
|
+
# Remove the key
|
240
|
+
if key_name in env_vars:
|
241
|
+
del env_vars[key_name]
|
242
|
+
|
243
|
+
# Write back to file
|
244
|
+
if not write_env_file(env_vars):
|
245
|
+
raise HTTPException(status_code=500, detail="Failed to update configuration file")
|
246
|
+
|
247
|
+
return {
|
248
|
+
"success": True,
|
249
|
+
"message": f"Successfully removed {key_name}",
|
250
|
+
"restart_required": True
|
251
|
+
}
|
252
|
+
|
253
|
+
except HTTPException:
|
254
|
+
raise
|
255
|
+
except Exception as e:
|
256
|
+
logger.error(f"Error deleting API key: {e}")
|
257
|
+
raise HTTPException(status_code=500, detail="Failed to delete API key")
|
258
|
+
|
259
|
+
@router.get("/general")
|
260
|
+
async def get_general_settings():
|
261
|
+
"""Get general platform settings"""
|
262
|
+
try:
|
263
|
+
env_vars = read_env_file()
|
264
|
+
|
265
|
+
settings = GeneralSettings(
|
266
|
+
platform_name=env_vars.get("PLATFORM_NAME", "ISA Model Platform"),
|
267
|
+
default_provider=env_vars.get("DEFAULT_LLM_PROVIDER", "auto"),
|
268
|
+
log_level=env_vars.get("LOG_LEVEL", "INFO"),
|
269
|
+
max_workers=int(env_vars.get("MAX_WORKERS", "1")),
|
270
|
+
request_timeout=int(env_vars.get("REQUEST_TIMEOUT", "300"))
|
271
|
+
)
|
272
|
+
|
273
|
+
return settings
|
274
|
+
|
275
|
+
except Exception as e:
|
276
|
+
logger.error(f"Error getting general settings: {e}")
|
277
|
+
raise HTTPException(status_code=500, detail="Failed to retrieve general settings")
|
278
|
+
|
279
|
+
@router.put("/general")
|
280
|
+
async def update_general_settings(settings: GeneralSettings):
|
281
|
+
"""Update general platform settings"""
|
282
|
+
try:
|
283
|
+
env_vars = read_env_file()
|
284
|
+
|
285
|
+
# Update settings
|
286
|
+
if settings.platform_name:
|
287
|
+
env_vars["PLATFORM_NAME"] = settings.platform_name
|
288
|
+
if settings.default_provider:
|
289
|
+
env_vars["DEFAULT_LLM_PROVIDER"] = settings.default_provider
|
290
|
+
if settings.log_level:
|
291
|
+
env_vars["LOG_LEVEL"] = settings.log_level
|
292
|
+
if settings.max_workers:
|
293
|
+
env_vars["MAX_WORKERS"] = str(settings.max_workers)
|
294
|
+
if settings.request_timeout:
|
295
|
+
env_vars["REQUEST_TIMEOUT"] = str(settings.request_timeout)
|
296
|
+
|
297
|
+
# Write back to file
|
298
|
+
if not write_env_file(env_vars):
|
299
|
+
raise HTTPException(status_code=500, detail="Failed to update configuration file")
|
300
|
+
|
301
|
+
return {
|
302
|
+
"success": True,
|
303
|
+
"message": "Successfully updated general settings",
|
304
|
+
"restart_required": True
|
305
|
+
}
|
306
|
+
|
307
|
+
except HTTPException:
|
308
|
+
raise
|
309
|
+
except Exception as e:
|
310
|
+
logger.error(f"Error updating general settings: {e}")
|
311
|
+
raise HTTPException(status_code=500, detail="Failed to update general settings")
|
312
|
+
|
313
|
+
@router.get("/backup")
|
314
|
+
async def get_config_backup():
|
315
|
+
"""Get information about configuration backups"""
|
316
|
+
try:
|
317
|
+
backups = []
|
318
|
+
|
319
|
+
if CONFIG_BACKUP_FILE.exists():
|
320
|
+
stat = CONFIG_BACKUP_FILE.stat()
|
321
|
+
backups.append({
|
322
|
+
"filename": CONFIG_BACKUP_FILE.name,
|
323
|
+
"size": stat.st_size,
|
324
|
+
"created": stat.st_mtime,
|
325
|
+
"type": "automatic"
|
326
|
+
})
|
327
|
+
|
328
|
+
return {
|
329
|
+
"backups": backups,
|
330
|
+
"backup_location": str(CONFIG_DIR)
|
331
|
+
}
|
332
|
+
|
333
|
+
except Exception as e:
|
334
|
+
logger.error(f"Error getting backup info: {e}")
|
335
|
+
raise HTTPException(status_code=500, detail="Failed to retrieve backup information")
|
336
|
+
|
337
|
+
@router.post("/backup/restore")
|
338
|
+
async def restore_config_backup():
|
339
|
+
"""Restore configuration from backup"""
|
340
|
+
try:
|
341
|
+
if not CONFIG_BACKUP_FILE.exists():
|
342
|
+
raise HTTPException(status_code=404, detail="No backup file found")
|
343
|
+
|
344
|
+
import shutil
|
345
|
+
shutil.copy2(CONFIG_BACKUP_FILE, ENV_FILE)
|
346
|
+
|
347
|
+
return {
|
348
|
+
"success": True,
|
349
|
+
"message": "Configuration restored from backup",
|
350
|
+
"restart_required": True
|
351
|
+
}
|
352
|
+
|
353
|
+
except HTTPException:
|
354
|
+
raise
|
355
|
+
except Exception as e:
|
356
|
+
logger.error(f"Error restoring backup: {e}")
|
357
|
+
raise HTTPException(status_code=500, detail="Failed to restore backup")
|
358
|
+
|
359
|
+
@router.get("/health")
|
360
|
+
async def settings_health():
|
361
|
+
"""Health check for settings service"""
|
362
|
+
try:
|
363
|
+
env_exists = ENV_FILE.exists()
|
364
|
+
backup_exists = CONFIG_BACKUP_FILE.exists()
|
365
|
+
|
366
|
+
# Test read access
|
367
|
+
env_vars = read_env_file() if env_exists else {}
|
368
|
+
|
369
|
+
return {
|
370
|
+
"status": "healthy",
|
371
|
+
"service": "settings",
|
372
|
+
"config_file_exists": env_exists,
|
373
|
+
"backup_exists": backup_exists,
|
374
|
+
"config_vars_count": len(env_vars),
|
375
|
+
"writable": os.access(CONFIG_DIR, os.W_OK) if CONFIG_DIR.exists() else False
|
376
|
+
}
|
377
|
+
|
378
|
+
except Exception as e:
|
379
|
+
logger.error(f"Settings health check failed: {e}")
|
380
|
+
return {
|
381
|
+
"status": "unhealthy",
|
382
|
+
"service": "settings",
|
383
|
+
"error": str(e)
|
384
|
+
}
|
385
|
+
|
386
|
+
# =================== PLATFORM API KEY MANAGEMENT ===================
|
387
|
+
|
388
|
+
@router.get("/auth/status")
|
389
|
+
async def get_auth_status():
|
390
|
+
"""Get current authentication status"""
|
391
|
+
try:
|
392
|
+
platform_keys = api_key_manager.list_api_keys()
|
393
|
+
|
394
|
+
return AuthSettings(
|
395
|
+
auth_enabled=api_key_manager.auth_enabled,
|
396
|
+
total_keys=len(platform_keys),
|
397
|
+
active_keys=sum(1 for key in platform_keys if key.get("active", True))
|
398
|
+
)
|
399
|
+
|
400
|
+
except Exception as e:
|
401
|
+
logger.error(f"Error getting auth status: {e}")
|
402
|
+
raise HTTPException(status_code=500, detail="Failed to get authentication status")
|
403
|
+
|
404
|
+
@router.post("/auth/bootstrap")
|
405
|
+
async def bootstrap_authentication():
|
406
|
+
"""Bootstrap authentication by creating initial admin key (only works when auth is disabled)"""
|
407
|
+
try:
|
408
|
+
if api_key_manager.auth_enabled:
|
409
|
+
raise HTTPException(status_code=400, detail="Authentication is already enabled")
|
410
|
+
|
411
|
+
# Enable auth and create default keys
|
412
|
+
default_keys = api_key_manager.enable_auth()
|
413
|
+
|
414
|
+
return {
|
415
|
+
"success": True,
|
416
|
+
"message": "Authentication bootstrapped successfully",
|
417
|
+
"keys_generated": default_keys,
|
418
|
+
"restart_required": False, # This takes effect immediately
|
419
|
+
"warning": "Save the generated API keys securely. They will not be shown again."
|
420
|
+
}
|
421
|
+
|
422
|
+
except Exception as e:
|
423
|
+
logger.error(f"Error bootstrapping authentication: {e}")
|
424
|
+
raise HTTPException(status_code=500, detail="Failed to bootstrap authentication")
|
425
|
+
|
426
|
+
@router.post("/auth/enable")
|
427
|
+
async def enable_authentication(current_user: Dict = Depends(require_admin_access)):
|
428
|
+
"""Enable API key authentication for the platform"""
|
429
|
+
try:
|
430
|
+
if api_key_manager.auth_enabled:
|
431
|
+
return {
|
432
|
+
"success": True,
|
433
|
+
"message": "Authentication is already enabled",
|
434
|
+
"keys_generated": None
|
435
|
+
}
|
436
|
+
|
437
|
+
# Enable auth and create default keys if needed
|
438
|
+
default_keys = api_key_manager.enable_auth()
|
439
|
+
|
440
|
+
return {
|
441
|
+
"success": True,
|
442
|
+
"message": "Authentication enabled successfully",
|
443
|
+
"keys_generated": default_keys,
|
444
|
+
"restart_required": True,
|
445
|
+
"warning": "Save the generated API keys securely. They will not be shown again."
|
446
|
+
}
|
447
|
+
|
448
|
+
except Exception as e:
|
449
|
+
logger.error(f"Error enabling authentication: {e}")
|
450
|
+
raise HTTPException(status_code=500, detail="Failed to enable authentication")
|
451
|
+
|
452
|
+
@router.post("/auth/disable")
|
453
|
+
async def disable_authentication(current_user: Dict = Depends(require_admin_access)):
|
454
|
+
"""Disable API key authentication for the platform"""
|
455
|
+
try:
|
456
|
+
if not api_key_manager.auth_enabled:
|
457
|
+
return {
|
458
|
+
"success": True,
|
459
|
+
"message": "Authentication is already disabled"
|
460
|
+
}
|
461
|
+
|
462
|
+
api_key_manager.disable_auth()
|
463
|
+
|
464
|
+
return {
|
465
|
+
"success": True,
|
466
|
+
"message": "Authentication disabled successfully",
|
467
|
+
"restart_required": True,
|
468
|
+
"warning": "All endpoints are now publicly accessible"
|
469
|
+
}
|
470
|
+
|
471
|
+
except Exception as e:
|
472
|
+
logger.error(f"Error disabling authentication: {e}")
|
473
|
+
raise HTTPException(status_code=500, detail="Failed to disable authentication")
|
474
|
+
|
475
|
+
@router.get("/auth/platform-keys")
|
476
|
+
async def get_platform_api_keys(current_user: Dict = Depends(require_admin_access)):
|
477
|
+
"""Get list of platform API keys"""
|
478
|
+
try:
|
479
|
+
if not api_key_manager.auth_enabled:
|
480
|
+
return {
|
481
|
+
"auth_enabled": False,
|
482
|
+
"api_keys": [],
|
483
|
+
"message": "Authentication is disabled"
|
484
|
+
}
|
485
|
+
|
486
|
+
keys = api_key_manager.list_api_keys()
|
487
|
+
|
488
|
+
return {
|
489
|
+
"auth_enabled": True,
|
490
|
+
"api_keys": keys,
|
491
|
+
"total_keys": len(keys)
|
492
|
+
}
|
493
|
+
|
494
|
+
except Exception as e:
|
495
|
+
logger.error(f"Error getting platform API keys: {e}")
|
496
|
+
raise HTTPException(status_code=500, detail="Failed to get platform API keys")
|
497
|
+
|
498
|
+
@router.post("/auth/platform-keys")
|
499
|
+
async def create_platform_api_key(
|
500
|
+
key_request: PlatformAPIKey,
|
501
|
+
current_user: Dict = Depends(require_admin_access)
|
502
|
+
):
|
503
|
+
"""Create a new platform API key"""
|
504
|
+
try:
|
505
|
+
if not api_key_manager.auth_enabled:
|
506
|
+
raise HTTPException(status_code=400, detail="Authentication is disabled")
|
507
|
+
|
508
|
+
# Validate scopes
|
509
|
+
valid_scopes = ["read", "write", "admin"]
|
510
|
+
invalid_scopes = [scope for scope in key_request.scopes if scope not in valid_scopes]
|
511
|
+
if invalid_scopes:
|
512
|
+
raise HTTPException(
|
513
|
+
status_code=400,
|
514
|
+
detail=f"Invalid scopes: {invalid_scopes}. Valid scopes: {valid_scopes}"
|
515
|
+
)
|
516
|
+
|
517
|
+
# Generate new API key
|
518
|
+
new_key = api_key_manager.generate_api_key(key_request.name, key_request.scopes)
|
519
|
+
|
520
|
+
return {
|
521
|
+
"success": True,
|
522
|
+
"message": f"API key '{key_request.name}' created successfully",
|
523
|
+
"api_key": new_key,
|
524
|
+
"scopes": key_request.scopes,
|
525
|
+
"warning": "Save this API key securely. It will not be shown again."
|
526
|
+
}
|
527
|
+
|
528
|
+
except HTTPException:
|
529
|
+
raise
|
530
|
+
except Exception as e:
|
531
|
+
logger.error(f"Error creating platform API key: {e}")
|
532
|
+
raise HTTPException(status_code=500, detail="Failed to create platform API key")
|
533
|
+
|
534
|
+
@router.delete("/auth/platform-keys/{key_hash}")
|
535
|
+
async def revoke_platform_api_key(
|
536
|
+
key_hash: str,
|
537
|
+
current_user: Dict = Depends(require_admin_access)
|
538
|
+
):
|
539
|
+
"""Revoke a platform API key"""
|
540
|
+
try:
|
541
|
+
if not api_key_manager.auth_enabled:
|
542
|
+
raise HTTPException(status_code=400, detail="Authentication is disabled")
|
543
|
+
|
544
|
+
# Find the key by hash prefix
|
545
|
+
keys = api_key_manager.list_api_keys()
|
546
|
+
target_key = None
|
547
|
+
|
548
|
+
for key in keys:
|
549
|
+
if key["key_hash"].startswith(key_hash):
|
550
|
+
target_key = key
|
551
|
+
break
|
552
|
+
|
553
|
+
if not target_key:
|
554
|
+
raise HTTPException(status_code=404, detail="API key not found")
|
555
|
+
|
556
|
+
# Note: We need the actual key to revoke, but we don't store it.
|
557
|
+
# In a real implementation, you'd store key hashes and mark them as revoked.
|
558
|
+
# For now, we'll mark it as revoked in the key data directly.
|
559
|
+
|
560
|
+
# This is a simplified revocation - in production you'd want a proper key management system
|
561
|
+
full_hash = None
|
562
|
+
for hash_key, data in api_key_manager.api_keys.items():
|
563
|
+
if hash_key.startswith(key_hash):
|
564
|
+
full_hash = hash_key
|
565
|
+
break
|
566
|
+
|
567
|
+
if full_hash:
|
568
|
+
api_key_manager.api_keys[full_hash]["active"] = False
|
569
|
+
api_key_manager.save_api_keys()
|
570
|
+
|
571
|
+
return {
|
572
|
+
"success": True,
|
573
|
+
"message": f"API key '{target_key['name']}' revoked successfully"
|
574
|
+
}
|
575
|
+
else:
|
576
|
+
raise HTTPException(status_code=404, detail="API key not found")
|
577
|
+
|
578
|
+
except HTTPException:
|
579
|
+
raise
|
580
|
+
except Exception as e:
|
581
|
+
logger.error(f"Error revoking platform API key: {e}")
|
582
|
+
raise HTTPException(status_code=500, detail="Failed to revoke platform API key")
|