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

313 lines
9.7 KiB
Python

from fastapi import FastAPI, Request, Depends, HTTPException, status
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
import os
import time
import psutil
from datetime import datetime, timedelta
# 导入数据库模型
from app.models.database import create_tables, get_db, APIKey, ProjectMapping, TriggerLog
from app.auth.middleware import auth_middleware, get_current_user
from app.config import settings
# 创建 FastAPI 应用
app = FastAPI(
title="Gitea Webhook Ambassador",
description="高性能的 Gitea 到 Jenkins 的 Webhook 服务",
version="2.0.0"
)
# 添加 CORS 中间件
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 创建数据库表
create_tables()
# 挂载静态文件
app.mount("/static", StaticFiles(directory="app/static"), name="static")
# 设置模板
templates = Jinja2Templates(directory="app/templates")
# 启动时间
start_time = datetime.now()
@app.get("/", response_class=HTMLResponse)
async def root(request: Request):
"""根路径 - 重定向到登录页"""
return RedirectResponse(url="/login")
@app.get("/login", response_class=HTMLResponse)
async def login_page(request: Request):
"""登录页面"""
return templates.TemplateResponse("login.html", {"request": request})
@app.get("/dashboard", response_class=HTMLResponse)
async def dashboard_page(request: Request):
"""仪表板页面"""
return templates.TemplateResponse("dashboard.html", {"request": request})
@app.post("/api/auth/login")
async def login(request: dict):
"""管理员登录"""
admin_key = os.getenv("ADMIN_SECRET_KEY", "admin-secret-key-change-in-production")
if request.get("secret_key") != admin_key:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid secret key"
)
# 生成 JWT 令牌
token = auth_middleware.create_access_token(
data={"sub": "admin", "role": "admin"}
)
return {"token": token}
@app.get("/api/stats")
async def get_stats(db: Session = Depends(get_db), current_user: dict = Depends(get_current_user)):
"""获取统计信息"""
try:
# 获取项目总数
total_projects = db.query(ProjectMapping).count()
# 获取 API 密钥总数
total_api_keys = db.query(APIKey).count()
# 获取今日触发次数
today = datetime.now().date()
today_triggers = db.query(TriggerLog).filter(
TriggerLog.created_at >= today
).count()
# 获取成功触发次数
successful_triggers = db.query(TriggerLog).filter(
TriggerLog.status == "success"
).count()
return {
"total_projects": total_projects,
"total_api_keys": total_api_keys,
"today_triggers": today_triggers,
"successful_triggers": successful_triggers
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"获取统计信息失败: {str(e)}")
@app.get("/api/keys", response_model=dict)
async def list_api_keys(db: Session = Depends(get_db), current_user: dict = Depends(get_current_user)):
"""获取所有 API 密钥(兼容前端)"""
try:
keys = db.query(APIKey).order_by(APIKey.created_at.desc()).all()
return {
"keys": [
{
"id": key.id,
"key": key.key,
"description": key.description,
"created_at": key.created_at.isoformat()
}
for key in keys
]
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"获取 API 密钥失败: {str(e)}")
@app.post("/api/keys", response_model=dict)
async def create_api_key(
request: dict,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""创建新的 API 密钥(兼容前端)"""
try:
# 生成新的 API 密钥
api_key_value = auth_middleware.generate_api_key()
# 保存到数据库
db_key = APIKey(
key=api_key_value,
description=request.get("description", "")
)
db.add(db_key)
db.commit()
db.refresh(db_key)
return {
"id": db_key.id,
"key": db_key.key,
"description": db_key.description,
"created_at": db_key.created_at.isoformat()
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"创建 API 密钥失败: {str(e)}")
@app.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:
key = db.query(APIKey).filter(APIKey.id == key_id).first()
if not key:
raise HTTPException(status_code=404, detail="API 密钥不存在")
db.delete(key)
db.commit()
return {"message": "API 密钥删除成功"}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"删除 API 密钥失败: {str(e)}")
@app.get("/api/projects/", response_model=dict)
async def list_projects(db: Session = Depends(get_db), current_user: dict = Depends(get_current_user)):
"""获取所有项目(兼容前端)"""
try:
projects = db.query(ProjectMapping).order_by(ProjectMapping.created_at.desc()).all()
return {
"projects": [
{
"id": project.id,
"name": project.repository_name.split('/')[-1],
"jenkinsJob": project.default_job,
"giteaRepo": project.repository_name,
"created_at": project.created_at.isoformat()
}
for project in projects
]
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"获取项目列表失败: {str(e)}")
@app.post("/api/projects/", response_model=dict)
async def create_project(
request: dict,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""创建新项目(兼容前端)"""
try:
# 检查项目是否已存在
existing_project = db.query(ProjectMapping).filter(
ProjectMapping.repository_name == request["giteaRepo"]
).first()
if existing_project:
raise HTTPException(status_code=400, detail="项目已存在")
# 创建新项目
project = ProjectMapping(
repository_name=request["giteaRepo"],
default_job=request["jenkinsJob"]
)
db.add(project)
db.commit()
db.refresh(project)
return {
"id": project.id,
"name": request["name"],
"jenkinsJob": project.default_job,
"giteaRepo": project.repository_name,
"created_at": project.created_at.isoformat()
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"创建项目失败: {str(e)}")
@app.delete("/api/projects/{project_id}")
async def delete_project(
project_id: int,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""删除项目(兼容前端)"""
try:
project = db.query(ProjectMapping).filter(ProjectMapping.id == project_id).first()
if not project:
raise HTTPException(status_code=404, detail="项目不存在")
db.delete(project)
db.commit()
return {"message": "项目删除成功"}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"删除项目失败: {str(e)}")
@app.get("/health")
async def health_check():
"""健康检查端点"""
try:
# 计算运行时间
uptime = datetime.now() - start_time
uptime_str = str(uptime).split('.')[0] # 移除微秒
# 获取内存使用情况
process = psutil.Process()
memory_info = process.memory_info()
memory_mb = memory_info.rss / 1024 / 1024
return {
"status": "healthy",
"version": "2.0.0",
"uptime": uptime_str,
"memory": f"{memory_mb:.1f} MB",
"timestamp": datetime.now().isoformat()
}
except Exception as e:
return {
"status": "unhealthy",
"error": str(e),
"timestamp": datetime.now().isoformat()
}
@app.get("/api/logs")
async def get_logs(
startTime: str = None,
endTime: str = None,
level: str = None,
query: str = None,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""获取日志(简化版本)"""
try:
# 这里应该实现真正的日志查询逻辑
# 目前返回模拟数据
logs = [
{
"timestamp": datetime.now().isoformat(),
"level": "info",
"message": "系统运行正常"
}
]
return {"logs": logs}
except Exception as e:
raise HTTPException(status_code=500, detail=f"获取日志失败: {str(e)}")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)