mirror of
https://github.com/TheSmallHanCat/flow2api.git
synced 2026-05-06 22:13:48 +08:00
670 lines
20 KiB
Python
670 lines
20 KiB
Python
"""Admin API routes"""
|
||
from fastapi import APIRouter, Depends, HTTPException, Header
|
||
from fastapi.responses import JSONResponse
|
||
from pydantic import BaseModel
|
||
from typing import Optional, List
|
||
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
|
||
|
||
|
||
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 ST2ATRequest(BaseModel):
|
||
"""ST转AT请求"""
|
||
st: str
|
||
|
||
|
||
# ========== Auth Middleware ==========
|
||
|
||
async def verify_admin_token(authorization: str = Header(None)):
|
||
"""Verify admin token"""
|
||
if not authorization or not authorization.startswith("Bearer "):
|
||
raise HTTPException(status_code=401, detail="Missing authorization")
|
||
|
||
token = authorization[7:]
|
||
admin_config = await db.get_admin_config()
|
||
|
||
# Simple token verification: check if matches api_key
|
||
if token != admin_config.api_key:
|
||
raise HTTPException(status_code=401, detail="Invalid admin token")
|
||
|
||
return token
|
||
|
||
|
||
# ========== Auth Endpoints ==========
|
||
|
||
@router.post("/api/admin/login")
|
||
async def admin_login(request: LoginRequest):
|
||
"""Admin login"""
|
||
admin_config = await db.get_admin_config()
|
||
|
||
if not AuthManager.verify_admin(request.username, request.password):
|
||
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||
|
||
return {
|
||
"success": True,
|
||
"token": admin_config.api_key,
|
||
"username": admin_config.username
|
||
}
|
||
|
||
|
||
@router.post("/api/admin/change-password")
|
||
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
|
||
await db.update_admin_config(password=request.new_password)
|
||
|
||
return {"success": True, "message": "密码修改成功"}
|
||
|
||
|
||
# ========== Token Management ==========
|
||
|
||
@router.get("/api/tokens")
|
||
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 # 直接返回数组,兼容前端
|
||
|
||
|
||
@router.post("/api/tokens")
|
||
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)}")
|
||
|
||
|
||
@router.put("/api/tokens/{token_id}")
|
||
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))
|
||
|
||
|
||
@router.delete("/api/tokens/{token_id}")
|
||
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))
|
||
|
||
|
||
@router.post("/api/tokens/{token_id}/enable")
|
||
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已启用"}
|
||
|
||
|
||
@router.post("/api/tokens/{token_id}/disable")
|
||
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已禁用"}
|
||
|
||
|
||
@router.post("/api/tokens/{token_id}/refresh-credits")
|
||
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)}")
|
||
|
||
|
||
@router.post("/api/tokens/{token_id}/refresh-at")
|
||
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)}")
|
||
|
||
|
||
@router.post("/api/tokens/st2at")
|
||
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 ==========
|
||
|
||
@router.get("/api/config/proxy")
|
||
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
|
||
}
|
||
}
|
||
|
||
|
||
@router.get("/api/proxy/config")
|
||
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
|
||
}
|
||
|
||
|
||
@router.post("/api/proxy/config")
|
||
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": "代理配置更新成功"}
|
||
|
||
|
||
@router.post("/api/config/proxy")
|
||
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": "代理配置更新成功"}
|
||
|
||
|
||
@router.get("/api/config/generation")
|
||
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
|
||
}
|
||
}
|
||
|
||
|
||
@router.post("/api/config/generation")
|
||
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)
|
||
return {"success": True, "message": "生成配置更新成功"}
|
||
|
||
|
||
# ========== System Info ==========
|
||
|
||
@router.get("/api/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 ==========
|
||
|
||
@router.post("/api/login")
|
||
async def login(request: LoginRequest):
|
||
"""Login endpoint (alias for /api/admin/login)"""
|
||
return await admin_login(request)
|
||
|
||
|
||
@router.get("/api/stats")
|
||
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
|
||
}
|
||
|
||
|
||
@router.get("/api/logs")
|
||
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]
|
||
|
||
|
||
@router.get("/api/admin/config")
|
||
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": 3, # Default value
|
||
"debug_enabled": config.debug_enabled # Return actual debug status
|
||
}
|
||
|
||
|
||
@router.post("/api/admin/password")
|
||
async def update_admin_password(
|
||
request: ChangePasswordRequest,
|
||
token: str = Depends(verify_admin_token)
|
||
):
|
||
"""Update admin password"""
|
||
return await change_password(request, token)
|
||
|
||
|
||
@router.post("/api/admin/apikey")
|
||
async def update_api_key(
|
||
request: UpdateAPIKeyRequest,
|
||
token: str = Depends(verify_admin_token)
|
||
):
|
||
"""Update API key"""
|
||
await db.update_admin_config(api_key=request.new_api_key)
|
||
return {"success": True, "message": "API Key更新成功"}
|
||
|
||
|
||
@router.post("/api/admin/debug")
|
||
async def update_debug_config(
|
||
request: UpdateDebugConfigRequest,
|
||
token: str = Depends(verify_admin_token)
|
||
):
|
||
"""Update debug configuration"""
|
||
try:
|
||
# Import config instance
|
||
from ..core.config import config
|
||
|
||
# Update in-memory config
|
||
config.set_debug_enabled(request.enabled)
|
||
|
||
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)}")
|
||
|
||
|
||
@router.get("/api/generation/timeout")
|
||
async def get_generation_timeout(token: str = Depends(verify_admin_token)):
|
||
"""Get generation timeout configuration"""
|
||
return await get_generation_config(token)
|
||
|
||
|
||
@router.post("/api/generation/timeout")
|
||
async def update_generation_timeout(
|
||
request: GenerationConfigRequest,
|
||
token: str = Depends(verify_admin_token)
|
||
):
|
||
"""Update generation timeout configuration"""
|
||
return await update_generation_config(request, token)
|
||
|
||
|
||
# ========== AT Auto Refresh Config ==========
|
||
|
||
@router.get("/api/token-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自动刷新
|
||
}
|
||
}
|
||
|
||
|
||
@router.post("/api/token-refresh/enabled")
|
||
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 ==========
|
||
|
||
@router.get("/api/cache/config")
|
||
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
|
||
}
|
||
}
|
||
|
||
|
||
@router.post("/api/cache/enabled")
|
||
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)
|
||
|
||
# Update runtime config
|
||
from ..core.config import config
|
||
config.set_cache_enabled(enabled)
|
||
|
||
return {"success": True, "message": f"缓存已{'启用' if enabled else '禁用'}"}
|
||
|
||
|
||
@router.post("/api/cache/config")
|
||
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)
|
||
|
||
# Update runtime config
|
||
from ..core.config import config
|
||
if enabled is not None:
|
||
config.set_cache_enabled(enabled)
|
||
if timeout is not None:
|
||
config.set_cache_timeout(timeout)
|
||
if base_url is not None:
|
||
config.set_cache_base_url(base_url)
|
||
|
||
return {"success": True, "message": "缓存配置更新成功"}
|
||
|
||
|
||
@router.post("/api/cache/base-url")
|
||
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)
|
||
|
||
# Update runtime config
|
||
from ..core.config import config
|
||
config.set_cache_base_url(base_url)
|
||
|
||
return {"success": True, "message": "缓存Base URL更新成功"}
|