首次提交

This commit is contained in:
2026-07-18 20:09:26 +08:00
commit 521ad3b635
48 changed files with 7199 additions and 0 deletions
+144
View File
@@ -0,0 +1,144 @@
from fastapi import APIRouter, Depends, HTTPException, Header
from fastapi.responses import PlainTextResponse, FileResponse
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from jinja2 import Environment, FileSystemLoader
from pathlib import Path
import os
from backend.database import get_db
from backend.models import Server, Domain, DeployLog
from backend.config import get_settings
router = APIRouter(tags=["client-api"])
settings = get_settings()
# Jinja2 环境,用于生成脚本
template_dir = Path(__file__).parent.parent / "templates_cert"
jinja_env = Environment(loader=FileSystemLoader(str(template_dir)))
async def verify_token(
authorization: str = Header(...),
db: AsyncSession = Depends(get_db),
):
"""验证 Bearer Token,返回对应的 Server 对象"""
if not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Invalid authorization header")
token = authorization[7:]
result = await db.execute(select(Server).where(Server.token == token))
server = result.scalar_one_or_none()
if not server:
raise HTTPException(status_code=401, detail="Invalid token")
return server
@router.get("/version/{domain}")
async def get_version(domain: str, db: AsyncSession = Depends(get_db)):
"""返回当前证书版本号(无需认证,方便客户端轻量检查)"""
result = await db.execute(select(Domain).where(Domain.domain == domain))
d = result.scalar_one_or_none()
if not d:
raise HTTPException(status_code=404, detail="Domain not found")
return PlainTextResponse(d.version)
@router.get("/cert/{domain}/fullchain")
async def get_fullchain(
domain: str,
server: Server = Depends(verify_token),
db: AsyncSession = Depends(get_db),
):
"""下载 fullchain.pem"""
result = await db.execute(
select(Domain).where(Domain.domain == domain, Domain.server_id == server.id)
)
d = result.scalar_one_or_none()
if not d:
raise HTTPException(status_code=404, detail="Domain not found for this server")
cert_path = Path(settings.cert_dir) / domain / "fullchain.pem"
if not cert_path.exists():
raise HTTPException(status_code=404, detail="Certificate file not found")
return FileResponse(str(cert_path), media_type="application/x-pem-file", filename="fullchain.pem")
@router.get("/cert/{domain}/private")
async def get_private_key(
domain: str,
server: Server = Depends(verify_token),
db: AsyncSession = Depends(get_db),
):
"""下载 private.key"""
result = await db.execute(
select(Domain).where(Domain.domain == domain, Domain.server_id == server.id)
)
d = result.scalar_one_or_none()
if not d:
raise HTTPException(status_code=404, detail="Domain not found for this server")
key_path = Path(settings.cert_dir) / domain / "private.key"
if not key_path.exists():
raise HTTPException(status_code=404, detail="Private key file not found")
return FileResponse(str(key_path), media_type="application/x-pem-file", filename="private.key")
@router.get("/script/{domain}")
async def generate_script(
domain: str,
server_name: str,
db: AsyncSession = Depends(get_db),
):
"""根据 server_name 生成对应的部署脚本"""
# 查找服务器
result = await db.execute(select(Server).where(Server.name == server_name))
server = result.scalar_one_or_none()
if not server:
raise HTTPException(status_code=404, detail="Server not found")
# 查找域名配置
result = await db.execute(
select(Domain).where(Domain.domain == domain, Domain.server_id == server.id)
)
d = result.scalar_one_or_none()
if not d:
raise HTTPException(status_code=404, detail="Domain not found for this server")
# 选择模板
if server.platform == "windows":
template_name = "deploy-cert.ps1.j2"
else:
template_name = "deploy-cert.sh.j2"
tpl = jinja_env.get_template(template_name)
script = tpl.render(
domain=domain,
base_url=settings.base_url,
token=server.token,
cert_dir=d.cert_dir,
check_cmd=d.check_cmd,
reload_cmd=d.reload_cmd,
)
media_type = "text/plain" if server.platform == "windows" else "application/x-sh"
return PlainTextResponse(script, media_type=media_type)
@router.post("/report/{domain}")
async def report_deploy(
domain: str,
status: str,
message: str = "",
server: Server = Depends(verify_token),
db: AsyncSession = Depends(get_db),
):
"""客户端上报部署结果"""
result = await db.execute(
select(Domain).where(Domain.domain == domain, Domain.server_id == server.id)
)
d = result.scalar_one_or_none()
if not d:
raise HTTPException(status_code=404, detail="Domain not found for this server")
log = DeployLog(domain_id=d.id, server_id=server.id, status=status, message=message)
db.add(log)
return {"ok": True}