ciyidogan commited on
Commit
a049554
·
verified ·
1 Parent(s): adfe3ef

Upload admin_routes.py

Browse files
Files changed (1) hide show
  1. admin_routes.py +853 -0
admin_routes.py ADDED
@@ -0,0 +1,853 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Flare Admin API Routes
3
+ ~~~~~~~~~~~~~~~~~~~~~
4
+ Admin UI için gerekli tüm endpoint'ler
5
+ """
6
+
7
+ import json
8
+ import hashlib
9
+ import secrets
10
+ import commentjson
11
+ from datetime import datetime, timedelta
12
+ from typing import Dict, List, Optional, Any
13
+ from pathlib import Path
14
+
15
+ from fastapi import APIRouter, HTTPException, Depends, Header
16
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
17
+ from pydantic import BaseModel, Field
18
+ import jwt
19
+
20
+ from utils import log
21
+ from config_provider import ConfigProvider
22
+
23
+ # ===================== Constants & Config =====================
24
+ JWT_SECRET = "flare-admin-secret-key-change-in-production"
25
+ JWT_ALGORITHM = "HS256"
26
+ JWT_EXPIRATION_HOURS = 24
27
+
28
+ router = APIRouter(prefix="/api")
29
+ security = HTTPBearer()
30
+
31
+ # ===================== Models =====================
32
+ class LoginRequest(BaseModel):
33
+ username: str
34
+ password: str
35
+
36
+ class LoginResponse(BaseModel):
37
+ token: str
38
+ username: str
39
+
40
+ class EnvironmentUpdate(BaseModel):
41
+ work_mode: str
42
+ cloud_token: Optional[str] = None
43
+ spark_endpoint: str
44
+
45
+ class ProjectCreate(BaseModel):
46
+ name: str
47
+ caption: Optional[str] = ""
48
+
49
+ class ProjectUpdate(BaseModel):
50
+ caption: str
51
+ last_update_date: str
52
+
53
+ class VersionCreate(BaseModel):
54
+ source_version_id: int
55
+ caption: str
56
+
57
+ class IntentModel(BaseModel):
58
+ name: str
59
+ caption: Optional[str] = ""
60
+ locale: str = "tr-TR"
61
+ detection_prompt: str
62
+ examples: List[str] = []
63
+ parameters: List[Dict[str, Any]] = []
64
+ action: str
65
+ fallback_timeout_prompt: Optional[str] = None
66
+ fallback_error_prompt: Optional[str] = None
67
+
68
+ class VersionUpdate(BaseModel):
69
+ caption: str
70
+ general_prompt: str
71
+ llm: Dict[str, Any]
72
+ intents: List[IntentModel]
73
+ last_update_date: str
74
+
75
+ class APICreate(BaseModel):
76
+ name: str
77
+ url: str
78
+ method: str = "POST"
79
+ headers: Dict[str, str] = {}
80
+ body_template: Dict[str, Any] = {}
81
+ timeout_seconds: int = 10
82
+ retry: Dict[str, Any] = Field(default_factory=lambda: {"retry_count": 3, "backoff_seconds": 2, "strategy": "static"})
83
+ proxy: Optional[str] = None
84
+ auth: Optional[Dict[str, Any]] = None
85
+ response_prompt: Optional[str] = None
86
+
87
+ class APIUpdate(BaseModel):
88
+ url: str
89
+ method: str
90
+ headers: Dict[str, str]
91
+ body_template: Dict[str, Any]
92
+ timeout_seconds: int
93
+ retry: Dict[str, Any]
94
+ proxy: Optional[str]
95
+ auth: Optional[Dict[str, Any]]
96
+ response_prompt: Optional[str]
97
+ last_update_date: str
98
+
99
+ class TestRequest(BaseModel):
100
+ test_type: str # "all", "ui", "backend", "integration", "spark"
101
+
102
+ # ===================== Helpers =====================
103
+ def hash_password(password: str) -> str:
104
+ """Simple SHA256 hash for demo. Use bcrypt in production!"""
105
+ return hashlib.sha256(password.encode()).hexdigest()
106
+
107
+ def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)) -> str:
108
+ """Verify JWT token and return username"""
109
+ try:
110
+ payload = jwt.decode(credentials.credentials, JWT_SECRET, algorithms=[JWT_ALGORITHM])
111
+ username = payload.get("sub")
112
+ if username is None:
113
+ raise HTTPException(status_code=401, detail="Invalid token")
114
+ return username
115
+ except jwt.ExpiredSignatureError:
116
+ raise HTTPException(status_code=401, detail="Token expired")
117
+ except jwt.JWTError:
118
+ raise HTTPException(status_code=401, detail="Invalid token")
119
+
120
+ def load_config() -> Dict[str, Any]:
121
+ """Load service_config.jsonc"""
122
+ config_path = Path("service_config.jsonc")
123
+ with open(config_path, 'r', encoding='utf-8') as f:
124
+ return commentjson.load(f)
125
+
126
+ def save_config(config: Dict[str, Any]):
127
+ """Save service_config.jsonc with pretty formatting"""
128
+ config_path = Path("service_config.jsonc")
129
+ with open(config_path, 'w', encoding='utf-8') as f:
130
+ # Convert to JSON string with proper formatting
131
+ json_str = json.dumps(config, indent=2, ensure_ascii=False)
132
+ f.write(json_str)
133
+
134
+ def get_timestamp() -> str:
135
+ """Get current ISO timestamp"""
136
+ return datetime.utcnow().isoformat() + "Z"
137
+
138
+ def add_activity_log(config: Dict[str, Any], user: str, action: str, entity_type: str,
139
+ entity_id: Any, entity_name: str, details: str = ""):
140
+ """Add entry to activity log"""
141
+ if "activity_log" not in config:
142
+ config["activity_log"] = []
143
+
144
+ log_entry = {
145
+ "id": len(config["activity_log"]) + 1,
146
+ "timestamp": get_timestamp(),
147
+ "user": user,
148
+ "action": action,
149
+ "entity_type": entity_type,
150
+ "entity_id": entity_id,
151
+ "entity_name": entity_name,
152
+ "details": details
153
+ }
154
+
155
+ config["activity_log"].append(log_entry)
156
+
157
+ # Keep only last 100 entries
158
+ if len(config["activity_log"]) > 100:
159
+ config["activity_log"] = config["activity_log"][-100:]
160
+
161
+ # ===================== Auth Endpoints =====================
162
+ @router.post("/login", response_model=LoginResponse)
163
+ async def login(request: LoginRequest):
164
+ """User login"""
165
+ config = load_config()
166
+
167
+ # Find user
168
+ users = config.get("config", {}).get("users", [])
169
+ user = next((u for u in users if u["username"] == request.username), None)
170
+
171
+ if not user:
172
+ raise HTTPException(status_code=401, detail="Invalid credentials")
173
+
174
+ # Verify password (simple hash for demo)
175
+ if hash_password(request.password) != user["password_hash"]:
176
+ raise HTTPException(status_code=401, detail="Invalid credentials")
177
+
178
+ # Create JWT token
179
+ expire = datetime.utcnow() + timedelta(hours=JWT_EXPIRATION_HOURS)
180
+ payload = {
181
+ "sub": request.username,
182
+ "exp": expire
183
+ }
184
+ token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
185
+
186
+ log(f"✅ User '{request.username}' logged in")
187
+ return LoginResponse(token=token, username=request.username)
188
+
189
+ # ===================== Environment Endpoints =====================
190
+ @router.get("/environment")
191
+ async def get_environment(username: str = Depends(verify_token)):
192
+ """Get environment configuration"""
193
+ config = load_config()
194
+ env_config = config.get("config", {})
195
+
196
+ return {
197
+ "work_mode": env_config.get("work_mode", "hfcloud"),
198
+ "cloud_token": env_config.get("cloud_token", ""),
199
+ "spark_endpoint": env_config.get("spark_endpoint", "")
200
+ }
201
+
202
+ @router.put("/environment")
203
+ async def update_environment(env: EnvironmentUpdate, username: str = Depends(verify_token)):
204
+ """Update environment configuration"""
205
+ config = load_config()
206
+
207
+ # Update config
208
+ config["config"]["work_mode"] = env.work_mode
209
+ config["config"]["cloud_token"] = env.cloud_token if env.work_mode != "on-premise" else ""
210
+ config["config"]["spark_endpoint"] = env.spark_endpoint
211
+ config["config"]["last_update_date"] = get_timestamp()
212
+ config["config"]["last_update_user"] = username
213
+
214
+ # Save
215
+ save_config(config)
216
+
217
+ # Add activity log
218
+ add_activity_log(config, username, "UPDATE_ENVIRONMENT", "config", 0, "environment",
219
+ f"Work mode: {env.work_mode}")
220
+ save_config(config)
221
+
222
+ log(f"✅ Environment updated by {username}")
223
+ return {"success": True}
224
+
225
+ # ===================== Project Endpoints =====================
226
+ @router.get("/projects")
227
+ async def list_projects(
228
+ include_deleted: bool = False,
229
+ username: str = Depends(verify_token)
230
+ ):
231
+ """List all projects"""
232
+ config = load_config()
233
+ projects = config.get("projects", [])
234
+
235
+ # Filter deleted if needed
236
+ if not include_deleted:
237
+ projects = [p for p in projects if not p.get("deleted", False)]
238
+
239
+ return projects
240
+
241
+ @router.post("/projects")
242
+ async def create_project(project: ProjectCreate, username: str = Depends(verify_token)):
243
+ """Create new project"""
244
+ config = load_config()
245
+
246
+ # Check duplicate name
247
+ existing = [p for p in config.get("projects", []) if p["name"] == project.name]
248
+ if existing:
249
+ raise HTTPException(status_code=400, detail="Project name already exists")
250
+
251
+ # Get next ID
252
+ config["config"]["project_id_counter"] = config["config"].get("project_id_counter", 0) + 1
253
+ project_id = config["config"]["project_id_counter"]
254
+
255
+ # Create project
256
+ new_project = {
257
+ "id": project_id,
258
+ "name": project.name,
259
+ "caption": project.caption,
260
+ "enabled": True,
261
+ "last_version_number": 0,
262
+ "version_id_counter": 0,
263
+ "versions": [],
264
+ "deleted": False,
265
+ "created_date": get_timestamp(),
266
+ "created_by": username,
267
+ "last_update_date": get_timestamp(),
268
+ "last_update_user": username
269
+ }
270
+
271
+ if "projects" not in config:
272
+ config["projects"] = []
273
+ config["projects"].append(new_project)
274
+
275
+ # Add activity log
276
+ add_activity_log(config, username, "CREATE_PROJECT", "project", project_id, project.name)
277
+
278
+ # Save
279
+ save_config(config)
280
+
281
+ log(f"✅ Project '{project.name}' created by {username}")
282
+ return new_project
283
+
284
+ @router.put("/projects/{project_id}")
285
+ async def update_project(
286
+ project_id: int,
287
+ update: ProjectUpdate,
288
+ username: str = Depends(verify_token)
289
+ ):
290
+ """Update project"""
291
+ config = load_config()
292
+
293
+ # Find project
294
+ project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
295
+ if not project:
296
+ raise HTTPException(status_code=404, detail="Project not found")
297
+
298
+ # Check race condition
299
+ if project.get("last_update_date") != update.last_update_date:
300
+ raise HTTPException(status_code=409, detail="Project was modified by another user")
301
+
302
+ # Update
303
+ project["caption"] = update.caption
304
+ project["last_update_date"] = get_timestamp()
305
+ project["last_update_user"] = username
306
+
307
+ # Add activity log
308
+ add_activity_log(config, username, "UPDATE_PROJECT", "project", project_id, project["name"])
309
+
310
+ # Save
311
+ save_config(config)
312
+
313
+ log(f"✅ Project '{project['name']}' updated by {username}")
314
+ return project
315
+
316
+ @router.delete("/projects/{project_id}")
317
+ async def delete_project(project_id: int, username: str = Depends(verify_token)):
318
+ """Delete project (soft delete)"""
319
+ config = load_config()
320
+
321
+ # Find project
322
+ project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
323
+ if not project:
324
+ raise HTTPException(status_code=404, detail="Project not found")
325
+
326
+ # Soft delete
327
+ project["deleted"] = True
328
+ project["last_update_date"] = get_timestamp()
329
+ project["last_update_user"] = username
330
+
331
+ # Add activity log
332
+ add_activity_log(config, username, "DELETE_PROJECT", "project", project_id, project["name"])
333
+
334
+ # Save
335
+ save_config(config)
336
+
337
+ log(f"✅ Project '{project['name']}' deleted by {username}")
338
+ return {"success": True}
339
+
340
+ @router.patch("/projects/{project_id}/toggle")
341
+ async def toggle_project(project_id: int, username: str = Depends(verify_token)):
342
+ """Enable/disable project"""
343
+ config = load_config()
344
+
345
+ # Find project
346
+ project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
347
+ if not project:
348
+ raise HTTPException(status_code=404, detail="Project not found")
349
+
350
+ # Toggle
351
+ project["enabled"] = not project.get("enabled", True)
352
+ project["last_update_date"] = get_timestamp()
353
+ project["last_update_user"] = username
354
+
355
+ # Add activity log
356
+ action = "ENABLE_PROJECT" if project["enabled"] else "DISABLE_PROJECT"
357
+ add_activity_log(config, username, action, "project", project_id, project["name"])
358
+
359
+ # Save
360
+ save_config(config)
361
+
362
+ log(f"✅ Project '{project['name']}' {'enabled' if project['enabled'] else 'disabled'} by {username}")
363
+ return {"enabled": project["enabled"]}
364
+
365
+ # ===================== Version Endpoints =====================
366
+ @router.post("/projects/{project_id}/versions")
367
+ async def create_version(
368
+ project_id: int,
369
+ version: VersionCreate,
370
+ username: str = Depends(verify_token)
371
+ ):
372
+ """Create new version from existing version"""
373
+ config = load_config()
374
+
375
+ # Find project
376
+ project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
377
+ if not project:
378
+ raise HTTPException(status_code=404, detail="Project not found")
379
+
380
+ # Check if there's unpublished version
381
+ unpublished = [v for v in project.get("versions", []) if not v.get("published", False)]
382
+ if unpublished:
383
+ raise HTTPException(status_code=400, detail="There is already an unpublished version")
384
+
385
+ # Find source version
386
+ source = next((v for v in project.get("versions", []) if v["id"] == version.source_version_id), None)
387
+ if not source:
388
+ raise HTTPException(status_code=404, detail="Source version not found")
389
+
390
+ # Create new version
391
+ project["version_id_counter"] = project.get("version_id_counter", 0) + 1
392
+ new_version = source.copy()
393
+ new_version["id"] = project["version_id_counter"]
394
+ new_version["caption"] = version.caption
395
+ new_version["published"] = False
396
+ new_version["created_date"] = get_timestamp()
397
+ new_version["created_by"] = username
398
+ new_version["last_update_date"] = get_timestamp()
399
+ new_version["last_update_user"] = username
400
+ new_version["publish_date"] = None
401
+ new_version["published_by"] = None
402
+
403
+ project["versions"].append(new_version)
404
+ project["last_update_date"] = get_timestamp()
405
+ project["last_update_user"] = username
406
+
407
+ # Add activity log
408
+ add_activity_log(config, username, "CREATE_VERSION", "version", new_version["id"],
409
+ f"{project['name']} v{new_version['id']}")
410
+
411
+ # Save
412
+ save_config(config)
413
+
414
+ log(f"✅ Version {new_version['id']} created for project '{project['name']}' by {username}")
415
+ return new_version
416
+
417
+ @router.put("/projects/{project_id}/versions/{version_id}")
418
+ async def update_version(
419
+ project_id: int,
420
+ version_id: int,
421
+ update: VersionUpdate,
422
+ username: str = Depends(verify_token)
423
+ ):
424
+ """Update version"""
425
+ config = load_config()
426
+
427
+ # Find project and version
428
+ project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
429
+ if not project:
430
+ raise HTTPException(status_code=404, detail="Project not found")
431
+
432
+ version = next((v for v in project.get("versions", []) if v["id"] == version_id), None)
433
+ if not version:
434
+ raise HTTPException(status_code=404, detail="Version not found")
435
+
436
+ # Check if published
437
+ if version.get("published", False):
438
+ raise HTTPException(status_code=400, detail="Cannot update published version")
439
+
440
+ # Check race condition
441
+ if version.get("last_update_date") != update.last_update_date:
442
+ raise HTTPException(status_code=409, detail="Version was modified by another user")
443
+
444
+ # Update
445
+ version["caption"] = update.caption
446
+ version["general_prompt"] = update.general_prompt
447
+ version["llm"] = update.llm
448
+ version["intents"] = [intent.dict() for intent in update.intents]
449
+ version["last_update_date"] = get_timestamp()
450
+ version["last_update_user"] = username
451
+
452
+ project["last_update_date"] = get_timestamp()
453
+ project["last_update_user"] = username
454
+
455
+ # Add activity log
456
+ add_activity_log(config, username, "UPDATE_VERSION", "version", version_id,
457
+ f"{project['name']} v{version_id}")
458
+
459
+ # Save
460
+ save_config(config)
461
+
462
+ log(f"✅ Version {version_id} updated for project '{project['name']}' by {username}")
463
+ return version
464
+
465
+ @router.post("/projects/{project_id}/versions/{version_id}/publish")
466
+ async def publish_version(
467
+ project_id: int,
468
+ version_id: int,
469
+ username: str = Depends(verify_token)
470
+ ):
471
+ """Publish version"""
472
+ config = load_config()
473
+
474
+ # Find project and version
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
+ version = next((v for v in project.get("versions", []) if v["id"] == version_id), None)
480
+ if not version:
481
+ raise HTTPException(status_code=404, detail="Version not found")
482
+
483
+ # Unpublish all other versions
484
+ for v in project.get("versions", []):
485
+ if v["id"] != version_id:
486
+ v["published"] = False
487
+
488
+ # Publish this version
489
+ version["published"] = True
490
+ version["publish_date"] = get_timestamp()
491
+ version["published_by"] = username
492
+ version["last_update_date"] = get_timestamp()
493
+ version["last_update_user"] = username
494
+
495
+ project["last_update_date"] = get_timestamp()
496
+ project["last_update_user"] = username
497
+
498
+ # Add activity log
499
+ add_activity_log(config, username, "PUBLISH_VERSION", "version", version_id,
500
+ f"{project['name']} v{version_id}")
501
+
502
+ # Save
503
+ save_config(config)
504
+
505
+ # TODO: Notify Spark about new version
506
+
507
+ log(f"✅ Version {version_id} published for project '{project['name']}' by {username}")
508
+ return {"success": True}
509
+
510
+ @router.delete("/projects/{project_id}/versions/{version_id}")
511
+ async def delete_version(
512
+ project_id: int,
513
+ version_id: int,
514
+ username: str = Depends(verify_token)
515
+ ):
516
+ """Delete version (soft delete)"""
517
+ config = load_config()
518
+
519
+ # Find project and version
520
+ project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
521
+ if not project:
522
+ raise HTTPException(status_code=404, detail="Project not found")
523
+
524
+ version = next((v for v in project.get("versions", []) if v["id"] == version_id), None)
525
+ if not version:
526
+ raise HTTPException(status_code=404, detail="Version not found")
527
+
528
+ # Cannot delete published version
529
+ if version.get("published", False):
530
+ raise HTTPException(status_code=400, detail="Cannot delete published version")
531
+
532
+ # Soft delete
533
+ version["deleted"] = True
534
+ version["last_update_date"] = get_timestamp()
535
+ version["last_update_user"] = username
536
+
537
+ project["last_update_date"] = get_timestamp()
538
+ project["last_update_user"] = username
539
+
540
+ # Add activity log
541
+ add_activity_log(config, username, "DELETE_VERSION", "version", version_id,
542
+ f"{project['name']} v{version_id}")
543
+
544
+ # Save
545
+ save_config(config)
546
+
547
+ log(f"✅ Version {version_id} deleted for project '{project['name']}' by {username}")
548
+ return {"success": True}
549
+
550
+ # ===================== API Endpoints =====================
551
+ @router.get("/apis")
552
+ async def list_apis(
553
+ include_deleted: bool = False,
554
+ username: str = Depends(verify_token)
555
+ ):
556
+ """List all APIs"""
557
+ config = load_config()
558
+ apis = config.get("apis", [])
559
+
560
+ # Filter deleted if needed
561
+ if not include_deleted:
562
+ apis = [a for a in apis if not a.get("deleted", False)]
563
+
564
+ return apis
565
+
566
+ @router.post("/apis")
567
+ async def create_api(api: APICreate, username: str = Depends(verify_token)):
568
+ """Create new API"""
569
+ config = load_config()
570
+
571
+ # Check duplicate name
572
+ existing = [a for a in config.get("apis", []) if a["name"] == api.name]
573
+ if existing:
574
+ raise HTTPException(status_code=400, detail="API name already exists")
575
+
576
+ # Create API
577
+ new_api = api.dict()
578
+ new_api["deleted"] = False
579
+ new_api["created_date"] = get_timestamp()
580
+ new_api["created_by"] = username
581
+ new_api["last_update_date"] = get_timestamp()
582
+ new_api["last_update_user"] = username
583
+
584
+ if "apis" not in config:
585
+ config["apis"] = []
586
+ config["apis"].append(new_api)
587
+
588
+ # Add activity log
589
+ add_activity_log(config, username, "CREATE_API", "api", api.name, api.name)
590
+
591
+ # Save
592
+ save_config(config)
593
+
594
+ log(f"✅ API '{api.name}' created by {username}")
595
+ return new_api
596
+
597
+ @router.put("/apis/{api_name}")
598
+ async def update_api(
599
+ api_name: str,
600
+ update: APIUpdate,
601
+ username: str = Depends(verify_token)
602
+ ):
603
+ """Update API"""
604
+ config = load_config()
605
+
606
+ # Find API
607
+ api = next((a for a in config.get("apis", []) if a["name"] == api_name), None)
608
+ if not api:
609
+ raise HTTPException(status_code=404, detail="API not found")
610
+
611
+ # Check race condition
612
+ if api.get("last_update_date") != update.last_update_date:
613
+ raise HTTPException(status_code=409, detail="API was modified by another user")
614
+
615
+ # Check if API is in use
616
+ for project in config.get("projects", []):
617
+ for version in project.get("versions", []):
618
+ for intent in version.get("intents", []):
619
+ if intent.get("action") == api_name and version.get("published", False):
620
+ raise HTTPException(status_code=400,
621
+ detail=f"API is used in published version of project '{project['name']}'")
622
+
623
+ # Update
624
+ update_dict = update.dict()
625
+ del update_dict["last_update_date"]
626
+ api.update(update_dict)
627
+ api["last_update_date"] = get_timestamp()
628
+ api["last_update_user"] = username
629
+
630
+ # Add activity log
631
+ add_activity_log(config, username, "UPDATE_API", "api", api_name, api_name)
632
+
633
+ # Save
634
+ save_config(config)
635
+
636
+ log(f"✅ API '{api_name}' updated by {username}")
637
+ return api
638
+
639
+ @router.delete("/apis/{api_name}")
640
+ async def delete_api(api_name: str, username: str = Depends(verify_token)):
641
+ """Delete API (soft delete)"""
642
+ config = load_config()
643
+
644
+ # Find API
645
+ api = next((a for a in config.get("apis", []) if a["name"] == api_name), None)
646
+ if not api:
647
+ raise HTTPException(status_code=404, detail="API not found")
648
+
649
+ # Check if API is in use
650
+ for project in config.get("projects", []):
651
+ for version in project.get("versions", []):
652
+ for intent in version.get("intents", []):
653
+ if intent.get("action") == api_name:
654
+ raise HTTPException(status_code=400,
655
+ detail=f"API is used in project '{project['name']}'")
656
+
657
+ # Soft delete
658
+ api["deleted"] = True
659
+ api["last_update_date"] = get_timestamp()
660
+ api["last_update_user"] = username
661
+
662
+ # Add activity log
663
+ add_activity_log(config, username, "DELETE_API", "api", api_name, api_name)
664
+
665
+ # Save
666
+ save_config(config)
667
+
668
+ log(f"✅ API '{api_name}' deleted by {username}")
669
+ return {"success": True}
670
+
671
+ # ===================== Test Endpoints =====================
672
+ @router.post("/apis/test")
673
+ async def test_api(api: APICreate, username: str = Depends(verify_token)):
674
+ """Test API endpoint"""
675
+ import requests
676
+
677
+ try:
678
+ # Prepare request
679
+ headers = api.headers.copy()
680
+
681
+ # Make request
682
+ response = requests.request(
683
+ method=api.method,
684
+ url=api.url,
685
+ headers=headers,
686
+ json=api.body_template if api.method in ["POST", "PUT", "PATCH"] else None,
687
+ params=api.body_template if api.method == "GET" else None,
688
+ timeout=api.timeout_seconds
689
+ )
690
+
691
+ return {
692
+ "success": True,
693
+ "status_code": response.status_code,
694
+ "response_time_ms": int(response.elapsed.total_seconds() * 1000),
695
+ "headers": dict(response.headers),
696
+ "body": response.text[:1000] # First 1000 chars
697
+ }
698
+ except Exception as e:
699
+ return {
700
+ "success": False,
701
+ "error": str(e)
702
+ }
703
+
704
+ @router.get("/activity-log")
705
+ async def get_activity_log(
706
+ limit: int = 50,
707
+ username: str = Depends(verify_token)
708
+ ):
709
+ """Get activity log"""
710
+ config = load_config()
711
+ logs = config.get("activity_log", [])
712
+
713
+ # Return last N entries
714
+ return logs[-limit:]
715
+
716
+ @router.post("/test/run-all")
717
+ async def run_all_tests(test: TestRequest, username: str = Depends(verify_token)):
718
+ """Run all tests"""
719
+ # This is a placeholder - in real implementation, this would run actual tests
720
+ log(f"🧪 Running {test.test_type} tests requested by {username}")
721
+
722
+ # Simulate test results
723
+ results = {
724
+ "test_type": test.test_type,
725
+ "start_time": get_timestamp(),
726
+ "tests": [
727
+ {"name": "Login with valid credentials", "status": "PASS", "duration_ms": 120},
728
+ {"name": "Create new project", "status": "PASS", "duration_ms": 340},
729
+ {"name": "Delete API in use", "status": "PASS", "duration_ms": 45},
730
+ {"name": "Race condition detection", "status": "PASS", "duration_ms": 567},
731
+ {"name": "Invalid token handling", "status": "PASS", "duration_ms": 23}
732
+ ],
733
+ "summary": {
734
+ "total": 5,
735
+ "passed": 5,
736
+ "failed": 0,
737
+ "duration_ms": 1095
738
+ }
739
+ }
740
+
741
+ return results
742
+
743
+ @router.post("/validate/regex")
744
+ async def validate_regex(pattern: str, test_value: str, username: str = Depends(verify_token)):
745
+ """Validate regex pattern"""
746
+ import re
747
+
748
+ try:
749
+ compiled = re.compile(pattern)
750
+ match = compiled.fullmatch(test_value)
751
+ return {
752
+ "valid": True,
753
+ "matches": match is not None
754
+ }
755
+ except re.error as e:
756
+ return {
757
+ "valid": False,
758
+ "error": str(e)
759
+ }
760
+
761
+ # ===================== Export/Import =====================
762
+ @router.get("/projects/{project_id}/export")
763
+ async def export_project(project_id: int, username: str = Depends(verify_token)):
764
+ """Export project as JSON"""
765
+ config = load_config()
766
+
767
+ # Find project
768
+ project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
769
+ if not project:
770
+ raise HTTPException(status_code=404, detail="Project not found")
771
+
772
+ # Collect all related APIs
773
+ api_names = set()
774
+ for version in project.get("versions", []):
775
+ for intent in version.get("intents", []):
776
+ api_names.add(intent.get("action"))
777
+
778
+ apis = [a for a in config.get("apis", []) if a["name"] in api_names]
779
+
780
+ # Create export
781
+ export_data = {
782
+ "export_date": get_timestamp(),
783
+ "exported_by": username,
784
+ "project": project,
785
+ "apis": apis
786
+ }
787
+
788
+ log(f"📤 Project '{project['name']}' exported by {username}")
789
+ return export_data
790
+
791
+ @router.post("/projects/import")
792
+ async def import_project(data: Dict[str, Any], username: str = Depends(verify_token)):
793
+ """Import project from JSON"""
794
+ config = load_config()
795
+
796
+ # Extract project and APIs
797
+ project_data = data.get("project", {})
798
+ apis_data = data.get("apis", [])
799
+
800
+ # Check duplicate project name
801
+ existing = [p for p in config.get("projects", []) if p["name"] == project_data.get("name")]
802
+ if existing:
803
+ # Generate new name
804
+ project_data["name"] = f"{project_data['name']}_imported_{int(datetime.now().timestamp())}"
805
+
806
+ # Generate new IDs
807
+ config["config"]["project_id_counter"] = config["config"].get("project_id_counter", 0) + 1
808
+ project_data["id"] = config["config"]["project_id_counter"]
809
+
810
+ # Reset version IDs
811
+ version_counter = 0
812
+ for version in project_data.get("versions", []):
813
+ version_counter += 1
814
+ version["id"] = version_counter
815
+ version["published"] = False # Imported versions are unpublished
816
+
817
+ project_data["version_id_counter"] = version_counter
818
+ project_data["created_date"] = get_timestamp()
819
+ project_data["created_by"] = username
820
+ project_data["last_update_date"] = get_timestamp()
821
+ project_data["last_update_user"] = username
822
+
823
+ # Import APIs
824
+ imported_apis = []
825
+ for api_data in apis_data:
826
+ # Check if API already exists
827
+ existing_api = next((a for a in config.get("apis", []) if a["name"] == api_data.get("name")), None)
828
+ if not existing_api:
829
+ api_data["created_date"] = get_timestamp()
830
+ api_data["created_by"] = username
831
+ api_data["last_update_date"] = get_timestamp()
832
+ api_data["last_update_user"] = username
833
+ api_data["deleted"] = False
834
+ config["apis"].append(api_data)
835
+ imported_apis.append(api_data["name"])
836
+
837
+ # Add project
838
+ config["projects"].append(project_data)
839
+
840
+ # Add activity log
841
+ add_activity_log(config, username, "IMPORT_PROJECT", "project", project_data["id"],
842
+ project_data["name"], f"Imported with {len(imported_apis)} APIs")
843
+
844
+ # Save
845
+ save_config(config)
846
+
847
+ log(f"📥 Project '{project_data['name']}' imported by {username}")
848
+ return {
849
+ "success": True,
850
+ "project_name": project_data["name"],
851
+ "project_id": project_data["id"],
852
+ "imported_apis": imported_apis
853
+ }