Spaces:
Paused
Paused
| """Admin API routes""" | |
| from fastapi import APIRouter, Depends, HTTPException, Header | |
| from fastapi.responses import JSONResponse | |
| from pydantic import BaseModel | |
| from typing import Optional, List | |
| import secrets | |
| from ..core.auth import AuthManager | |
| from ..core.database import Database | |
| from ..services.token_manager import TokenManager | |
| from ..services.proxy_manager import ProxyManager | |
| router = APIRouter() | |
| # Dependency injection | |
| token_manager: TokenManager = None | |
| proxy_manager: ProxyManager = None | |
| db: Database = None | |
| # Store active admin session tokens (in production, use Redis or database) | |
| active_admin_tokens = set() | |
| def set_dependencies(tm: TokenManager, pm: ProxyManager, database: Database): | |
| """Set service instances""" | |
| global token_manager, proxy_manager, db | |
| token_manager = tm | |
| proxy_manager = pm | |
| db = database | |
| # ========== Request Models ========== | |
| class LoginRequest(BaseModel): | |
| username: str | |
| password: str | |
| class AddTokenRequest(BaseModel): | |
| st: str | |
| project_id: Optional[str] = None # 用户可选输入project_id | |
| project_name: Optional[str] = None | |
| remark: Optional[str] = None | |
| image_enabled: bool = True | |
| video_enabled: bool = True | |
| image_concurrency: int = -1 | |
| video_concurrency: int = -1 | |
| class UpdateTokenRequest(BaseModel): | |
| st: str # Session Token (必填,用于刷新AT) | |
| project_id: Optional[str] = None # 用户可选输入project_id | |
| project_name: Optional[str] = None | |
| remark: Optional[str] = None | |
| image_enabled: Optional[bool] = None | |
| video_enabled: Optional[bool] = None | |
| image_concurrency: Optional[int] = None | |
| video_concurrency: Optional[int] = None | |
| class ProxyConfigRequest(BaseModel): | |
| proxy_enabled: bool | |
| proxy_url: Optional[str] = None | |
| class GenerationConfigRequest(BaseModel): | |
| image_timeout: int | |
| video_timeout: int | |
| class ChangePasswordRequest(BaseModel): | |
| old_password: str | |
| new_password: str | |
| class UpdateAPIKeyRequest(BaseModel): | |
| new_api_key: str | |
| class UpdateDebugConfigRequest(BaseModel): | |
| enabled: bool | |
| class UpdateAdminConfigRequest(BaseModel): | |
| error_ban_threshold: int | |
| class ST2ATRequest(BaseModel): | |
| """ST转AT请求""" | |
| st: str | |
| # ========== Auth Middleware ========== | |
| async def verify_admin_token(authorization: str = Header(None)): | |
| """Verify admin session token (NOT API key)""" | |
| if not authorization or not authorization.startswith("Bearer "): | |
| raise HTTPException(status_code=401, detail="Missing authorization") | |
| token = authorization[7:] | |
| # Check if token is in active session tokens | |
| if token not in active_admin_tokens: | |
| raise HTTPException(status_code=401, detail="Invalid or expired admin token") | |
| return token | |
| # ========== Auth Endpoints ========== | |
| async def admin_login(request: LoginRequest): | |
| """Admin login - returns session token (NOT API key)""" | |
| admin_config = await db.get_admin_config() | |
| if not AuthManager.verify_admin(request.username, request.password): | |
| raise HTTPException(status_code=401, detail="Invalid credentials") | |
| # Generate independent session token | |
| session_token = f"admin-{secrets.token_urlsafe(32)}" | |
| # Store in active tokens | |
| active_admin_tokens.add(session_token) | |
| return { | |
| "success": True, | |
| "token": session_token, # Session token (NOT API key) | |
| "username": admin_config.username | |
| } | |
| async def admin_logout(token: str = Depends(verify_admin_token)): | |
| """Admin logout - invalidate session token""" | |
| active_admin_tokens.discard(token) | |
| return {"success": True, "message": "退出登录成功"} | |
| async def change_password( | |
| request: ChangePasswordRequest, | |
| token: str = Depends(verify_admin_token) | |
| ): | |
| """Change admin password""" | |
| admin_config = await db.get_admin_config() | |
| # Verify old password | |
| if not AuthManager.verify_admin(admin_config.username, request.old_password): | |
| raise HTTPException(status_code=400, detail="旧密码错误") | |
| # Update password in database | |
| await db.update_admin_config(password=request.new_password) | |
| # 🔥 Hot reload: sync database config to memory | |
| await db.reload_config_to_memory() | |
| # 🔑 Invalidate all admin session tokens (force re-login for security) | |
| active_admin_tokens.clear() | |
| return {"success": True, "message": "密码修改成功,请重新登录"} | |
| # ========== Token Management ========== | |
| async def get_tokens(token: str = Depends(verify_admin_token)): | |
| """Get all tokens with statistics""" | |
| tokens = await token_manager.get_all_tokens() | |
| result = [] | |
| for t in tokens: | |
| stats = await db.get_token_stats(t.id) | |
| result.append({ | |
| "id": t.id, | |
| "st": t.st, # Session Token for editing | |
| "at": t.at, # Access Token for editing (从ST转换而来) | |
| "at_expires": t.at_expires.isoformat() if t.at_expires else None, # 🆕 AT过期时间 | |
| "token": t.at, # 兼容前端 token.token 的访问方式 | |
| "email": t.email, | |
| "name": t.name, | |
| "remark": t.remark, | |
| "is_active": t.is_active, | |
| "created_at": t.created_at.isoformat() if t.created_at else None, | |
| "last_used_at": t.last_used_at.isoformat() if t.last_used_at else None, | |
| "use_count": t.use_count, | |
| "credits": t.credits, # 🆕 余额 | |
| "user_paygate_tier": t.user_paygate_tier, | |
| "current_project_id": t.current_project_id, # 🆕 项目ID | |
| "current_project_name": t.current_project_name, # 🆕 项目名称 | |
| "image_enabled": t.image_enabled, | |
| "video_enabled": t.video_enabled, | |
| "image_concurrency": t.image_concurrency, | |
| "video_concurrency": t.video_concurrency, | |
| "image_count": stats.image_count if stats else 0, | |
| "video_count": stats.video_count if stats else 0, | |
| "error_count": stats.error_count if stats else 0 | |
| }) | |
| return result # 直接返回数组,兼容前端 | |
| async def add_token( | |
| request: AddTokenRequest, | |
| token: str = Depends(verify_admin_token) | |
| ): | |
| """Add a new token""" | |
| try: | |
| new_token = await token_manager.add_token( | |
| st=request.st, | |
| project_id=request.project_id, # 🆕 支持用户指定project_id | |
| project_name=request.project_name, | |
| remark=request.remark, | |
| image_enabled=request.image_enabled, | |
| video_enabled=request.video_enabled, | |
| image_concurrency=request.image_concurrency, | |
| video_concurrency=request.video_concurrency | |
| ) | |
| return { | |
| "success": True, | |
| "message": "Token添加成功", | |
| "token": { | |
| "id": new_token.id, | |
| "email": new_token.email, | |
| "credits": new_token.credits, | |
| "project_id": new_token.current_project_id, | |
| "project_name": new_token.current_project_name | |
| } | |
| } | |
| except ValueError as e: | |
| raise HTTPException(status_code=400, detail=str(e)) | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"添加Token失败: {str(e)}") | |
| async def update_token( | |
| token_id: int, | |
| request: UpdateTokenRequest, | |
| token: str = Depends(verify_admin_token) | |
| ): | |
| """Update token - 使用ST自动刷新AT""" | |
| try: | |
| # 先ST转AT | |
| result = await token_manager.flow_client.st_to_at(request.st) | |
| at = result["access_token"] | |
| expires = result.get("expires") | |
| # 解析过期时间 | |
| from datetime import datetime | |
| at_expires = None | |
| if expires: | |
| try: | |
| at_expires = datetime.fromisoformat(expires.replace('Z', '+00:00')) | |
| except: | |
| pass | |
| # 更新token (包含AT、ST、AT过期时间、project_id和project_name) | |
| await token_manager.update_token( | |
| token_id=token_id, | |
| st=request.st, | |
| at=at, | |
| at_expires=at_expires, # 🆕 更新AT过期时间 | |
| project_id=request.project_id, | |
| project_name=request.project_name, | |
| remark=request.remark, | |
| image_enabled=request.image_enabled, | |
| video_enabled=request.video_enabled, | |
| image_concurrency=request.image_concurrency, | |
| video_concurrency=request.video_concurrency | |
| ) | |
| return {"success": True, "message": "Token更新成功"} | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def delete_token( | |
| token_id: int, | |
| token: str = Depends(verify_admin_token) | |
| ): | |
| """Delete token""" | |
| try: | |
| await token_manager.delete_token(token_id) | |
| return {"success": True, "message": "Token删除成功"} | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def enable_token( | |
| token_id: int, | |
| token: str = Depends(verify_admin_token) | |
| ): | |
| """Enable token""" | |
| await token_manager.enable_token(token_id) | |
| return {"success": True, "message": "Token已启用"} | |
| async def disable_token( | |
| token_id: int, | |
| token: str = Depends(verify_admin_token) | |
| ): | |
| """Disable token""" | |
| await token_manager.disable_token(token_id) | |
| return {"success": True, "message": "Token已禁用"} | |
| async def refresh_credits( | |
| token_id: int, | |
| token: str = Depends(verify_admin_token) | |
| ): | |
| """刷新Token余额 🆕""" | |
| try: | |
| credits = await token_manager.refresh_credits(token_id) | |
| return { | |
| "success": True, | |
| "message": "余额刷新成功", | |
| "credits": credits | |
| } | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"刷新余额失败: {str(e)}") | |
| async def refresh_at( | |
| token_id: int, | |
| token: str = Depends(verify_admin_token) | |
| ): | |
| """手动刷新Token的AT (使用ST转换) 🆕""" | |
| try: | |
| # 调用token_manager的内部刷新方法 | |
| success = await token_manager._refresh_at(token_id) | |
| if success: | |
| # 获取更新后的token信息 | |
| updated_token = await token_manager.get_token(token_id) | |
| return { | |
| "success": True, | |
| "message": "AT刷新成功", | |
| "token": { | |
| "id": updated_token.id, | |
| "email": updated_token.email, | |
| "at_expires": updated_token.at_expires.isoformat() if updated_token.at_expires else None | |
| } | |
| } | |
| else: | |
| raise HTTPException(status_code=500, detail="AT刷新失败") | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"刷新AT失败: {str(e)}") | |
| async def st_to_at( | |
| request: ST2ATRequest, | |
| token: str = Depends(verify_admin_token) | |
| ): | |
| """Convert Session Token to Access Token (仅转换,不添加到数据库)""" | |
| try: | |
| result = await token_manager.flow_client.st_to_at(request.st) | |
| return { | |
| "success": True, | |
| "message": "ST converted to AT successfully", | |
| "access_token": result["access_token"], | |
| "email": result.get("user", {}).get("email"), | |
| "expires": result.get("expires") | |
| } | |
| except Exception as e: | |
| raise HTTPException(status_code=400, detail=str(e)) | |
| # ========== Config Management ========== | |
| async def get_proxy_config(token: str = Depends(verify_admin_token)): | |
| """Get proxy configuration""" | |
| config = await proxy_manager.get_proxy_config() | |
| return { | |
| "success": True, | |
| "config": { | |
| "enabled": config.enabled, | |
| "proxy_url": config.proxy_url | |
| } | |
| } | |
| async def get_proxy_config_alias(token: str = Depends(verify_admin_token)): | |
| """Get proxy configuration (alias for frontend compatibility)""" | |
| config = await proxy_manager.get_proxy_config() | |
| return { | |
| "proxy_enabled": config.enabled, # Frontend expects proxy_enabled | |
| "proxy_url": config.proxy_url | |
| } | |
| async def update_proxy_config_alias( | |
| request: ProxyConfigRequest, | |
| token: str = Depends(verify_admin_token) | |
| ): | |
| """Update proxy configuration (alias for frontend compatibility)""" | |
| await proxy_manager.update_proxy_config(request.proxy_enabled, request.proxy_url) | |
| return {"success": True, "message": "代理配置更新成功"} | |
| async def update_proxy_config( | |
| request: ProxyConfigRequest, | |
| token: str = Depends(verify_admin_token) | |
| ): | |
| """Update proxy configuration""" | |
| await proxy_manager.update_proxy_config(request.proxy_enabled, request.proxy_url) | |
| return {"success": True, "message": "代理配置更新成功"} | |
| async def get_generation_config(token: str = Depends(verify_admin_token)): | |
| """Get generation timeout configuration""" | |
| config = await db.get_generation_config() | |
| return { | |
| "success": True, | |
| "config": { | |
| "image_timeout": config.image_timeout, | |
| "video_timeout": config.video_timeout | |
| } | |
| } | |
| async def update_generation_config( | |
| request: GenerationConfigRequest, | |
| token: str = Depends(verify_admin_token) | |
| ): | |
| """Update generation timeout configuration""" | |
| await db.update_generation_config(request.image_timeout, request.video_timeout) | |
| # 🔥 Hot reload: sync database config to memory | |
| await db.reload_config_to_memory() | |
| return {"success": True, "message": "生成配置更新成功"} | |
| # ========== System Info ========== | |
| async def get_system_info(token: str = Depends(verify_admin_token)): | |
| """Get system information""" | |
| tokens = await token_manager.get_all_tokens() | |
| active_tokens = [t for t in tokens if t.is_active] | |
| total_credits = sum(t.credits for t in active_tokens) | |
| return { | |
| "success": True, | |
| "info": { | |
| "total_tokens": len(tokens), | |
| "active_tokens": len(active_tokens), | |
| "total_credits": total_credits, | |
| "version": "1.0.0" | |
| } | |
| } | |
| # ========== Additional Routes for Frontend Compatibility ========== | |
| async def login(request: LoginRequest): | |
| """Login endpoint (alias for /api/admin/login)""" | |
| return await admin_login(request) | |
| async def logout(token: str = Depends(verify_admin_token)): | |
| """Logout endpoint (alias for /api/admin/logout)""" | |
| return await admin_logout(token) | |
| async def get_stats(token: str = Depends(verify_admin_token)): | |
| """Get statistics for dashboard""" | |
| tokens = await token_manager.get_all_tokens() | |
| active_tokens = [t for t in tokens if t.is_active] | |
| # Calculate totals | |
| total_images = 0 | |
| total_videos = 0 | |
| total_errors = 0 | |
| today_images = 0 | |
| today_videos = 0 | |
| today_errors = 0 | |
| for t in tokens: | |
| stats = await db.get_token_stats(t.id) | |
| if stats: | |
| total_images += stats.image_count | |
| total_videos += stats.video_count | |
| total_errors += stats.error_count | |
| today_images += stats.today_image_count | |
| today_videos += stats.today_video_count | |
| today_errors += stats.today_error_count | |
| return { | |
| "total_tokens": len(tokens), | |
| "active_tokens": len(active_tokens), | |
| "total_images": total_images, | |
| "total_videos": total_videos, | |
| "total_errors": total_errors, | |
| "today_images": today_images, | |
| "today_videos": today_videos, | |
| "today_errors": today_errors | |
| } | |
| async def get_logs( | |
| limit: int = 100, | |
| token: str = Depends(verify_admin_token) | |
| ): | |
| """Get request logs with token email""" | |
| logs = await db.get_logs(limit=limit) | |
| return [{ | |
| "id": log.get("id"), | |
| "token_id": log.get("token_id"), | |
| "token_email": log.get("token_email"), | |
| "token_username": log.get("token_username"), | |
| "operation": log.get("operation"), | |
| "status_code": log.get("status_code"), | |
| "duration": log.get("duration"), | |
| "created_at": log.get("created_at") | |
| } for log in logs] | |
| async def get_admin_config(token: str = Depends(verify_admin_token)): | |
| """Get admin configuration""" | |
| from ..core.config import config | |
| admin_config = await db.get_admin_config() | |
| return { | |
| "admin_username": admin_config.username, | |
| "api_key": admin_config.api_key, | |
| "error_ban_threshold": admin_config.error_ban_threshold, | |
| "debug_enabled": config.debug_enabled # Return actual debug status | |
| } | |
| async def update_admin_config( | |
| request: UpdateAdminConfigRequest, | |
| token: str = Depends(verify_admin_token) | |
| ): | |
| """Update admin configuration (error_ban_threshold)""" | |
| # Update error_ban_threshold in database | |
| await db.update_admin_config(error_ban_threshold=request.error_ban_threshold) | |
| return {"success": True, "message": "配置更新成功"} | |
| async def update_admin_password( | |
| request: ChangePasswordRequest, | |
| token: str = Depends(verify_admin_token) | |
| ): | |
| """Update admin password""" | |
| return await change_password(request, token) | |
| async def update_api_key( | |
| request: UpdateAPIKeyRequest, | |
| token: str = Depends(verify_admin_token) | |
| ): | |
| """Update API key (for external API calls, NOT for admin login)""" | |
| # Update API key in database | |
| await db.update_admin_config(api_key=request.new_api_key) | |
| # 🔥 Hot reload: sync database config to memory | |
| await db.reload_config_to_memory() | |
| return {"success": True, "message": "API Key更新成功"} | |
| async def update_debug_config( | |
| request: UpdateDebugConfigRequest, | |
| token: str = Depends(verify_admin_token) | |
| ): | |
| """Update debug configuration""" | |
| try: | |
| # Update debug config in database | |
| await db.update_debug_config(enabled=request.enabled) | |
| # 🔥 Hot reload: sync database config to memory | |
| await db.reload_config_to_memory() | |
| status = "enabled" if request.enabled else "disabled" | |
| return {"success": True, "message": f"Debug mode {status}", "enabled": request.enabled} | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Failed to update debug config: {str(e)}") | |
| async def get_generation_timeout(token: str = Depends(verify_admin_token)): | |
| """Get generation timeout configuration""" | |
| return await get_generation_config(token) | |
| async def update_generation_timeout( | |
| request: GenerationConfigRequest, | |
| token: str = Depends(verify_admin_token) | |
| ): | |
| """Update generation timeout configuration""" | |
| await db.update_generation_config(request.image_timeout, request.video_timeout) | |
| # 🔥 Hot reload: sync database config to memory | |
| await db.reload_config_to_memory() | |
| return {"success": True, "message": "生成配置更新成功"} | |
| # ========== AT Auto Refresh Config ========== | |
| async def get_token_refresh_config(token: str = Depends(verify_admin_token)): | |
| """Get AT auto refresh configuration (默认启用)""" | |
| return { | |
| "success": True, | |
| "config": { | |
| "at_auto_refresh_enabled": True # Flow2API默认启用AT自动刷新 | |
| } | |
| } | |
| async def update_token_refresh_enabled( | |
| token: str = Depends(verify_admin_token) | |
| ): | |
| """Update AT auto refresh enabled (Flow2API固定启用,此接口仅用于前端兼容)""" | |
| return { | |
| "success": True, | |
| "message": "Flow2API的AT自动刷新默认启用且无法关闭" | |
| } | |
| # ========== Cache Configuration Endpoints ========== | |
| async def get_cache_config(token: str = Depends(verify_admin_token)): | |
| """Get cache configuration""" | |
| cache_config = await db.get_cache_config() | |
| # Calculate effective base URL | |
| effective_base_url = cache_config.cache_base_url if cache_config.cache_base_url else f"http://127.0.0.1:8000" | |
| return { | |
| "success": True, | |
| "config": { | |
| "enabled": cache_config.cache_enabled, | |
| "timeout": cache_config.cache_timeout, | |
| "base_url": cache_config.cache_base_url or "", | |
| "effective_base_url": effective_base_url | |
| } | |
| } | |
| async def update_cache_enabled( | |
| request: dict, | |
| token: str = Depends(verify_admin_token) | |
| ): | |
| """Update cache enabled status""" | |
| enabled = request.get("enabled", False) | |
| await db.update_cache_config(enabled=enabled) | |
| # 🔥 Hot reload: sync database config to memory | |
| await db.reload_config_to_memory() | |
| return {"success": True, "message": f"缓存已{'启用' if enabled else '禁用'}"} | |
| async def update_cache_config_full( | |
| request: dict, | |
| token: str = Depends(verify_admin_token) | |
| ): | |
| """Update complete cache configuration""" | |
| enabled = request.get("enabled") | |
| timeout = request.get("timeout") | |
| base_url = request.get("base_url") | |
| await db.update_cache_config(enabled=enabled, timeout=timeout, base_url=base_url) | |
| # 🔥 Hot reload: sync database config to memory | |
| await db.reload_config_to_memory() | |
| return {"success": True, "message": "缓存配置更新成功"} | |
| async def update_cache_base_url( | |
| request: dict, | |
| token: str = Depends(verify_admin_token) | |
| ): | |
| """Update cache base URL""" | |
| base_url = request.get("base_url", "") | |
| await db.update_cache_config(base_url=base_url) | |
| # 🔥 Hot reload: sync database config to memory | |
| await db.reload_config_to_memory() | |
| return {"success": True, "message": "缓存Base URL更新成功"} | |