- 新增完整的 Python 实现,替代 Go 版本 - 添加 Web 登录界面和仪表板 - 实现 JWT 认证和 API 密钥管理 - 添加数据库存储功能 - 保持与 Go 版本一致的目录结构和启动脚本 - 包含完整的文档和测试脚本
313 lines
9.7 KiB
Python
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) |