freeleaps-ops/apps/gitea-webhook-ambassador-python/app/handlers/admin.py
Nicolas f6c515157c feat: 添加 Python 版本的 Gitea Webhook Ambassador
- 新增完整的 Python 实现,替代 Go 版本
- 添加 Web 登录界面和仪表板
- 实现 JWT 认证和 API 密钥管理
- 添加数据库存储功能
- 保持与 Go 版本一致的目录结构和启动脚本
- 包含完整的文档和测试脚本
2025-07-20 21:17:10 +08:00

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)}")