- 新增完整的 Python 实现,替代 Go 版本 - 添加 Web 登录界面和仪表板 - 实现 JWT 认证和 API 密钥管理 - 添加数据库存储功能 - 保持与 Go 版本一致的目录结构和启动脚本 - 包含完整的文档和测试脚本
287 lines
8.5 KiB
Python
287 lines
8.5 KiB
Python
"""
|
|
管理 API 处理器
|
|
提供项目映射和 API 密钥管理功能
|
|
"""
|
|
|
|
import secrets
|
|
from datetime import datetime, timedelta
|
|
from typing import List, Optional
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.database import get_db
|
|
from app.models.api_key import APIKey
|
|
from app.models.project_mapping import ProjectMapping
|
|
from app.auth import get_current_user
|
|
|
|
router = APIRouter(prefix="/api/admin", tags=["admin"])
|
|
|
|
# API 密钥相关模型
|
|
class APIKeyResponse(BaseModel):
|
|
id: int
|
|
name: str
|
|
key_prefix: str
|
|
created_at: datetime
|
|
last_used: datetime
|
|
is_active: bool
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
class CreateAPIKeyRequest(BaseModel):
|
|
name: str
|
|
|
|
class CreateAPIKeyResponse(BaseModel):
|
|
id: int
|
|
name: str
|
|
key: str
|
|
created_at: datetime
|
|
|
|
# 项目映射相关模型
|
|
class ProjectMappingRequest(BaseModel):
|
|
repository_name: str
|
|
default_job: str
|
|
branch_jobs: Optional[List[dict]] = []
|
|
branch_patterns: Optional[List[dict]] = []
|
|
|
|
class ProjectMappingResponse(BaseModel):
|
|
id: int
|
|
repository_name: str
|
|
default_job: str
|
|
branch_jobs: List[dict]
|
|
branch_patterns: List[dict]
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
# API 密钥管理端点
|
|
@router.get("/api-keys", response_model=List[APIKeyResponse])
|
|
async def list_api_keys(
|
|
db: Session = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""列出所有 API 密钥"""
|
|
try:
|
|
api_keys = db.query(APIKey).order_by(APIKey.created_at.desc()).all()
|
|
return api_keys
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to list API keys: {str(e)}")
|
|
|
|
@router.post("/api-keys", response_model=CreateAPIKeyResponse)
|
|
async def create_api_key(
|
|
request: CreateAPIKeyRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""创建新的 API 密钥"""
|
|
try:
|
|
# 生成 API 密钥
|
|
api_key = secrets.token_urlsafe(32)
|
|
key_prefix = api_key[:8] # 显示前8位作为前缀
|
|
|
|
# 创建数据库记录
|
|
db_api_key = APIKey(
|
|
name=request.name,
|
|
key_hash=api_key, # 实际应用中应该哈希存储
|
|
key_prefix=key_prefix,
|
|
created_at=datetime.utcnow(),
|
|
last_used=datetime.utcnow(),
|
|
is_active=True
|
|
)
|
|
|
|
db.add(db_api_key)
|
|
db.commit()
|
|
db.refresh(db_api_key)
|
|
|
|
return CreateAPIKeyResponse(
|
|
id=db_api_key.id,
|
|
name=db_api_key.name,
|
|
key=api_key, # 只在创建时返回完整密钥
|
|
created_at=db_api_key.created_at
|
|
)
|
|
|
|
except Exception as e:
|
|
db.rollback()
|
|
raise HTTPException(status_code=500, detail=f"Failed to create API key: {str(e)}")
|
|
|
|
@router.delete("/api-keys/{key_id}")
|
|
async def delete_api_key(
|
|
key_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""删除 API 密钥"""
|
|
try:
|
|
api_key = db.query(APIKey).filter(APIKey.id == key_id).first()
|
|
if not api_key:
|
|
raise HTTPException(status_code=404, detail="API key not found")
|
|
|
|
db.delete(api_key)
|
|
db.commit()
|
|
|
|
return {"message": "API key deleted successfully"}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
raise HTTPException(status_code=500, detail=f"Failed to delete API key: {str(e)}")
|
|
|
|
@router.post("/api-keys/{key_id}/revoke")
|
|
async def revoke_api_key(
|
|
key_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""撤销 API 密钥"""
|
|
try:
|
|
api_key = db.query(APIKey).filter(APIKey.id == key_id).first()
|
|
if not api_key:
|
|
raise HTTPException(status_code=404, detail="API key not found")
|
|
|
|
api_key.is_active = False
|
|
db.commit()
|
|
|
|
return {"message": "API key revoked successfully"}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
raise HTTPException(status_code=500, detail=f"Failed to revoke API key: {str(e)}")
|
|
|
|
# 项目映射管理端点
|
|
@router.get("/projects", response_model=List[ProjectMappingResponse])
|
|
async def list_project_mappings(
|
|
db: Session = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""列出所有项目映射"""
|
|
try:
|
|
mappings = db.query(ProjectMapping).order_by(ProjectMapping.created_at.desc()).all()
|
|
return mappings
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to list project mappings: {str(e)}")
|
|
|
|
@router.post("/projects", response_model=ProjectMappingResponse)
|
|
async def create_project_mapping(
|
|
request: ProjectMappingRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""创建项目映射"""
|
|
try:
|
|
# 检查是否已存在
|
|
existing = db.query(ProjectMapping).filter(
|
|
ProjectMapping.repository_name == request.repository_name
|
|
).first()
|
|
|
|
if existing:
|
|
raise HTTPException(status_code=400, detail="Project mapping already exists")
|
|
|
|
# 创建新映射
|
|
mapping = ProjectMapping(
|
|
repository_name=request.repository_name,
|
|
default_job=request.default_job,
|
|
branch_jobs=request.branch_jobs or [],
|
|
branch_patterns=request.branch_patterns or [],
|
|
created_at=datetime.utcnow(),
|
|
updated_at=datetime.utcnow()
|
|
)
|
|
|
|
db.add(mapping)
|
|
db.commit()
|
|
db.refresh(mapping)
|
|
|
|
return mapping
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
raise HTTPException(status_code=500, detail=f"Failed to create project mapping: {str(e)}")
|
|
|
|
@router.get("/projects/{repository_name}", response_model=ProjectMappingResponse)
|
|
async def get_project_mapping(
|
|
repository_name: str,
|
|
db: Session = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""获取项目映射"""
|
|
try:
|
|
mapping = db.query(ProjectMapping).filter(
|
|
ProjectMapping.repository_name == repository_name
|
|
).first()
|
|
|
|
if not mapping:
|
|
raise HTTPException(status_code=404, detail="Project mapping not found")
|
|
|
|
return mapping
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to get project mapping: {str(e)}")
|
|
|
|
@router.delete("/projects/{repository_name}")
|
|
async def delete_project_mapping(
|
|
repository_name: str,
|
|
db: Session = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""删除项目映射"""
|
|
try:
|
|
mapping = db.query(ProjectMapping).filter(
|
|
ProjectMapping.repository_name == repository_name
|
|
).first()
|
|
|
|
if not mapping:
|
|
raise HTTPException(status_code=404, detail="Project mapping not found")
|
|
|
|
db.delete(mapping)
|
|
db.commit()
|
|
|
|
return {"message": "Project mapping deleted successfully"}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
raise HTTPException(status_code=500, detail=f"Failed to delete project mapping: {str(e)}")
|
|
|
|
# 统计信息端点
|
|
@router.get("/stats")
|
|
async def get_admin_stats(
|
|
db: Session = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""获取管理统计信息"""
|
|
try:
|
|
# API 密钥统计
|
|
total_keys = db.query(APIKey).count()
|
|
active_keys = db.query(APIKey).filter(APIKey.is_active == True).count()
|
|
|
|
# 最近使用的密钥
|
|
recent_keys = db.query(APIKey).filter(
|
|
APIKey.last_used >= datetime.utcnow() - timedelta(days=7)
|
|
).count()
|
|
|
|
# 项目映射统计
|
|
total_mappings = db.query(ProjectMapping).count()
|
|
|
|
return {
|
|
"api_keys": {
|
|
"total": total_keys,
|
|
"active": active_keys,
|
|
"recently_used": recent_keys
|
|
},
|
|
"project_mappings": {
|
|
"total": total_mappings
|
|
}
|
|
}
|
|
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to get admin stats: {str(e)}") |