Spaces:
Building
Building
Update admin_routes.py
Browse files- admin_routes.py +470 -459
admin_routes.py
CHANGED
@@ -1,80 +1,36 @@
|
|
|
|
|
|
|
|
1 |
"""
|
2 |
-
|
3 |
-
~~~~~~~~~~~~~~~~~~~~~
|
4 |
-
Admin UI için gerekli tüm endpoint'ler
|
5 |
-
"""
|
6 |
-
import time
|
7 |
-
import threading
|
8 |
import os
|
9 |
-
import
|
10 |
import hashlib
|
11 |
-
import
|
12 |
-
import
|
13 |
-
import
|
14 |
-
from
|
15 |
-
from typing import Dict, List, Optional, Any
|
16 |
from pathlib import Path
|
17 |
-
|
|
|
|
|
|
|
|
|
18 |
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
19 |
from pydantic import BaseModel, Field
|
20 |
-
import jwt
|
21 |
-
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
|
22 |
-
from utils import log
|
23 |
-
from config_provider import ConfigProvider
|
24 |
-
|
25 |
-
# Activity log retention policy (keep last 30 days)
|
26 |
-
ACTIVITY_LOG_RETENTION_DAYS = 30
|
27 |
-
ACTIVITY_LOG_MAX_ENTRIES = 10000
|
28 |
|
29 |
-
|
30 |
-
def cleanup_activity_log():
|
31 |
-
"""Cleanup old activity log entries - runs in background thread"""
|
32 |
-
while True:
|
33 |
-
try:
|
34 |
-
config = load_config()
|
35 |
-
|
36 |
-
if "activity_log" in config:
|
37 |
-
# Calculate cutoff date
|
38 |
-
cutoff_date = datetime.utcnow() - timedelta(days=ACTIVITY_LOG_RETENTION_DAYS)
|
39 |
-
|
40 |
-
# Filter recent entries
|
41 |
-
original_count = len(config["activity_log"])
|
42 |
-
config["activity_log"] = [
|
43 |
-
log_entry for log_entry in config["activity_log"]
|
44 |
-
if datetime.fromisoformat(log_entry["timestamp"].replace("Z", "+00:00")) > cutoff_date
|
45 |
-
]
|
46 |
-
|
47 |
-
# Also limit by max entries
|
48 |
-
if len(config["activity_log"]) > ACTIVITY_LOG_MAX_ENTRIES:
|
49 |
-
config["activity_log"] = config["activity_log"][-ACTIVITY_LOG_MAX_ENTRIES:]
|
50 |
-
|
51 |
-
# Save if anything was removed
|
52 |
-
removed_count = original_count - len(config["activity_log"])
|
53 |
-
if removed_count > 0:
|
54 |
-
save_config(config)
|
55 |
-
log(f"🧹 Cleaned up {removed_count} old activity log entries")
|
56 |
-
|
57 |
-
except Exception as e:
|
58 |
-
log(f"❌ Activity log cleanup error: {e}")
|
59 |
-
|
60 |
-
# Run cleanup once per day
|
61 |
-
time.sleep(86400) # 24 hours
|
62 |
-
|
63 |
-
# Start cleanup task when module loads
|
64 |
-
def start_cleanup_task():
|
65 |
-
thread = threading.Thread(target=cleanup_activity_log, daemon=True)
|
66 |
-
thread.start()
|
67 |
|
68 |
-
# =====================
|
69 |
def get_jwt_config():
|
70 |
-
"""Get JWT configuration based on
|
71 |
-
|
72 |
|
73 |
-
if
|
74 |
-
# Cloud mode - use
|
75 |
jwt_secret = os.getenv("JWT_SECRET")
|
76 |
if not jwt_secret:
|
77 |
-
log("
|
78 |
jwt_secret = "flare-admin-secret-key-change-in-production" # Fallback
|
79 |
else:
|
80 |
# On-premise mode - use .env file
|
@@ -113,6 +69,8 @@ class EnvironmentUpdate(BaseModel):
|
|
113 |
class ProjectCreate(BaseModel):
|
114 |
name: str
|
115 |
caption: Optional[str] = ""
|
|
|
|
|
116 |
|
117 |
class ProjectUpdate(BaseModel):
|
118 |
caption: str
|
@@ -188,109 +146,112 @@ def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security))
|
|
188 |
raise HTTPException(status_code=401, detail="Invalid token")
|
189 |
|
190 |
def hash_password(password: str, salt: str = None) -> tuple[str, str]:
|
191 |
-
"""Hash password with bcrypt.
|
|
|
192 |
if salt is None:
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
else
|
197 |
-
# Convert string salt to bytes
|
198 |
-
salt_bytes = salt.encode('utf-8')
|
199 |
|
200 |
# Hash the password
|
201 |
-
hashed = bcrypt.hashpw(password.encode('utf-8'), salt_bytes)
|
202 |
|
203 |
-
return hashed, salt
|
204 |
|
205 |
-
def verify_password(password: str,
|
206 |
-
"""Verify password against hash
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
return bcrypt.checkpw(password.encode('utf-8'),
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
def load_config()
|
219 |
"""Load service_config.jsonc"""
|
220 |
config_path = Path("service_config.jsonc")
|
|
|
|
|
|
|
221 |
with open(config_path, 'r', encoding='utf-8') as f:
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
def
|
233 |
-
"""
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
239 |
if "activity_log" not in config:
|
240 |
config["activity_log"] = []
|
241 |
-
|
242 |
-
|
243 |
-
|
|
|
|
|
|
|
244 |
"timestamp": get_timestamp(),
|
245 |
-
"user":
|
246 |
"action": action,
|
247 |
"entity_type": entity_type,
|
248 |
"entity_id": entity_id,
|
249 |
"entity_name": entity_name,
|
250 |
"details": details
|
251 |
-
}
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
if len(config["activity_log"]) > 100:
|
257 |
-
config["activity_log"] = config["activity_log"][-100:]
|
258 |
|
259 |
# ===================== Auth Endpoints =====================
|
260 |
@router.post("/login", response_model=LoginResponse)
|
261 |
async def login(request: LoginRequest):
|
262 |
-
"""
|
263 |
config = load_config()
|
264 |
-
jwt_config = get_jwt_config()
|
265 |
-
|
266 |
-
# Find user
|
267 |
users = config.get("config", {}).get("users", [])
|
|
|
|
|
268 |
user = next((u for u in users if u["username"] == request.username), None)
|
269 |
-
|
270 |
if not user:
|
271 |
raise HTTPException(status_code=401, detail="Invalid credentials")
|
272 |
-
|
273 |
-
# Verify password
|
274 |
-
if user.get("salt"):
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
if hashlib.sha256(request.password.encode()).hexdigest() != user["password_hash"]:
|
281 |
-
raise HTTPException(status_code=401, detail="Invalid credentials")
|
282 |
-
|
283 |
-
# Create JWT token
|
284 |
-
expire = datetime.utcnow() + timedelta(hours=jwt_config["expiration_hours"])
|
285 |
payload = {
|
286 |
"sub": request.username,
|
287 |
-
"exp":
|
288 |
}
|
|
|
289 |
token = jwt.encode(payload, jwt_config["secret"], algorithm=jwt_config["algorithm"])
|
290 |
-
|
291 |
log(f"✅ User '{request.username}' logged in")
|
292 |
return LoginResponse(token=token, username=request.username)
|
293 |
-
|
294 |
@router.post("/change-password")
|
295 |
async def change_password(
|
296 |
request: ChangePasswordRequest,
|
@@ -298,74 +259,74 @@ async def change_password(
|
|
298 |
):
|
299 |
"""Change user password"""
|
300 |
config = load_config()
|
301 |
-
|
302 |
-
# Find user
|
303 |
users = config.get("config", {}).get("users", [])
|
304 |
-
user_index = next((i for i, u in enumerate(users) if u["username"] == username), None)
|
305 |
|
306 |
-
|
|
|
|
|
307 |
raise HTTPException(status_code=404, detail="User not found")
|
308 |
|
309 |
-
user = users[user_index]
|
310 |
-
|
311 |
# Verify current password
|
312 |
-
if not verify_password(request.current_password, user["password_hash"], user.get("salt"
|
313 |
raise HTTPException(status_code=401, detail="Current password is incorrect")
|
314 |
|
315 |
-
#
|
316 |
new_hash, new_salt = hash_password(request.new_password)
|
317 |
-
|
318 |
-
|
319 |
-
users[user_index]["password_hash"] = new_hash
|
320 |
-
users[user_index]["salt"] = new_salt
|
321 |
|
322 |
# Save config
|
323 |
save_config(config)
|
324 |
|
325 |
-
# Add activity log
|
326 |
-
add_activity_log(config, username, "CHANGE_PASSWORD", "user", username, username, "Password changed")
|
327 |
-
save_config(config)
|
328 |
-
|
329 |
log(f"✅ Password changed for user '{username}'")
|
330 |
-
return {"success": True
|
331 |
-
|
332 |
# ===================== Environment Endpoints =====================
|
333 |
@router.get("/environment")
|
334 |
async def get_environment(username: str = Depends(verify_token)):
|
335 |
"""Get environment configuration"""
|
336 |
config = load_config()
|
337 |
env_config = config.get("config", {})
|
338 |
-
|
339 |
return {
|
340 |
-
"work_mode": env_config.get("work_mode", "
|
341 |
"cloud_token": env_config.get("cloud_token", ""),
|
342 |
-
"spark_endpoint": env_config.get("spark_endpoint", "")
|
343 |
}
|
344 |
|
345 |
@router.put("/environment")
|
346 |
-
async def update_environment(
|
|
|
|
|
|
|
347 |
"""Update environment configuration"""
|
348 |
config = load_config()
|
349 |
-
|
350 |
# Update config
|
351 |
-
config["config"]["work_mode"] =
|
352 |
-
config["config"]["cloud_token"] =
|
353 |
-
config["config"]["spark_endpoint"] =
|
354 |
config["config"]["last_update_date"] = get_timestamp()
|
355 |
config["config"]["last_update_user"] = username
|
356 |
-
|
357 |
-
# Save
|
358 |
-
save_config(config)
|
359 |
-
|
360 |
# Add activity log
|
361 |
-
add_activity_log(config, username, "UPDATE_ENVIRONMENT", "config",
|
362 |
-
f"
|
|
|
|
|
363 |
save_config(config)
|
364 |
-
|
365 |
-
log(f"✅ Environment updated by {username}")
|
366 |
return {"success": True}
|
367 |
|
368 |
# ===================== Project Endpoints =====================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
369 |
@router.get("/projects")
|
370 |
async def list_projects(
|
371 |
include_deleted: bool = False,
|
@@ -374,18 +335,13 @@ async def list_projects(
|
|
374 |
"""List all projects"""
|
375 |
config = load_config()
|
376 |
projects = config.get("projects", [])
|
377 |
-
|
378 |
# Filter deleted if needed
|
379 |
if not include_deleted:
|
380 |
projects = [p for p in projects if not p.get("deleted", False)]
|
381 |
-
|
382 |
return projects
|
383 |
|
384 |
-
@router.get("/projects/names")
|
385 |
-
def list_enabled_projects():
|
386 |
-
cfg = ConfigProvider.get()
|
387 |
-
return [p.name for p in cfg.projects if p.enabled]
|
388 |
-
|
389 |
@router.get("/projects/{project_id}")
|
390 |
async def get_project(
|
391 |
project_id: int,
|
@@ -395,13 +351,13 @@ async def get_project(
|
|
395 |
try:
|
396 |
config = load_config()
|
397 |
projects = config.get("projects", [])
|
398 |
-
|
399 |
project = next((p for p in projects if p.get("id") == project_id), None)
|
400 |
if not project or project.get("deleted", False):
|
401 |
raise HTTPException(status_code=404, detail="Project not found")
|
402 |
-
|
403 |
return project
|
404 |
-
|
405 |
except HTTPException:
|
406 |
raise
|
407 |
except Exception as e:
|
@@ -415,11 +371,11 @@ async def create_project(
|
|
415 |
username: str = Depends(verify_token)
|
416 |
):
|
417 |
cfg = load_config()
|
418 |
-
|
419 |
-
# 1️⃣ Yeni proje ID
|
420 |
project_id = cfg["config"].get("project_id_counter", 0) + 1
|
421 |
cfg["config"]["project_id_counter"] = project_id
|
422 |
-
|
423 |
# 2️⃣ Proje gövdesi
|
424 |
new_project = {
|
425 |
"id": project_id,
|
@@ -433,10 +389,10 @@ async def create_project(
|
|
433 |
"created_by": username,
|
434 |
"last_update_date": datetime.utcnow().isoformat(),
|
435 |
"last_update_user": username,
|
436 |
-
|
437 |
# *** Versiyon sayaçları ***
|
438 |
"version_id_counter": 1,
|
439 |
-
|
440 |
# *** İlk versiyon (no = 1) ***
|
441 |
"versions": [{
|
442 |
"id": 1,
|
@@ -468,14 +424,14 @@ async def create_project(
|
|
468 |
"published_by": None
|
469 |
}]
|
470 |
}
|
471 |
-
|
472 |
cfg.setdefault("projects", []).append(new_project)
|
473 |
save_config(cfg)
|
474 |
-
|
475 |
add_activity_log(cfg, username, "CREATE_PROJECT",
|
476 |
"project", project_id, new_project["name"])
|
477 |
save_config(cfg)
|
478 |
-
|
479 |
return new_project # 201 CREATED
|
480 |
|
481 |
@router.put("/projects/{project_id}")
|
@@ -486,27 +442,27 @@ async def update_project(
|
|
486 |
):
|
487 |
"""Update project"""
|
488 |
config = load_config()
|
489 |
-
|
490 |
# Find project
|
491 |
project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
|
492 |
if not project:
|
493 |
raise HTTPException(status_code=404, detail="Project not found")
|
494 |
-
|
495 |
# Check race condition
|
496 |
if project.get("last_update_date") != update.last_update_date:
|
497 |
raise HTTPException(status_code=409, detail="Project was modified by another user")
|
498 |
-
|
499 |
# Update
|
500 |
project["caption"] = update.caption
|
501 |
project["last_update_date"] = get_timestamp()
|
502 |
project["last_update_user"] = username
|
503 |
-
|
504 |
# Add activity log
|
505 |
add_activity_log(config, username, "UPDATE_PROJECT", "project", project_id, project["name"])
|
506 |
-
|
507 |
# Save
|
508 |
save_config(config)
|
509 |
-
|
510 |
log(f"✅ Project '{project['name']}' updated by {username}")
|
511 |
return project
|
512 |
|
@@ -514,101 +470,165 @@ async def update_project(
|
|
514 |
async def delete_project(project_id: int, username: str = Depends(verify_token)):
|
515 |
"""Delete project (soft delete)"""
|
516 |
config = load_config()
|
517 |
-
|
518 |
# Find project
|
519 |
project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
|
520 |
if not project:
|
521 |
raise HTTPException(status_code=404, detail="Project not found")
|
522 |
-
|
523 |
# Soft delete
|
524 |
project["deleted"] = True
|
525 |
project["last_update_date"] = get_timestamp()
|
526 |
project["last_update_user"] = username
|
527 |
-
|
528 |
# Add activity log
|
529 |
add_activity_log(config, username, "DELETE_PROJECT", "project", project_id, project["name"])
|
530 |
-
|
531 |
# Save
|
532 |
save_config(config)
|
533 |
-
|
534 |
log(f"✅ Project '{project['name']}' deleted by {username}")
|
535 |
return {"success": True}
|
536 |
|
537 |
@router.patch("/projects/{project_id}/toggle")
|
538 |
async def toggle_project(project_id: int, username: str = Depends(verify_token)):
|
539 |
-
"""
|
540 |
config = load_config()
|
541 |
-
|
542 |
# Find project
|
543 |
project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
|
544 |
if not project:
|
545 |
raise HTTPException(status_code=404, detail="Project not found")
|
546 |
-
|
547 |
# Toggle
|
548 |
-
project["enabled"] = not project.get("enabled",
|
549 |
project["last_update_date"] = get_timestamp()
|
550 |
project["last_update_user"] = username
|
551 |
-
|
552 |
# Add activity log
|
553 |
action = "ENABLE_PROJECT" if project["enabled"] else "DISABLE_PROJECT"
|
554 |
add_activity_log(config, username, action, "project", project_id, project["name"])
|
555 |
-
|
556 |
# Save
|
557 |
save_config(config)
|
558 |
-
|
559 |
log(f"✅ Project '{project['name']}' {'enabled' if project['enabled'] else 'disabled'} by {username}")
|
560 |
return {"enabled": project["enabled"]}
|
561 |
|
562 |
# ===================== Version Endpoints =====================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
563 |
@router.post("/projects/{project_id}/versions")
|
564 |
async def create_version(
|
565 |
project_id: int,
|
566 |
-
|
567 |
username: str = Depends(verify_token)
|
568 |
):
|
569 |
-
"""Create new version
|
570 |
config = load_config()
|
571 |
-
|
572 |
# Find project
|
573 |
project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
|
574 |
if not project:
|
575 |
raise HTTPException(status_code=404, detail="Project not found")
|
576 |
-
|
577 |
-
#
|
578 |
-
|
579 |
-
|
580 |
-
|
581 |
-
|
582 |
-
|
583 |
-
|
584 |
-
|
585 |
-
|
586 |
-
|
587 |
-
|
588 |
-
|
589 |
-
|
590 |
-
|
591 |
-
|
592 |
-
|
593 |
-
|
594 |
-
|
595 |
-
|
596 |
-
|
597 |
-
|
598 |
-
|
599 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
600 |
project["versions"].append(new_version)
|
|
|
|
|
601 |
project["last_update_date"] = get_timestamp()
|
602 |
project["last_update_user"] = username
|
603 |
-
|
604 |
# Add activity log
|
605 |
-
add_activity_log(config, username, "CREATE_VERSION", "version",
|
606 |
-
f"{project['name']} v{
|
607 |
-
|
608 |
# Save
|
609 |
save_config(config)
|
610 |
-
|
611 |
-
log(f"✅ Version {
|
612 |
return new_version
|
613 |
|
614 |
@router.put("/projects/{project_id}/versions/{version_id}")
|
@@ -616,51 +636,48 @@ async def update_version(
|
|
616 |
project_id: int,
|
617 |
version_id: int,
|
618 |
update: VersionUpdate,
|
619 |
-
force: bool = False, # Add force parameter
|
620 |
username: str = Depends(verify_token)
|
621 |
):
|
622 |
"""Update version"""
|
623 |
config = load_config()
|
624 |
-
|
625 |
# Find project and version
|
626 |
project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
|
627 |
if not project:
|
628 |
raise HTTPException(status_code=404, detail="Project not found")
|
629 |
-
|
630 |
version = next((v for v in project.get("versions", []) if v["id"] == version_id), None)
|
631 |
if not version:
|
632 |
raise HTTPException(status_code=404, detail="Version not found")
|
633 |
-
|
634 |
-
# Check
|
|
|
|
|
|
|
|
|
635 |
if version.get("published", False):
|
636 |
-
raise HTTPException(status_code=400, detail="Cannot
|
637 |
-
|
638 |
-
#
|
639 |
-
if not force and version.get("last_update_date") != update.last_update_date:
|
640 |
-
raise HTTPException(
|
641 |
-
status_code=409,
|
642 |
-
detail="Version was modified by another user. Please reload or force save."
|
643 |
-
)
|
644 |
-
|
645 |
-
# Update
|
646 |
version["caption"] = update.caption
|
647 |
version["general_prompt"] = update.general_prompt
|
648 |
version["llm"] = update.llm
|
649 |
version["intents"] = [intent.dict() for intent in update.intents]
|
650 |
version["last_update_date"] = get_timestamp()
|
651 |
version["last_update_user"] = username
|
652 |
-
|
|
|
653 |
project["last_update_date"] = get_timestamp()
|
654 |
project["last_update_user"] = username
|
655 |
-
|
656 |
# Add activity log
|
657 |
add_activity_log(config, username, "UPDATE_VERSION", "version", version_id,
|
658 |
-
f"{project['name']} v{
|
659 |
-
|
660 |
# Save
|
661 |
save_config(config)
|
662 |
-
|
663 |
-
log(f"✅ Version {
|
664 |
return version
|
665 |
|
666 |
@router.post("/projects/{project_id}/versions/{version_id}/publish")
|
@@ -671,40 +688,39 @@ async def publish_version(
|
|
671 |
):
|
672 |
"""Publish version"""
|
673 |
config = load_config()
|
674 |
-
|
675 |
# Find project and version
|
676 |
project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
|
677 |
if not project:
|
678 |
raise HTTPException(status_code=404, detail="Project not found")
|
679 |
-
|
680 |
version = next((v for v in project.get("versions", []) if v["id"] == version_id), None)
|
681 |
if not version:
|
682 |
raise HTTPException(status_code=404, detail="Version not found")
|
683 |
-
|
684 |
# Unpublish all other versions
|
685 |
for v in project.get("versions", []):
|
686 |
if v["id"] != version_id:
|
687 |
v["published"] = False
|
688 |
-
|
689 |
# Publish this version
|
690 |
version["published"] = True
|
691 |
version["publish_date"] = get_timestamp()
|
692 |
version["published_by"] = username
|
693 |
version["last_update_date"] = get_timestamp()
|
694 |
version["last_update_user"] = username
|
695 |
-
|
|
|
696 |
project["last_update_date"] = get_timestamp()
|
697 |
project["last_update_user"] = username
|
698 |
-
|
699 |
# Add activity log
|
700 |
add_activity_log(config, username, "PUBLISH_VERSION", "version", version_id,
|
701 |
-
f"{project['name']} v{
|
702 |
-
|
703 |
# Save
|
704 |
save_config(config)
|
705 |
-
|
706 |
-
# TODO: Notify Spark about new version
|
707 |
-
|
708 |
log(f"✅ Version {version_id} published for project '{project['name']}' by {username}")
|
709 |
return {"success": True}
|
710 |
|
@@ -716,35 +732,35 @@ async def delete_version(
|
|
716 |
):
|
717 |
"""Delete version (soft delete)"""
|
718 |
config = load_config()
|
719 |
-
|
720 |
# Find project and version
|
721 |
project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
|
722 |
if not project:
|
723 |
raise HTTPException(status_code=404, detail="Project not found")
|
724 |
-
|
725 |
version = next((v for v in project.get("versions", []) if v["id"] == version_id), None)
|
726 |
if not version:
|
727 |
raise HTTPException(status_code=404, detail="Version not found")
|
728 |
-
|
729 |
# Cannot delete published version
|
730 |
if version.get("published", False):
|
731 |
raise HTTPException(status_code=400, detail="Cannot delete published version")
|
732 |
-
|
733 |
# Soft delete
|
734 |
version["deleted"] = True
|
735 |
version["last_update_date"] = get_timestamp()
|
736 |
version["last_update_user"] = username
|
737 |
-
|
738 |
project["last_update_date"] = get_timestamp()
|
739 |
project["last_update_user"] = username
|
740 |
-
|
741 |
# Add activity log
|
742 |
add_activity_log(config, username, "DELETE_VERSION", "version", version_id,
|
743 |
f"{project['name']} v{version_id}")
|
744 |
-
|
745 |
# Save
|
746 |
save_config(config)
|
747 |
-
|
748 |
log(f"✅ Version {version_id} deleted for project '{project['name']}' by {username}")
|
749 |
return {"success": True}
|
750 |
|
@@ -757,23 +773,23 @@ async def list_apis(
|
|
757 |
"""List all APIs"""
|
758 |
config = load_config()
|
759 |
apis = config.get("apis", [])
|
760 |
-
|
761 |
# Filter deleted if needed
|
762 |
if not include_deleted:
|
763 |
apis = [a for a in apis if not a.get("deleted", False)]
|
764 |
-
|
765 |
return apis
|
766 |
|
767 |
@router.post("/apis")
|
768 |
async def create_api(api: APICreate, username: str = Depends(verify_token)):
|
769 |
"""Create new API"""
|
770 |
config = load_config()
|
771 |
-
|
772 |
# Check duplicate name
|
773 |
existing = [a for a in config.get("apis", []) if a["name"] == api.name]
|
774 |
if existing:
|
775 |
raise HTTPException(status_code=400, detail="API name already exists")
|
776 |
-
|
777 |
# Create API
|
778 |
new_api = api.dict()
|
779 |
new_api["deleted"] = False
|
@@ -781,17 +797,17 @@ async def create_api(api: APICreate, username: str = Depends(verify_token)):
|
|
781 |
new_api["created_by"] = username
|
782 |
new_api["last_update_date"] = get_timestamp()
|
783 |
new_api["last_update_user"] = username
|
784 |
-
|
785 |
if "apis" not in config:
|
786 |
config["apis"] = []
|
787 |
config["apis"].append(new_api)
|
788 |
-
|
789 |
# Add activity log
|
790 |
add_activity_log(config, username, "CREATE_API", "api", api.name, api.name)
|
791 |
-
|
792 |
# Save
|
793 |
save_config(config)
|
794 |
-
|
795 |
log(f"✅ API '{api.name}' created by {username}")
|
796 |
return new_api
|
797 |
|
@@ -803,16 +819,16 @@ async def update_api(
|
|
803 |
):
|
804 |
"""Update API"""
|
805 |
config = load_config()
|
806 |
-
|
807 |
# Find API
|
808 |
api = next((a for a in config.get("apis", []) if a["name"] == api_name), None)
|
809 |
if not api:
|
810 |
raise HTTPException(status_code=404, detail="API not found")
|
811 |
-
|
812 |
# Check race condition
|
813 |
if api.get("last_update_date") != update.last_update_date:
|
814 |
raise HTTPException(status_code=409, detail="API was modified by another user")
|
815 |
-
|
816 |
# Check if API is in use
|
817 |
for project in config.get("projects", []):
|
818 |
for version in project.get("versions", []):
|
@@ -820,20 +836,20 @@ async def update_api(
|
|
820 |
if intent.get("action") == api_name and version.get("published", False):
|
821 |
raise HTTPException(status_code=400,
|
822 |
detail=f"API is used in published version of project '{project['name']}'")
|
823 |
-
|
824 |
# Update
|
825 |
update_dict = update.dict()
|
826 |
del update_dict["last_update_date"]
|
827 |
api.update(update_dict)
|
828 |
api["last_update_date"] = get_timestamp()
|
829 |
api["last_update_user"] = username
|
830 |
-
|
831 |
# Add activity log
|
832 |
add_activity_log(config, username, "UPDATE_API", "api", api_name, api_name)
|
833 |
-
|
834 |
# Save
|
835 |
save_config(config)
|
836 |
-
|
837 |
log(f"✅ API '{api_name}' updated by {username}")
|
838 |
return api
|
839 |
|
@@ -841,12 +857,12 @@ async def update_api(
|
|
841 |
async def delete_api(api_name: str, username: str = Depends(verify_token)):
|
842 |
"""Delete API (soft delete)"""
|
843 |
config = load_config()
|
844 |
-
|
845 |
# Find API
|
846 |
api = next((a for a in config.get("apis", []) if a["name"] == api_name), None)
|
847 |
if not api:
|
848 |
raise HTTPException(status_code=404, detail="API not found")
|
849 |
-
|
850 |
# Check if API is in use
|
851 |
for project in config.get("projects", []):
|
852 |
for version in project.get("versions", []):
|
@@ -854,18 +870,18 @@ async def delete_api(api_name: str, username: str = Depends(verify_token)):
|
|
854 |
if intent.get("action") == api_name:
|
855 |
raise HTTPException(status_code=400,
|
856 |
detail=f"API is used in project '{project['name']}'")
|
857 |
-
|
858 |
# Soft delete
|
859 |
api["deleted"] = True
|
860 |
api["last_update_date"] = get_timestamp()
|
861 |
api["last_update_user"] = username
|
862 |
-
|
863 |
# Add activity log
|
864 |
add_activity_log(config, username, "DELETE_API", "api", api_name, api_name)
|
865 |
-
|
866 |
# Save
|
867 |
save_config(config)
|
868 |
-
|
869 |
log(f"✅ API '{api_name}' deleted by {username}")
|
870 |
return {"success": True}
|
871 |
|
@@ -874,7 +890,7 @@ async def delete_api(api_name: str, username: str = Depends(verify_token)):
|
|
874 |
async def test_api(api: APICreate, username: str = Depends(verify_token)):
|
875 |
"""Test API endpoint"""
|
876 |
import requests
|
877 |
-
|
878 |
try:
|
879 |
# Prepare request
|
880 |
headers = api.headers.copy()
|
@@ -882,7 +898,7 @@ async def test_api(api: APICreate, username: str = Depends(verify_token)):
|
|
882 |
# Add sample auth token for testing
|
883 |
if api.auth and api.auth.get("enabled"):
|
884 |
headers["Authorization"] = "Bearer test_token_12345"
|
885 |
-
|
886 |
# Make request
|
887 |
response = requests.request(
|
888 |
method=api.method,
|
@@ -893,192 +909,187 @@ async def test_api(api: APICreate, username: str = Depends(verify_token)):
|
|
893 |
timeout=api.timeout_seconds,
|
894 |
proxies={"http": api.proxy, "https": api.proxy} if api.proxy else None
|
895 |
)
|
896 |
-
|
897 |
return {
|
898 |
"success": True,
|
899 |
"status_code": response.status_code,
|
900 |
-
"
|
901 |
-
"headers": dict(response.headers)
|
902 |
-
"body": response.text[:1000], # First 1000 chars
|
903 |
-
"request_headers": headers, # Debug için
|
904 |
-
"request_body": api.body_template # Debug için
|
905 |
-
}
|
906 |
-
except requests.exceptions.Timeout:
|
907 |
-
return {
|
908 |
-
"success": False,
|
909 |
-
"error": f"Request timed out after {api.timeout_seconds} seconds"
|
910 |
}
|
911 |
except Exception as e:
|
912 |
return {
|
913 |
"success": False,
|
914 |
-
"error": str(e)
|
915 |
-
"error_type": type(e).__name__
|
916 |
}
|
917 |
|
918 |
-
@router.
|
919 |
-
async def
|
920 |
-
|
921 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
922 |
username: str = Depends(verify_token)
|
923 |
):
|
924 |
-
"""
|
925 |
config = load_config()
|
926 |
-
logs = config.get("activity_log", [])
|
927 |
|
928 |
-
#
|
929 |
-
|
|
|
930 |
|
931 |
-
#
|
932 |
-
|
933 |
-
|
934 |
-
|
|
|
935 |
|
936 |
-
# Get
|
937 |
-
|
|
|
938 |
|
939 |
-
|
940 |
-
|
941 |
-
"
|
942 |
-
"
|
943 |
-
"
|
944 |
-
"
|
945 |
-
|
946 |
-
|
947 |
-
|
948 |
-
|
949 |
-
|
950 |
-
|
951 |
-
|
952 |
-
|
953 |
-
|
954 |
-
results = {
|
955 |
-
"test_type": test.test_type,
|
956 |
-
"start_time": get_timestamp(),
|
957 |
-
"tests": [
|
958 |
-
{"name": "Login with valid credentials", "status": "PASS", "duration_ms": 120},
|
959 |
-
{"name": "Create new project", "status": "PASS", "duration_ms": 340},
|
960 |
-
{"name": "Delete API in use", "status": "PASS", "duration_ms": 45},
|
961 |
-
{"name": "Race condition detection", "status": "PASS", "duration_ms": 567},
|
962 |
-
{"name": "Invalid token handling", "status": "PASS", "duration_ms": 23}
|
963 |
-
],
|
964 |
-
"summary": {
|
965 |
-
"total": 5,
|
966 |
-
"passed": 5,
|
967 |
-
"failed": 0,
|
968 |
-
"duration_ms": 1095
|
969 |
-
}
|
970 |
}
|
971 |
-
|
972 |
-
|
973 |
-
|
974 |
-
|
975 |
-
|
976 |
-
|
977 |
-
|
978 |
-
|
979 |
-
|
980 |
-
|
981 |
-
|
982 |
-
|
983 |
-
"
|
984 |
-
"
|
985 |
-
|
986 |
-
|
987 |
-
|
988 |
-
"
|
989 |
-
"
|
|
|
|
|
990 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
991 |
|
992 |
-
# ===================== Export/Import =====================
|
993 |
@router.get("/projects/{project_id}/export")
|
994 |
-
async def export_project(
|
|
|
|
|
|
|
995 |
"""Export project as JSON"""
|
996 |
config = load_config()
|
997 |
-
|
998 |
# Find project
|
999 |
project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
|
1000 |
if not project:
|
1001 |
raise HTTPException(status_code=404, detail="Project not found")
|
1002 |
-
|
1003 |
-
#
|
1004 |
-
api_names = set()
|
1005 |
-
for version in project.get("versions", []):
|
1006 |
-
for intent in version.get("intents", []):
|
1007 |
-
api_names.add(intent.get("action"))
|
1008 |
-
|
1009 |
-
apis = [a for a in config.get("apis", []) if a["name"] in api_names]
|
1010 |
-
|
1011 |
-
# Create export
|
1012 |
export_data = {
|
1013 |
-
"
|
1014 |
-
"
|
1015 |
-
"
|
1016 |
-
"
|
|
|
1017 |
}
|
1018 |
-
|
1019 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1020 |
return export_data
|
1021 |
|
1022 |
-
|
1023 |
-
|
1024 |
-
|
|
|
|
|
|
|
|
|
1025 |
config = load_config()
|
|
|
|
|
|
|
|
|
1026 |
|
1027 |
-
|
1028 |
-
|
1029 |
-
|
1030 |
-
|
1031 |
-
|
1032 |
-
|
1033 |
-
|
1034 |
-
|
1035 |
-
|
1036 |
-
|
1037 |
-
|
1038 |
-
|
1039 |
-
|
1040 |
-
|
1041 |
-
|
1042 |
-
|
1043 |
-
for version in project_data.get("versions", []):
|
1044 |
-
version_counter += 1
|
1045 |
-
version["id"] = version_counter
|
1046 |
-
version["published"] = False # Imported versions are unpublished
|
1047 |
-
|
1048 |
-
project_data["version_id_counter"] = version_counter
|
1049 |
-
project_data["created_date"] = get_timestamp()
|
1050 |
-
project_data["created_by"] = username
|
1051 |
-
project_data["last_update_date"] = get_timestamp()
|
1052 |
-
project_data["last_update_user"] = username
|
1053 |
-
|
1054 |
-
# Import APIs
|
1055 |
-
imported_apis = []
|
1056 |
-
for api_data in apis_data:
|
1057 |
-
# Check if API already exists
|
1058 |
-
existing_api = next((a for a in config.get("apis", []) if a["name"] == api_data.get("name")), None)
|
1059 |
-
if not existing_api:
|
1060 |
-
api_data["created_date"] = get_timestamp()
|
1061 |
-
api_data["created_by"] = username
|
1062 |
-
api_data["last_update_date"] = get_timestamp()
|
1063 |
-
api_data["last_update_user"] = username
|
1064 |
-
api_data["deleted"] = False
|
1065 |
-
config["apis"].append(api_data)
|
1066 |
-
imported_apis.append(api_data["name"])
|
1067 |
-
|
1068 |
-
# Add project
|
1069 |
-
config["projects"].append(project_data)
|
1070 |
-
|
1071 |
-
# Add activity log
|
1072 |
-
add_activity_log(config, username, "IMPORT_PROJECT", "project", project_data["id"],
|
1073 |
-
project_data["name"], f"Imported with {len(imported_apis)} APIs")
|
1074 |
-
|
1075 |
-
# Save
|
1076 |
-
save_config(config)
|
1077 |
|
1078 |
-
|
1079 |
-
|
1080 |
-
|
1081 |
-
|
1082 |
-
|
1083 |
-
"imported_apis": imported_apis
|
1084 |
-
}
|
|
|
1 |
+
"""Admin API endpoints for Flare
|
2 |
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
3 |
+
Provides authentication, project, version, and API management endpoints.
|
4 |
"""
|
5 |
+
|
|
|
|
|
|
|
|
|
|
|
6 |
import os
|
7 |
+
import sys
|
8 |
import hashlib
|
9 |
+
import json
|
10 |
+
import jwt
|
11 |
+
from datetime import datetime, timedelta, timezone
|
12 |
+
from typing import Optional, List, Dict, Any
|
|
|
13 |
from pathlib import Path
|
14 |
+
import threading
|
15 |
+
import time
|
16 |
+
import bcrypt
|
17 |
+
|
18 |
+
from fastapi import APIRouter, HTTPException, Depends, Body, Query
|
19 |
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
20 |
from pydantic import BaseModel, Field
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
|
22 |
+
from utils import log
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
|
24 |
+
# ===================== JWT Config =====================
|
25 |
def get_jwt_config():
|
26 |
+
"""Get JWT configuration based on environment"""
|
27 |
+
work_mode = os.getenv("WORK_MODE", "on-premise")
|
28 |
|
29 |
+
if work_mode == "hfcloud":
|
30 |
+
# Cloud mode - use secrets from environment
|
31 |
jwt_secret = os.getenv("JWT_SECRET")
|
32 |
if not jwt_secret:
|
33 |
+
log("⚠️ WARNING: JWT_SECRET not found in environment, using fallback")
|
34 |
jwt_secret = "flare-admin-secret-key-change-in-production" # Fallback
|
35 |
else:
|
36 |
# On-premise mode - use .env file
|
|
|
69 |
class ProjectCreate(BaseModel):
|
70 |
name: str
|
71 |
caption: Optional[str] = ""
|
72 |
+
icon: Optional[str] = "folder"
|
73 |
+
description: Optional[str] = ""
|
74 |
|
75 |
class ProjectUpdate(BaseModel):
|
76 |
caption: str
|
|
|
146 |
raise HTTPException(status_code=401, detail="Invalid token")
|
147 |
|
148 |
def hash_password(password: str, salt: str = None) -> tuple[str, str]:
|
149 |
+
"""Hash password with bcrypt.
|
150 |
+
Returns (hashed_password, salt)"""
|
151 |
if salt is None:
|
152 |
+
salt = bcrypt.gensalt().decode('utf-8')
|
153 |
+
|
154 |
+
# Ensure salt is bytes
|
155 |
+
salt_bytes = salt.encode('utf-8') if isinstance(salt, str) else salt
|
|
|
|
|
156 |
|
157 |
# Hash the password
|
158 |
+
hashed = bcrypt.hashpw(password.encode('utf-8'), salt_bytes)
|
159 |
|
160 |
+
return hashed.decode('utf-8'), salt
|
161 |
|
162 |
+
def verify_password(password: str, hashed: str, salt: str = None) -> bool:
|
163 |
+
"""Verify password against hash"""
|
164 |
+
try:
|
165 |
+
# For bcrypt hashes (they contain salt)
|
166 |
+
if hashed.startswith('$2b$') or hashed.startswith('$2a$'):
|
167 |
+
return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8'))
|
168 |
+
|
169 |
+
# For legacy SHA256 hashes
|
170 |
+
return hashlib.sha256(password.encode()).hexdigest() == hashed
|
171 |
+
except Exception as e:
|
172 |
+
log(f"Password verification error: {e}")
|
173 |
+
return False
|
174 |
+
|
175 |
+
def load_config():
|
176 |
"""Load service_config.jsonc"""
|
177 |
config_path = Path("service_config.jsonc")
|
178 |
+
if not config_path.exists():
|
179 |
+
return {"config": {}, "projects": [], "apis": []}
|
180 |
+
|
181 |
with open(config_path, 'r', encoding='utf-8') as f:
|
182 |
+
content = f.read()
|
183 |
+
# Remove comments for JSON parsing
|
184 |
+
lines = []
|
185 |
+
for line in content.split('\n'):
|
186 |
+
stripped = line.strip()
|
187 |
+
if not stripped.startswith('//'):
|
188 |
+
lines.append(line)
|
189 |
+
clean_content = '\n'.join(lines)
|
190 |
+
return json.loads(clean_content)
|
191 |
+
|
192 |
+
def save_config(config: dict):
|
193 |
+
"""Save config back to service_config.jsonc"""
|
194 |
+
with open("service_config.jsonc", 'w', encoding='utf-8') as f:
|
195 |
+
json.dump(config, f, indent=2, ensure_ascii=False)
|
196 |
+
|
197 |
+
def get_timestamp():
|
198 |
+
"""Get current timestamp in ISO format with milliseconds"""
|
199 |
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
200 |
+
|
201 |
+
def add_activity_log(config: dict, username: str, action: str,
|
202 |
+
entity_type: str, entity_id: Any, entity_name: str,
|
203 |
+
details: str = ""):
|
204 |
+
"""Add activity log entry"""
|
205 |
if "activity_log" not in config:
|
206 |
config["activity_log"] = []
|
207 |
+
|
208 |
+
# Get next ID
|
209 |
+
log_id = max([log.get("id", 0) for log in config["activity_log"]], default=0) + 1
|
210 |
+
|
211 |
+
config["activity_log"].append({
|
212 |
+
"id": log_id,
|
213 |
"timestamp": get_timestamp(),
|
214 |
+
"user": username,
|
215 |
"action": action,
|
216 |
"entity_type": entity_type,
|
217 |
"entity_id": entity_id,
|
218 |
"entity_name": entity_name,
|
219 |
"details": details
|
220 |
+
})
|
221 |
+
|
222 |
+
# Keep only last 1000 entries
|
223 |
+
if len(config["activity_log"]) > 1000:
|
224 |
+
config["activity_log"] = config["activity_log"][-1000:]
|
|
|
|
|
225 |
|
226 |
# ===================== Auth Endpoints =====================
|
227 |
@router.post("/login", response_model=LoginResponse)
|
228 |
async def login(request: LoginRequest):
|
229 |
+
"""Authenticate user and return JWT token"""
|
230 |
config = load_config()
|
|
|
|
|
|
|
231 |
users = config.get("config", {}).get("users", [])
|
232 |
+
|
233 |
+
# Find user
|
234 |
user = next((u for u in users if u["username"] == request.username), None)
|
|
|
235 |
if not user:
|
236 |
raise HTTPException(status_code=401, detail="Invalid credentials")
|
237 |
+
|
238 |
+
# Verify password
|
239 |
+
if not verify_password(request.password, user["password_hash"], user.get("salt")):
|
240 |
+
raise HTTPException(status_code=401, detail="Invalid credentials")
|
241 |
+
|
242 |
+
# Generate JWT token
|
243 |
+
jwt_config = get_jwt_config()
|
244 |
+
|
|
|
|
|
|
|
|
|
|
|
245 |
payload = {
|
246 |
"sub": request.username,
|
247 |
+
"exp": datetime.now(timezone.utc) + timedelta(hours=jwt_config["expiration_hours"])
|
248 |
}
|
249 |
+
|
250 |
token = jwt.encode(payload, jwt_config["secret"], algorithm=jwt_config["algorithm"])
|
251 |
+
|
252 |
log(f"✅ User '{request.username}' logged in")
|
253 |
return LoginResponse(token=token, username=request.username)
|
254 |
+
|
255 |
@router.post("/change-password")
|
256 |
async def change_password(
|
257 |
request: ChangePasswordRequest,
|
|
|
259 |
):
|
260 |
"""Change user password"""
|
261 |
config = load_config()
|
|
|
|
|
262 |
users = config.get("config", {}).get("users", [])
|
|
|
263 |
|
264 |
+
# Find user
|
265 |
+
user = next((u for u in users if u["username"] == username), None)
|
266 |
+
if not user:
|
267 |
raise HTTPException(status_code=404, detail="User not found")
|
268 |
|
|
|
|
|
269 |
# Verify current password
|
270 |
+
if not verify_password(request.current_password, user["password_hash"], user.get("salt")):
|
271 |
raise HTTPException(status_code=401, detail="Current password is incorrect")
|
272 |
|
273 |
+
# Hash new password
|
274 |
new_hash, new_salt = hash_password(request.new_password)
|
275 |
+
user["password_hash"] = new_hash
|
276 |
+
user["salt"] = new_salt
|
|
|
|
|
277 |
|
278 |
# Save config
|
279 |
save_config(config)
|
280 |
|
|
|
|
|
|
|
|
|
281 |
log(f"✅ Password changed for user '{username}'")
|
282 |
+
return {"success": True}
|
283 |
+
|
284 |
# ===================== Environment Endpoints =====================
|
285 |
@router.get("/environment")
|
286 |
async def get_environment(username: str = Depends(verify_token)):
|
287 |
"""Get environment configuration"""
|
288 |
config = load_config()
|
289 |
env_config = config.get("config", {})
|
290 |
+
|
291 |
return {
|
292 |
+
"work_mode": env_config.get("work_mode", "on-premise"),
|
293 |
"cloud_token": env_config.get("cloud_token", ""),
|
294 |
+
"spark_endpoint": env_config.get("spark_endpoint", "http://localhost:7861")
|
295 |
}
|
296 |
|
297 |
@router.put("/environment")
|
298 |
+
async def update_environment(
|
299 |
+
update: EnvironmentUpdate,
|
300 |
+
username: str = Depends(verify_token)
|
301 |
+
):
|
302 |
"""Update environment configuration"""
|
303 |
config = load_config()
|
304 |
+
|
305 |
# Update config
|
306 |
+
config["config"]["work_mode"] = update.work_mode
|
307 |
+
config["config"]["cloud_token"] = update.cloud_token or ""
|
308 |
+
config["config"]["spark_endpoint"] = update.spark_endpoint
|
309 |
config["config"]["last_update_date"] = get_timestamp()
|
310 |
config["config"]["last_update_user"] = username
|
311 |
+
|
|
|
|
|
|
|
312 |
# Add activity log
|
313 |
+
add_activity_log(config, username, "UPDATE_ENVIRONMENT", "config", "environment",
|
314 |
+
"environment", f"Changed to {update.work_mode}")
|
315 |
+
|
316 |
+
# Save
|
317 |
save_config(config)
|
318 |
+
|
319 |
+
log(f"✅ Environment updated to {update.work_mode} by {username}")
|
320 |
return {"success": True}
|
321 |
|
322 |
# ===================== Project Endpoints =====================
|
323 |
+
@router.get("/projects/names")
|
324 |
+
def list_enabled_projects():
|
325 |
+
"""Get list of enabled project names for chat"""
|
326 |
+
cfg = load_config()
|
327 |
+
projects = cfg.get("projects", [])
|
328 |
+
return [p["name"] for p in projects if p.get("enabled", False) and not p.get("deleted", False)]
|
329 |
+
|
330 |
@router.get("/projects")
|
331 |
async def list_projects(
|
332 |
include_deleted: bool = False,
|
|
|
335 |
"""List all projects"""
|
336 |
config = load_config()
|
337 |
projects = config.get("projects", [])
|
338 |
+
|
339 |
# Filter deleted if needed
|
340 |
if not include_deleted:
|
341 |
projects = [p for p in projects if not p.get("deleted", False)]
|
342 |
+
|
343 |
return projects
|
344 |
|
|
|
|
|
|
|
|
|
|
|
345 |
@router.get("/projects/{project_id}")
|
346 |
async def get_project(
|
347 |
project_id: int,
|
|
|
351 |
try:
|
352 |
config = load_config()
|
353 |
projects = config.get("projects", [])
|
354 |
+
|
355 |
project = next((p for p in projects if p.get("id") == project_id), None)
|
356 |
if not project or project.get("deleted", False):
|
357 |
raise HTTPException(status_code=404, detail="Project not found")
|
358 |
+
|
359 |
return project
|
360 |
+
|
361 |
except HTTPException:
|
362 |
raise
|
363 |
except Exception as e:
|
|
|
371 |
username: str = Depends(verify_token)
|
372 |
):
|
373 |
cfg = load_config()
|
374 |
+
|
375 |
+
# 1️⃣ Yeni proje ID'si
|
376 |
project_id = cfg["config"].get("project_id_counter", 0) + 1
|
377 |
cfg["config"]["project_id_counter"] = project_id
|
378 |
+
|
379 |
# 2️⃣ Proje gövdesi
|
380 |
new_project = {
|
381 |
"id": project_id,
|
|
|
389 |
"created_by": username,
|
390 |
"last_update_date": datetime.utcnow().isoformat(),
|
391 |
"last_update_user": username,
|
392 |
+
|
393 |
# *** Versiyon sayaçları ***
|
394 |
"version_id_counter": 1,
|
395 |
+
|
396 |
# *** İlk versiyon (no = 1) ***
|
397 |
"versions": [{
|
398 |
"id": 1,
|
|
|
424 |
"published_by": None
|
425 |
}]
|
426 |
}
|
427 |
+
|
428 |
cfg.setdefault("projects", []).append(new_project)
|
429 |
save_config(cfg)
|
430 |
+
|
431 |
add_activity_log(cfg, username, "CREATE_PROJECT",
|
432 |
"project", project_id, new_project["name"])
|
433 |
save_config(cfg)
|
434 |
+
|
435 |
return new_project # 201 CREATED
|
436 |
|
437 |
@router.put("/projects/{project_id}")
|
|
|
442 |
):
|
443 |
"""Update project"""
|
444 |
config = load_config()
|
445 |
+
|
446 |
# Find project
|
447 |
project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
|
448 |
if not project:
|
449 |
raise HTTPException(status_code=404, detail="Project not found")
|
450 |
+
|
451 |
# Check race condition
|
452 |
if project.get("last_update_date") != update.last_update_date:
|
453 |
raise HTTPException(status_code=409, detail="Project was modified by another user")
|
454 |
+
|
455 |
# Update
|
456 |
project["caption"] = update.caption
|
457 |
project["last_update_date"] = get_timestamp()
|
458 |
project["last_update_user"] = username
|
459 |
+
|
460 |
# Add activity log
|
461 |
add_activity_log(config, username, "UPDATE_PROJECT", "project", project_id, project["name"])
|
462 |
+
|
463 |
# Save
|
464 |
save_config(config)
|
465 |
+
|
466 |
log(f"✅ Project '{project['name']}' updated by {username}")
|
467 |
return project
|
468 |
|
|
|
470 |
async def delete_project(project_id: int, username: str = Depends(verify_token)):
|
471 |
"""Delete project (soft delete)"""
|
472 |
config = load_config()
|
473 |
+
|
474 |
# Find project
|
475 |
project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
|
476 |
if not project:
|
477 |
raise HTTPException(status_code=404, detail="Project not found")
|
478 |
+
|
479 |
# Soft delete
|
480 |
project["deleted"] = True
|
481 |
project["last_update_date"] = get_timestamp()
|
482 |
project["last_update_user"] = username
|
483 |
+
|
484 |
# Add activity log
|
485 |
add_activity_log(config, username, "DELETE_PROJECT", "project", project_id, project["name"])
|
486 |
+
|
487 |
# Save
|
488 |
save_config(config)
|
489 |
+
|
490 |
log(f"✅ Project '{project['name']}' deleted by {username}")
|
491 |
return {"success": True}
|
492 |
|
493 |
@router.patch("/projects/{project_id}/toggle")
|
494 |
async def toggle_project(project_id: int, username: str = Depends(verify_token)):
|
495 |
+
"""Toggle project enabled status"""
|
496 |
config = load_config()
|
497 |
+
|
498 |
# Find project
|
499 |
project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
|
500 |
if not project:
|
501 |
raise HTTPException(status_code=404, detail="Project not found")
|
502 |
+
|
503 |
# Toggle
|
504 |
+
project["enabled"] = not project.get("enabled", False)
|
505 |
project["last_update_date"] = get_timestamp()
|
506 |
project["last_update_user"] = username
|
507 |
+
|
508 |
# Add activity log
|
509 |
action = "ENABLE_PROJECT" if project["enabled"] else "DISABLE_PROJECT"
|
510 |
add_activity_log(config, username, action, "project", project_id, project["name"])
|
511 |
+
|
512 |
# Save
|
513 |
save_config(config)
|
514 |
+
|
515 |
log(f"✅ Project '{project['name']}' {'enabled' if project['enabled'] else 'disabled'} by {username}")
|
516 |
return {"enabled": project["enabled"]}
|
517 |
|
518 |
# ===================== Version Endpoints =====================
|
519 |
+
@router.get("/projects/{project_id}/versions")
|
520 |
+
async def list_versions(
|
521 |
+
project_id: int,
|
522 |
+
include_deleted: bool = False,
|
523 |
+
username: str = Depends(verify_token)
|
524 |
+
):
|
525 |
+
"""List project versions"""
|
526 |
+
config = load_config()
|
527 |
+
|
528 |
+
# Find project
|
529 |
+
project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
|
530 |
+
if not project:
|
531 |
+
raise HTTPException(status_code=404, detail="Project not found")
|
532 |
+
|
533 |
+
versions = project.get("versions", [])
|
534 |
+
|
535 |
+
# Filter deleted if needed
|
536 |
+
if not include_deleted:
|
537 |
+
versions = [v for v in versions if not v.get("deleted", False)]
|
538 |
+
|
539 |
+
return versions
|
540 |
+
|
541 |
@router.post("/projects/{project_id}/versions")
|
542 |
async def create_version(
|
543 |
project_id: int,
|
544 |
+
version_data: VersionCreate,
|
545 |
username: str = Depends(verify_token)
|
546 |
):
|
547 |
+
"""Create new version"""
|
548 |
config = load_config()
|
549 |
+
|
550 |
# Find project
|
551 |
project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
|
552 |
if not project:
|
553 |
raise HTTPException(status_code=404, detail="Project not found")
|
554 |
+
|
555 |
+
# Get next version ID
|
556 |
+
version_id = project.get("version_id_counter", 0) + 1
|
557 |
+
project["version_id_counter"] = version_id
|
558 |
+
|
559 |
+
# Get next version number
|
560 |
+
existing_versions = [v for v in project.get("versions", []) if not v.get("deleted", False)]
|
561 |
+
version_no = max([v.get("no", 0) for v in existing_versions], default=0) + 1
|
562 |
+
|
563 |
+
# Create base version
|
564 |
+
new_version = {
|
565 |
+
"id": version_id,
|
566 |
+
"no": version_no,
|
567 |
+
"caption": version_data.caption,
|
568 |
+
"description": f"Version {version_no}",
|
569 |
+
"published": False,
|
570 |
+
"deleted": False,
|
571 |
+
"created_date": get_timestamp(),
|
572 |
+
"created_by": username,
|
573 |
+
"last_update_date": get_timestamp(),
|
574 |
+
"last_update_user": username,
|
575 |
+
"publish_date": None,
|
576 |
+
"published_by": None
|
577 |
+
}
|
578 |
+
|
579 |
+
# Copy from source version if specified
|
580 |
+
if version_data.source_version_id:
|
581 |
+
source_version = next(
|
582 |
+
(v for v in project.get("versions", []) if v["id"] == version_data.source_version_id),
|
583 |
+
None
|
584 |
+
)
|
585 |
+
if source_version:
|
586 |
+
# Copy configuration from source
|
587 |
+
new_version.update({
|
588 |
+
"general_prompt": source_version.get("general_prompt", ""),
|
589 |
+
"default_api": source_version.get("default_api", ""),
|
590 |
+
"llm": source_version.get("llm", {}).copy(),
|
591 |
+
"intents": [intent.copy() for intent in source_version.get("intents", [])],
|
592 |
+
"parameters": [param.copy() for param in source_version.get("parameters", [])]
|
593 |
+
})
|
594 |
+
else:
|
595 |
+
# Empty template
|
596 |
+
new_version.update({
|
597 |
+
"general_prompt": "",
|
598 |
+
"default_api": "",
|
599 |
+
"llm": {
|
600 |
+
"repo_id": "",
|
601 |
+
"generation_config": {
|
602 |
+
"max_new_tokens": 512,
|
603 |
+
"temperature": 0.7,
|
604 |
+
"top_p": 0.95,
|
605 |
+
"top_k": 50,
|
606 |
+
"repetition_penalty": 1.1
|
607 |
+
},
|
608 |
+
"use_fine_tune": False,
|
609 |
+
"fine_tune_zip": ""
|
610 |
+
},
|
611 |
+
"intents": [],
|
612 |
+
"parameters": []
|
613 |
+
})
|
614 |
+
|
615 |
+
# Add to project
|
616 |
+
if "versions" not in project:
|
617 |
+
project["versions"] = []
|
618 |
project["versions"].append(new_version)
|
619 |
+
|
620 |
+
# Update project timestamp
|
621 |
project["last_update_date"] = get_timestamp()
|
622 |
project["last_update_user"] = username
|
623 |
+
|
624 |
# Add activity log
|
625 |
+
add_activity_log(config, username, "CREATE_VERSION", "version", version_id,
|
626 |
+
f"{project['name']} v{version_no}")
|
627 |
+
|
628 |
# Save
|
629 |
save_config(config)
|
630 |
+
|
631 |
+
log(f"✅ Version {version_no} created for project '{project['name']}' by {username}")
|
632 |
return new_version
|
633 |
|
634 |
@router.put("/projects/{project_id}/versions/{version_id}")
|
|
|
636 |
project_id: int,
|
637 |
version_id: int,
|
638 |
update: VersionUpdate,
|
|
|
639 |
username: str = Depends(verify_token)
|
640 |
):
|
641 |
"""Update version"""
|
642 |
config = load_config()
|
643 |
+
|
644 |
# Find project and version
|
645 |
project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
|
646 |
if not project:
|
647 |
raise HTTPException(status_code=404, detail="Project not found")
|
648 |
+
|
649 |
version = next((v for v in project.get("versions", []) if v["id"] == version_id), None)
|
650 |
if not version:
|
651 |
raise HTTPException(status_code=404, detail="Version not found")
|
652 |
+
|
653 |
+
# Check race condition
|
654 |
+
if version.get("last_update_date") != update.last_update_date:
|
655 |
+
raise HTTPException(status_code=409, detail="Version was modified by another user")
|
656 |
+
|
657 |
+
# Cannot update published version
|
658 |
if version.get("published", False):
|
659 |
+
raise HTTPException(status_code=400, detail="Cannot modify published version")
|
660 |
+
|
661 |
+
# Update version
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
662 |
version["caption"] = update.caption
|
663 |
version["general_prompt"] = update.general_prompt
|
664 |
version["llm"] = update.llm
|
665 |
version["intents"] = [intent.dict() for intent in update.intents]
|
666 |
version["last_update_date"] = get_timestamp()
|
667 |
version["last_update_user"] = username
|
668 |
+
|
669 |
+
# Update project timestamp
|
670 |
project["last_update_date"] = get_timestamp()
|
671 |
project["last_update_user"] = username
|
672 |
+
|
673 |
# Add activity log
|
674 |
add_activity_log(config, username, "UPDATE_VERSION", "version", version_id,
|
675 |
+
f"{project['name']} v{version['no']}")
|
676 |
+
|
677 |
# Save
|
678 |
save_config(config)
|
679 |
+
|
680 |
+
log(f"✅ Version {version['no']} updated for project '{project['name']}' by {username}")
|
681 |
return version
|
682 |
|
683 |
@router.post("/projects/{project_id}/versions/{version_id}/publish")
|
|
|
688 |
):
|
689 |
"""Publish version"""
|
690 |
config = load_config()
|
691 |
+
|
692 |
# Find project and version
|
693 |
project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
|
694 |
if not project:
|
695 |
raise HTTPException(status_code=404, detail="Project not found")
|
696 |
+
|
697 |
version = next((v for v in project.get("versions", []) if v["id"] == version_id), None)
|
698 |
if not version:
|
699 |
raise HTTPException(status_code=404, detail="Version not found")
|
700 |
+
|
701 |
# Unpublish all other versions
|
702 |
for v in project.get("versions", []):
|
703 |
if v["id"] != version_id:
|
704 |
v["published"] = False
|
705 |
+
|
706 |
# Publish this version
|
707 |
version["published"] = True
|
708 |
version["publish_date"] = get_timestamp()
|
709 |
version["published_by"] = username
|
710 |
version["last_update_date"] = get_timestamp()
|
711 |
version["last_update_user"] = username
|
712 |
+
|
713 |
+
# Update project timestamp
|
714 |
project["last_update_date"] = get_timestamp()
|
715 |
project["last_update_user"] = username
|
716 |
+
|
717 |
# Add activity log
|
718 |
add_activity_log(config, username, "PUBLISH_VERSION", "version", version_id,
|
719 |
+
f"{project['name']} v{version['no']}")
|
720 |
+
|
721 |
# Save
|
722 |
save_config(config)
|
723 |
+
|
|
|
|
|
724 |
log(f"✅ Version {version_id} published for project '{project['name']}' by {username}")
|
725 |
return {"success": True}
|
726 |
|
|
|
732 |
):
|
733 |
"""Delete version (soft delete)"""
|
734 |
config = load_config()
|
735 |
+
|
736 |
# Find project and version
|
737 |
project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
|
738 |
if not project:
|
739 |
raise HTTPException(status_code=404, detail="Project not found")
|
740 |
+
|
741 |
version = next((v for v in project.get("versions", []) if v["id"] == version_id), None)
|
742 |
if not version:
|
743 |
raise HTTPException(status_code=404, detail="Version not found")
|
744 |
+
|
745 |
# Cannot delete published version
|
746 |
if version.get("published", False):
|
747 |
raise HTTPException(status_code=400, detail="Cannot delete published version")
|
748 |
+
|
749 |
# Soft delete
|
750 |
version["deleted"] = True
|
751 |
version["last_update_date"] = get_timestamp()
|
752 |
version["last_update_user"] = username
|
753 |
+
|
754 |
project["last_update_date"] = get_timestamp()
|
755 |
project["last_update_user"] = username
|
756 |
+
|
757 |
# Add activity log
|
758 |
add_activity_log(config, username, "DELETE_VERSION", "version", version_id,
|
759 |
f"{project['name']} v{version_id}")
|
760 |
+
|
761 |
# Save
|
762 |
save_config(config)
|
763 |
+
|
764 |
log(f"✅ Version {version_id} deleted for project '{project['name']}' by {username}")
|
765 |
return {"success": True}
|
766 |
|
|
|
773 |
"""List all APIs"""
|
774 |
config = load_config()
|
775 |
apis = config.get("apis", [])
|
776 |
+
|
777 |
# Filter deleted if needed
|
778 |
if not include_deleted:
|
779 |
apis = [a for a in apis if not a.get("deleted", False)]
|
780 |
+
|
781 |
return apis
|
782 |
|
783 |
@router.post("/apis")
|
784 |
async def create_api(api: APICreate, username: str = Depends(verify_token)):
|
785 |
"""Create new API"""
|
786 |
config = load_config()
|
787 |
+
|
788 |
# Check duplicate name
|
789 |
existing = [a for a in config.get("apis", []) if a["name"] == api.name]
|
790 |
if existing:
|
791 |
raise HTTPException(status_code=400, detail="API name already exists")
|
792 |
+
|
793 |
# Create API
|
794 |
new_api = api.dict()
|
795 |
new_api["deleted"] = False
|
|
|
797 |
new_api["created_by"] = username
|
798 |
new_api["last_update_date"] = get_timestamp()
|
799 |
new_api["last_update_user"] = username
|
800 |
+
|
801 |
if "apis" not in config:
|
802 |
config["apis"] = []
|
803 |
config["apis"].append(new_api)
|
804 |
+
|
805 |
# Add activity log
|
806 |
add_activity_log(config, username, "CREATE_API", "api", api.name, api.name)
|
807 |
+
|
808 |
# Save
|
809 |
save_config(config)
|
810 |
+
|
811 |
log(f"✅ API '{api.name}' created by {username}")
|
812 |
return new_api
|
813 |
|
|
|
819 |
):
|
820 |
"""Update API"""
|
821 |
config = load_config()
|
822 |
+
|
823 |
# Find API
|
824 |
api = next((a for a in config.get("apis", []) if a["name"] == api_name), None)
|
825 |
if not api:
|
826 |
raise HTTPException(status_code=404, detail="API not found")
|
827 |
+
|
828 |
# Check race condition
|
829 |
if api.get("last_update_date") != update.last_update_date:
|
830 |
raise HTTPException(status_code=409, detail="API was modified by another user")
|
831 |
+
|
832 |
# Check if API is in use
|
833 |
for project in config.get("projects", []):
|
834 |
for version in project.get("versions", []):
|
|
|
836 |
if intent.get("action") == api_name and version.get("published", False):
|
837 |
raise HTTPException(status_code=400,
|
838 |
detail=f"API is used in published version of project '{project['name']}'")
|
839 |
+
|
840 |
# Update
|
841 |
update_dict = update.dict()
|
842 |
del update_dict["last_update_date"]
|
843 |
api.update(update_dict)
|
844 |
api["last_update_date"] = get_timestamp()
|
845 |
api["last_update_user"] = username
|
846 |
+
|
847 |
# Add activity log
|
848 |
add_activity_log(config, username, "UPDATE_API", "api", api_name, api_name)
|
849 |
+
|
850 |
# Save
|
851 |
save_config(config)
|
852 |
+
|
853 |
log(f"✅ API '{api_name}' updated by {username}")
|
854 |
return api
|
855 |
|
|
|
857 |
async def delete_api(api_name: str, username: str = Depends(verify_token)):
|
858 |
"""Delete API (soft delete)"""
|
859 |
config = load_config()
|
860 |
+
|
861 |
# Find API
|
862 |
api = next((a for a in config.get("apis", []) if a["name"] == api_name), None)
|
863 |
if not api:
|
864 |
raise HTTPException(status_code=404, detail="API not found")
|
865 |
+
|
866 |
# Check if API is in use
|
867 |
for project in config.get("projects", []):
|
868 |
for version in project.get("versions", []):
|
|
|
870 |
if intent.get("action") == api_name:
|
871 |
raise HTTPException(status_code=400,
|
872 |
detail=f"API is used in project '{project['name']}'")
|
873 |
+
|
874 |
# Soft delete
|
875 |
api["deleted"] = True
|
876 |
api["last_update_date"] = get_timestamp()
|
877 |
api["last_update_user"] = username
|
878 |
+
|
879 |
# Add activity log
|
880 |
add_activity_log(config, username, "DELETE_API", "api", api_name, api_name)
|
881 |
+
|
882 |
# Save
|
883 |
save_config(config)
|
884 |
+
|
885 |
log(f"✅ API '{api_name}' deleted by {username}")
|
886 |
return {"success": True}
|
887 |
|
|
|
890 |
async def test_api(api: APICreate, username: str = Depends(verify_token)):
|
891 |
"""Test API endpoint"""
|
892 |
import requests
|
893 |
+
|
894 |
try:
|
895 |
# Prepare request
|
896 |
headers = api.headers.copy()
|
|
|
898 |
# Add sample auth token for testing
|
899 |
if api.auth and api.auth.get("enabled"):
|
900 |
headers["Authorization"] = "Bearer test_token_12345"
|
901 |
+
|
902 |
# Make request
|
903 |
response = requests.request(
|
904 |
method=api.method,
|
|
|
909 |
timeout=api.timeout_seconds,
|
910 |
proxies={"http": api.proxy, "https": api.proxy} if api.proxy else None
|
911 |
)
|
912 |
+
|
913 |
return {
|
914 |
"success": True,
|
915 |
"status_code": response.status_code,
|
916 |
+
"response": response.text[:500], # First 500 chars
|
917 |
+
"headers": dict(response.headers)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
918 |
}
|
919 |
except Exception as e:
|
920 |
return {
|
921 |
"success": False,
|
922 |
+
"error": str(e)
|
|
|
923 |
}
|
924 |
|
925 |
+
@router.post("/test/run-all")
|
926 |
+
async def run_all_tests(
|
927 |
+
request: TestRequest,
|
928 |
+
username: str = Depends(verify_token)
|
929 |
+
):
|
930 |
+
"""Run all tests"""
|
931 |
+
# TODO: Implement test runner
|
932 |
+
return {
|
933 |
+
"status": "completed",
|
934 |
+
"total": 10,
|
935 |
+
"passed": 8,
|
936 |
+
"failed": 2,
|
937 |
+
"details": []
|
938 |
+
}
|
939 |
+
|
940 |
+
# ===================== Import/Export Endpoints =====================
|
941 |
+
@router.post("/projects/import")
|
942 |
+
async def import_project(
|
943 |
+
project_data: dict = Body(...),
|
944 |
username: str = Depends(verify_token)
|
945 |
):
|
946 |
+
"""Import project from JSON"""
|
947 |
config = load_config()
|
|
|
948 |
|
949 |
+
# Validate structure
|
950 |
+
if "name" not in project_data:
|
951 |
+
raise HTTPException(status_code=400, detail="Invalid project data")
|
952 |
|
953 |
+
# Check duplicate name
|
954 |
+
existing = [p for p in config.get("projects", [])
|
955 |
+
if p["name"] == project_data["name"] and not p.get("deleted", False)]
|
956 |
+
if existing:
|
957 |
+
raise HTTPException(status_code=400, detail="Project name already exists")
|
958 |
|
959 |
+
# Get new project ID
|
960 |
+
project_id = config["config"].get("project_id_counter", 0) + 1
|
961 |
+
config["config"]["project_id_counter"] = project_id
|
962 |
|
963 |
+
# Create new project
|
964 |
+
new_project = {
|
965 |
+
"id": project_id,
|
966 |
+
"name": project_data["name"],
|
967 |
+
"caption": project_data.get("caption", ""),
|
968 |
+
"icon": project_data.get("icon", "folder"),
|
969 |
+
"description": project_data.get("description", ""),
|
970 |
+
"enabled": False,
|
971 |
+
"deleted": False,
|
972 |
+
"created_date": get_timestamp(),
|
973 |
+
"created_by": username,
|
974 |
+
"last_update_date": get_timestamp(),
|
975 |
+
"last_update_user": username,
|
976 |
+
"version_id_counter": 1,
|
977 |
+
"versions": []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
978 |
}
|
979 |
+
|
980 |
+
# Import versions
|
981 |
+
for idx, version_data in enumerate(project_data.get("versions", [])):
|
982 |
+
new_version = {
|
983 |
+
"id": idx + 1,
|
984 |
+
"no": idx + 1,
|
985 |
+
"caption": version_data.get("caption", f"Version {idx + 1}"),
|
986 |
+
"description": version_data.get("description", ""),
|
987 |
+
"published": False,
|
988 |
+
"deleted": False,
|
989 |
+
"created_date": get_timestamp(),
|
990 |
+
"created_by": username,
|
991 |
+
"last_update_date": get_timestamp(),
|
992 |
+
"last_update_user": username,
|
993 |
+
"publish_date": None,
|
994 |
+
"published_by": None,
|
995 |
+
"general_prompt": version_data.get("general_prompt", ""),
|
996 |
+
"default_api": version_data.get("default_api", ""),
|
997 |
+
"llm": version_data.get("llm", {}),
|
998 |
+
"intents": version_data.get("intents", []),
|
999 |
+
"parameters": version_data.get("parameters", [])
|
1000 |
}
|
1001 |
+
new_project["versions"].append(new_version)
|
1002 |
+
new_project["version_id_counter"] = idx + 1
|
1003 |
+
|
1004 |
+
# Add to config
|
1005 |
+
if "projects" not in config:
|
1006 |
+
config["projects"] = []
|
1007 |
+
config["projects"].append(new_project)
|
1008 |
+
|
1009 |
+
# Add activity log
|
1010 |
+
add_activity_log(config, username, "IMPORT_PROJECT", "project", project_id, new_project["name"])
|
1011 |
+
|
1012 |
+
# Save
|
1013 |
+
save_config(config)
|
1014 |
+
|
1015 |
+
log(f"✅ Project '{new_project['name']}' imported by {username}")
|
1016 |
+
return new_project
|
1017 |
|
|
|
1018 |
@router.get("/projects/{project_id}/export")
|
1019 |
+
async def export_project(
|
1020 |
+
project_id: int,
|
1021 |
+
username: str = Depends(verify_token)
|
1022 |
+
):
|
1023 |
"""Export project as JSON"""
|
1024 |
config = load_config()
|
1025 |
+
|
1026 |
# Find project
|
1027 |
project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
|
1028 |
if not project:
|
1029 |
raise HTTPException(status_code=404, detail="Project not found")
|
1030 |
+
|
1031 |
+
# Create export data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1032 |
export_data = {
|
1033 |
+
"name": project["name"],
|
1034 |
+
"caption": project.get("caption", ""),
|
1035 |
+
"icon": project.get("icon", "folder"),
|
1036 |
+
"description": project.get("description", ""),
|
1037 |
+
"versions": []
|
1038 |
}
|
1039 |
+
|
1040 |
+
# Export versions
|
1041 |
+
for version in project.get("versions", []):
|
1042 |
+
if not version.get("deleted", False):
|
1043 |
+
export_version = {
|
1044 |
+
"caption": version.get("caption", ""),
|
1045 |
+
"description": version.get("description", ""),
|
1046 |
+
"general_prompt": version.get("general_prompt", ""),
|
1047 |
+
"default_api": version.get("default_api", ""),
|
1048 |
+
"llm": version.get("llm", {}),
|
1049 |
+
"intents": version.get("intents", []),
|
1050 |
+
"parameters": version.get("parameters", [])
|
1051 |
+
}
|
1052 |
+
export_data["versions"].append(export_version)
|
1053 |
+
|
1054 |
+
# Add activity log
|
1055 |
+
add_activity_log(config, username, "EXPORT_PROJECT", "project", project_id, project["name"])
|
1056 |
+
save_config(config)
|
1057 |
+
|
1058 |
+
log(f"✅ Project '{project['name']}' exported by {username}")
|
1059 |
return export_data
|
1060 |
|
1061 |
+
# ===================== Activity Log Endpoints =====================
|
1062 |
+
@router.get("/activity-log")
|
1063 |
+
async def get_activity_log(
|
1064 |
+
limit: int = Query(100, ge=1, le=1000),
|
1065 |
+
username: str = Depends(verify_token)
|
1066 |
+
):
|
1067 |
+
"""Get activity log"""
|
1068 |
config = load_config()
|
1069 |
+
logs = config.get("activity_log", [])
|
1070 |
+
|
1071 |
+
# Return latest entries
|
1072 |
+
return logs[-limit:]
|
1073 |
|
1074 |
+
# ===================== Cleanup Task =====================
|
1075 |
+
def cleanup_old_logs():
|
1076 |
+
"""Cleanup old activity logs (runs in background)"""
|
1077 |
+
while True:
|
1078 |
+
try:
|
1079 |
+
config = load_config()
|
1080 |
+
if "activity_log" in config and len(config["activity_log"]) > 5000:
|
1081 |
+
# Keep only last 1000 entries
|
1082 |
+
config["activity_log"] = config["activity_log"][-1000:]
|
1083 |
+
save_config(config)
|
1084 |
+
log("🧹 Cleaned up old activity logs")
|
1085 |
+
except Exception as e:
|
1086 |
+
log(f"Error in cleanup task: {e}")
|
1087 |
+
|
1088 |
+
# Run every hour
|
1089 |
+
time.sleep(3600)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1090 |
|
1091 |
+
def start_cleanup_task():
|
1092 |
+
"""Start cleanup task in background"""
|
1093 |
+
cleanup_thread = threading.Thread(target=cleanup_old_logs, daemon=True)
|
1094 |
+
cleanup_thread.start()
|
1095 |
+
log("🧹 Started activity log cleanup task")
|
|
|
|