536 lines
18 KiB
Python
536 lines
18 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, Header, Cookie
|
|||
|
|
from fastapi.responses import JSONResponse
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
|
from sqlalchemy import select, func
|
||
|
|
from sqlalchemy.orm import selectinload
|
||
|
|
from pydantic import BaseModel
|
||
|
|
from datetime import datetime, timedelta
|
||
|
|
import hashlib
|
||
|
|
import hmac
|
||
|
|
import json
|
||
|
|
import time
|
||
|
|
|
||
|
|
from backend.database import get_db
|
||
|
|
from backend.models import Server, Domain, DeployLog, AcmeConfig, AcmeLog
|
||
|
|
from backend.config import get_settings
|
||
|
|
|
||
|
|
router = APIRouter(tags=["admin-api"])
|
||
|
|
|
||
|
|
|
||
|
|
# ──────────────── Auth ────────────────
|
||
|
|
|
||
|
|
def generate_token(username: str, secret_key: str) -> str:
|
||
|
|
"""生成简单的认证 token"""
|
||
|
|
expire = int(time.time()) + 86400 * 7 # 7 天过期
|
||
|
|
payload = f"{username}:{expire}"
|
||
|
|
signature = hmac.new(secret_key.encode(), payload.encode(), hashlib.sha256).hexdigest()[:16]
|
||
|
|
return f"{payload}:{signature}"
|
||
|
|
|
||
|
|
|
||
|
|
def verify_token(token: str, secret_key: str) -> bool:
|
||
|
|
"""验证 token"""
|
||
|
|
try:
|
||
|
|
parts = token.split(":")
|
||
|
|
if len(parts) != 3:
|
||
|
|
return False
|
||
|
|
username, expire_str, signature = parts
|
||
|
|
expire = int(expire_str)
|
||
|
|
if time.time() > expire:
|
||
|
|
return False
|
||
|
|
payload = f"{username}:{expire_str}"
|
||
|
|
expected = hmac.new(secret_key.encode(), payload.encode(), hashlib.sha256).hexdigest()[:16]
|
||
|
|
return hmac.compare_digest(signature, expected)
|
||
|
|
except Exception:
|
||
|
|
return False
|
||
|
|
|
||
|
|
|
||
|
|
async def require_auth(authorization: str = Header(None), admin_token: str = Cookie(None)):
|
||
|
|
"""认证依赖,检查 Header 或 Cookie 中的 token"""
|
||
|
|
settings = get_settings()
|
||
|
|
|
||
|
|
# 从 Header 获取
|
||
|
|
token = None
|
||
|
|
if authorization and authorization.startswith("Bearer "):
|
||
|
|
token = authorization[7:]
|
||
|
|
# 从 Cookie 获取
|
||
|
|
elif admin_token:
|
||
|
|
token = admin_token
|
||
|
|
|
||
|
|
if not token or not verify_token(token, settings.secret_key):
|
||
|
|
raise HTTPException(status_code=401, detail="未登录或登录已过期")
|
||
|
|
return True
|
||
|
|
|
||
|
|
|
||
|
|
class LoginRequest(BaseModel):
|
||
|
|
username: str
|
||
|
|
password: str
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/login")
|
||
|
|
async def login(data: LoginRequest):
|
||
|
|
settings = get_settings()
|
||
|
|
if data.username != settings.admin_username or data.password != settings.admin_password:
|
||
|
|
raise HTTPException(status_code=401, detail="用户名或密码错误")
|
||
|
|
|
||
|
|
token = generate_token(data.username, settings.secret_key)
|
||
|
|
response = JSONResponse(content={"ok": True, "token": token})
|
||
|
|
response.set_cookie(
|
||
|
|
key="admin_token",
|
||
|
|
value=token,
|
||
|
|
max_age=86400 * 7,
|
||
|
|
httponly=True,
|
||
|
|
samesite="lax",
|
||
|
|
)
|
||
|
|
return response
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/logout")
|
||
|
|
async def logout():
|
||
|
|
response = JSONResponse(content={"ok": True})
|
||
|
|
response.delete_cookie("admin_token")
|
||
|
|
return response
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/me")
|
||
|
|
async def check_auth(_: bool = Depends(require_auth)):
|
||
|
|
return {"ok": True}
|
||
|
|
|
||
|
|
|
||
|
|
# ──────────────── Schemas ────────────────
|
||
|
|
|
||
|
|
class ServerCreate(BaseModel):
|
||
|
|
name: str
|
||
|
|
platform: str = "linux"
|
||
|
|
token: str
|
||
|
|
ip: str | None = None
|
||
|
|
|
||
|
|
|
||
|
|
class ServerUpdate(BaseModel):
|
||
|
|
name: str | None = None
|
||
|
|
platform: str | None = None
|
||
|
|
token: str | None = None
|
||
|
|
ip: str | None = None
|
||
|
|
|
||
|
|
|
||
|
|
class DomainCreate(BaseModel):
|
||
|
|
server_id: int
|
||
|
|
domain: str
|
||
|
|
cert_dir: str
|
||
|
|
check_cmd: str = "nginx -t"
|
||
|
|
reload_cmd: str = "systemctl reload nginx"
|
||
|
|
|
||
|
|
|
||
|
|
class DomainUpdate(BaseModel):
|
||
|
|
server_id: int | None = None
|
||
|
|
domain: str | None = None
|
||
|
|
cert_dir: str | None = None
|
||
|
|
check_cmd: str | None = None
|
||
|
|
reload_cmd: str | None = None
|
||
|
|
version: str | None = None
|
||
|
|
|
||
|
|
|
||
|
|
# ──────────────── Stats ────────────────
|
||
|
|
|
||
|
|
@router.get("/stats")
|
||
|
|
async def get_stats(db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)):
|
||
|
|
"""仪表盘统计数据"""
|
||
|
|
server_count = (await db.execute(select(func.count(Server.id)))).scalar() or 0
|
||
|
|
domain_count = (await db.execute(select(func.count(Domain.id)))) .scalar() or 0
|
||
|
|
|
||
|
|
# 即将过期(7 天内)
|
||
|
|
now = datetime.utcnow()
|
||
|
|
soon = now.replace(day=now.day + 7) if now.day <= 24 else now.replace(month=now.month + 1, day=now.day + 7 - 30)
|
||
|
|
expiring = (await db.execute(
|
||
|
|
select(func.count(Domain.id)).where(
|
||
|
|
Domain.cert_not_after.isnot(None),
|
||
|
|
Domain.cert_not_after <= soon,
|
||
|
|
)
|
||
|
|
)).scalar() or 0
|
||
|
|
|
||
|
|
# 最近日志
|
||
|
|
result = await db.execute(
|
||
|
|
select(DeployLog)
|
||
|
|
.options(selectinload(DeployLog.domain), selectinload(DeployLog.domain).selectinload(Domain.server))
|
||
|
|
.order_by(DeployLog.created_at.desc())
|
||
|
|
.limit(10)
|
||
|
|
)
|
||
|
|
logs = result.scalars().all()
|
||
|
|
|
||
|
|
return {
|
||
|
|
"server_count": server_count,
|
||
|
|
"domain_count": domain_count,
|
||
|
|
"expiring_count": expiring,
|
||
|
|
"recent_logs": [
|
||
|
|
{
|
||
|
|
"id": log.id,
|
||
|
|
"domain": log.domain.domain if log.domain else None,
|
||
|
|
"server": log.domain.server.name if log.domain and log.domain.server else None,
|
||
|
|
"status": log.status,
|
||
|
|
"message": log.message,
|
||
|
|
"created_at": log.created_at.isoformat(),
|
||
|
|
}
|
||
|
|
for log in logs
|
||
|
|
],
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
# ──────────────── Servers ────────────────
|
||
|
|
|
||
|
|
@router.get("/servers")
|
||
|
|
async def list_servers(db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)):
|
||
|
|
result = await db.execute(select(Server).options(selectinload(Server.domains)).order_by(Server.id))
|
||
|
|
servers = result.scalars().all()
|
||
|
|
return [
|
||
|
|
{
|
||
|
|
"id": s.id,
|
||
|
|
"name": s.name,
|
||
|
|
"platform": s.platform,
|
||
|
|
"token": s.token,
|
||
|
|
"ip": s.ip,
|
||
|
|
"domain_count": len(s.domains),
|
||
|
|
"created_at": s.created_at.isoformat(),
|
||
|
|
}
|
||
|
|
for s in servers
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/servers/{server_id}")
|
||
|
|
async def get_server(server_id: int, db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)):
|
||
|
|
result = await db.execute(select(Server).where(Server.id == server_id))
|
||
|
|
s = result.scalar_one_or_none()
|
||
|
|
if not s:
|
||
|
|
raise HTTPException(404, "Server not found")
|
||
|
|
return {"id": s.id, "name": s.name, "platform": s.platform, "token": s.token, "ip": s.ip}
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/servers", status_code=201)
|
||
|
|
async def create_server(data: ServerCreate, db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)):
|
||
|
|
server = Server(**data.model_dump())
|
||
|
|
db.add(server)
|
||
|
|
await db.flush()
|
||
|
|
await db.refresh(server)
|
||
|
|
return {"id": server.id, "name": server.name}
|
||
|
|
|
||
|
|
|
||
|
|
@router.put("/servers/{server_id}")
|
||
|
|
async def update_server(server_id: int, data: ServerUpdate, db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)):
|
||
|
|
result = await db.execute(select(Server).where(Server.id == server_id))
|
||
|
|
server = result.scalar_one_or_none()
|
||
|
|
if not server:
|
||
|
|
raise HTTPException(404, "Server not found")
|
||
|
|
for key, value in data.model_dump(exclude_unset=True).items():
|
||
|
|
setattr(server, key, value)
|
||
|
|
return {"id": server.id}
|
||
|
|
|
||
|
|
|
||
|
|
@router.delete("/servers/{server_id}")
|
||
|
|
async def delete_server(server_id: int, db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)):
|
||
|
|
result = await db.execute(select(Server).where(Server.id == server_id))
|
||
|
|
server = result.scalar_one_or_none()
|
||
|
|
if not server:
|
||
|
|
raise HTTPException(404, "Server not found")
|
||
|
|
await db.delete(server)
|
||
|
|
return {"ok": True}
|
||
|
|
|
||
|
|
|
||
|
|
# ──────────────── Domains ────────────────
|
||
|
|
|
||
|
|
@router.get("/domains")
|
||
|
|
async def list_domains(db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)):
|
||
|
|
result = await db.execute(
|
||
|
|
select(Domain).options(selectinload(Domain.server)).order_by(Domain.id)
|
||
|
|
)
|
||
|
|
domains = result.scalars().all()
|
||
|
|
return [
|
||
|
|
{
|
||
|
|
"id": d.id,
|
||
|
|
"server_id": d.server_id,
|
||
|
|
"server_name": d.server.name if d.server else None,
|
||
|
|
"domain": d.domain,
|
||
|
|
"cert_dir": d.cert_dir,
|
||
|
|
"check_cmd": d.check_cmd,
|
||
|
|
"reload_cmd": d.reload_cmd,
|
||
|
|
"version": d.version,
|
||
|
|
"cert_not_after": d.cert_not_after.isoformat() if d.cert_not_after else None,
|
||
|
|
}
|
||
|
|
for d in domains
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/domains/{domain_id}")
|
||
|
|
async def get_domain(domain_id: int, db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)):
|
||
|
|
result = await db.execute(select(Domain).where(Domain.id == domain_id))
|
||
|
|
d = result.scalar_one_or_none()
|
||
|
|
if not d:
|
||
|
|
raise HTTPException(404, "Domain not found")
|
||
|
|
return {
|
||
|
|
"id": d.id,
|
||
|
|
"server_id": d.server_id,
|
||
|
|
"domain": d.domain,
|
||
|
|
"cert_dir": d.cert_dir,
|
||
|
|
"check_cmd": d.check_cmd,
|
||
|
|
"reload_cmd": d.reload_cmd,
|
||
|
|
"version": d.version,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/domains", status_code=201)
|
||
|
|
async def create_domain(data: DomainCreate, db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)):
|
||
|
|
domain = Domain(**data.model_dump())
|
||
|
|
db.add(domain)
|
||
|
|
await db.flush()
|
||
|
|
await db.refresh(domain)
|
||
|
|
return {"id": domain.id, "domain": domain.domain}
|
||
|
|
|
||
|
|
|
||
|
|
@router.put("/domains/{domain_id}")
|
||
|
|
async def update_domain(domain_id: int, data: DomainUpdate, db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)):
|
||
|
|
result = await db.execute(select(Domain).where(Domain.id == domain_id))
|
||
|
|
domain = result.scalar_one_or_none()
|
||
|
|
if not domain:
|
||
|
|
raise HTTPException(404, "Domain not found")
|
||
|
|
for key, value in data.model_dump(exclude_unset=True).items():
|
||
|
|
setattr(domain, key, value)
|
||
|
|
return {"id": domain.id}
|
||
|
|
|
||
|
|
|
||
|
|
@router.delete("/domains/{domain_id}")
|
||
|
|
async def delete_domain(domain_id: int, db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)):
|
||
|
|
result = await db.execute(select(Domain).where(Domain.id == domain_id))
|
||
|
|
domain = result.scalar_one_or_none()
|
||
|
|
if not domain:
|
||
|
|
raise HTTPException(404, "Domain not found")
|
||
|
|
await db.delete(domain)
|
||
|
|
return {"ok": True}
|
||
|
|
|
||
|
|
|
||
|
|
# ──────────────── Logs ────────────────
|
||
|
|
|
||
|
|
@router.get("/logs")
|
||
|
|
async def list_logs(status: str | None = None, db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)):
|
||
|
|
query = (
|
||
|
|
select(DeployLog)
|
||
|
|
.options(selectinload(DeployLog.domain), selectinload(DeployLog.domain).selectinload(Domain.server))
|
||
|
|
.order_by(DeployLog.created_at.desc())
|
||
|
|
.limit(100)
|
||
|
|
)
|
||
|
|
if status:
|
||
|
|
query = query.where(DeployLog.status == status)
|
||
|
|
result = await db.execute(query)
|
||
|
|
logs = result.scalars().all()
|
||
|
|
return [
|
||
|
|
{
|
||
|
|
"id": log.id,
|
||
|
|
"domain": log.domain.domain if log.domain else None,
|
||
|
|
"server": log.domain.server.name if log.domain and log.domain.server else None,
|
||
|
|
"status": log.status,
|
||
|
|
"message": log.message,
|
||
|
|
"created_at": log.created_at.isoformat(),
|
||
|
|
}
|
||
|
|
for log in logs
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
# ──────────────── ACME 配置 ────────────────
|
||
|
|
|
||
|
|
class AcmeConfigUpdate(BaseModel):
|
||
|
|
acme_server: str | None = None
|
||
|
|
email: str | None = None
|
||
|
|
dns_provider: str | None = None
|
||
|
|
dns_credentials: str | None = None
|
||
|
|
renew_days: int | None = None
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/acme/config")
|
||
|
|
async def get_acme_config(db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)):
|
||
|
|
result = await db.execute(select(AcmeConfig).where(AcmeConfig.id == 1))
|
||
|
|
config = result.scalar_one_or_none()
|
||
|
|
if not config:
|
||
|
|
# 返回默认配置
|
||
|
|
return {
|
||
|
|
"acme_server": "https://acme-v02.api.letsencrypt.org/directory",
|
||
|
|
"email": "",
|
||
|
|
"dns_provider": "aliyun",
|
||
|
|
"dns_credentials": "{}",
|
||
|
|
"renew_days": 30,
|
||
|
|
"has_account_key": False,
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
"acme_server": config.acme_server,
|
||
|
|
"email": config.email,
|
||
|
|
"dns_provider": config.dns_provider,
|
||
|
|
"dns_credentials": config.dns_credentials,
|
||
|
|
"renew_days": config.renew_days,
|
||
|
|
"has_account_key": config.account_key is not None,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
@router.put("/acme/config")
|
||
|
|
async def update_acme_config(data: AcmeConfigUpdate, db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)):
|
||
|
|
result = await db.execute(select(AcmeConfig).where(AcmeConfig.id == 1))
|
||
|
|
config = result.scalar_one_or_none()
|
||
|
|
if not config:
|
||
|
|
config = AcmeConfig(id=1)
|
||
|
|
db.add(config)
|
||
|
|
for key, value in data.model_dump(exclude_unset=True).items():
|
||
|
|
setattr(config, key, value)
|
||
|
|
return {"ok": True}
|
||
|
|
|
||
|
|
|
||
|
|
# ──────────────── ACME 操作 ────────────────
|
||
|
|
|
||
|
|
@router.post("/acme/issue/{domain_id}")
|
||
|
|
async def issue_cert(domain_id: int, db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)):
|
||
|
|
"""为指定域名申请证书"""
|
||
|
|
from backend.acme_service import AcmeService
|
||
|
|
from backend.config import get_settings
|
||
|
|
|
||
|
|
# 获取 ACME 配置
|
||
|
|
result = await db.execute(select(AcmeConfig).where(AcmeConfig.id == 1))
|
||
|
|
config = result.scalar_one_or_none()
|
||
|
|
if not config or not config.email:
|
||
|
|
raise HTTPException(400, "ACME not configured, please set email first")
|
||
|
|
|
||
|
|
# 获取域名
|
||
|
|
result = await db.execute(select(Domain).where(Domain.id == domain_id))
|
||
|
|
domain = result.scalar_one_or_none()
|
||
|
|
if not domain:
|
||
|
|
raise HTTPException(404, "Domain not found")
|
||
|
|
|
||
|
|
settings = get_settings()
|
||
|
|
service = AcmeService(config, settings.cert_dir)
|
||
|
|
|
||
|
|
# 记录开始
|
||
|
|
log = AcmeLog(domain_id=domain_id, action="issue", status="pending", message=f"Issuing certificate for {domain.domain}")
|
||
|
|
db.add(log)
|
||
|
|
await db.flush()
|
||
|
|
|
||
|
|
try:
|
||
|
|
success, msg = service.issue_certificate(domain.domain)
|
||
|
|
log.status = "success" if success else "failed"
|
||
|
|
log.message = msg
|
||
|
|
|
||
|
|
if success:
|
||
|
|
# 更新域名的证书到期时间
|
||
|
|
info = service.get_cert_info(domain.domain)
|
||
|
|
if info:
|
||
|
|
domain.cert_not_after = datetime.fromisoformat(info["not_after"])
|
||
|
|
domain.version = str(int(domain.version or "0") + 1)
|
||
|
|
|
||
|
|
await db.flush()
|
||
|
|
return {"success": success, "message": msg}
|
||
|
|
except Exception as e:
|
||
|
|
log.status = "failed"
|
||
|
|
log.message = str(e)
|
||
|
|
await db.flush()
|
||
|
|
raise HTTPException(500, str(e))
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/acme/renew/{domain_id}")
|
||
|
|
async def renew_cert(domain_id: int, db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)):
|
||
|
|
"""续签指定域名的证书"""
|
||
|
|
return await issue_cert(domain_id, db)
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/acme/auto-renew")
|
||
|
|
async def auto_renew_all(db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)):
|
||
|
|
"""自动续签所有即将过期的证书"""
|
||
|
|
from backend.acme_service import AcmeService
|
||
|
|
from backend.config import get_settings
|
||
|
|
|
||
|
|
result = await db.execute(select(AcmeConfig).where(AcmeConfig.id == 1))
|
||
|
|
config = result.scalar_one_or_none()
|
||
|
|
if not config or not config.email:
|
||
|
|
raise HTTPException(400, "ACME not configured")
|
||
|
|
|
||
|
|
settings = get_settings()
|
||
|
|
service = AcmeService(config, settings.cert_dir)
|
||
|
|
|
||
|
|
# 获取所有域名
|
||
|
|
result = await db.execute(select(Domain))
|
||
|
|
domains = result.scalars().all()
|
||
|
|
|
||
|
|
results = []
|
||
|
|
for d in domains:
|
||
|
|
need_renew, days_left = service.check_expiry(d.domain)
|
||
|
|
if need_renew:
|
||
|
|
log = AcmeLog(domain_id=d.id, action="renew", status="pending", message=f"Auto-renewing {d.domain}")
|
||
|
|
db.add(log)
|
||
|
|
await db.flush()
|
||
|
|
|
||
|
|
success, msg = service.renew_certificate(d.domain)
|
||
|
|
log.status = "success" if success else "failed"
|
||
|
|
log.message = msg
|
||
|
|
|
||
|
|
if success:
|
||
|
|
info = service.get_cert_info(d.domain)
|
||
|
|
if info:
|
||
|
|
d.cert_not_after = datetime.fromisoformat(info["not_after"])
|
||
|
|
d.version = str(int(d.version or "0") + 1)
|
||
|
|
|
||
|
|
results.append({"domain": d.domain, "success": success, "message": msg, "days_left": days_left})
|
||
|
|
else:
|
||
|
|
results.append({"domain": d.domain, "success": True, "message": f"Skipped, {days_left} days left", "days_left": days_left})
|
||
|
|
|
||
|
|
return {"results": results}
|
||
|
|
|
||
|
|
|
||
|
|
# ──────────────── ACME 日志 ────────────────
|
||
|
|
|
||
|
|
@router.get("/acme/logs")
|
||
|
|
async def list_acme_logs(status: str | None = None, db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)):
|
||
|
|
query = (
|
||
|
|
select(AcmeLog)
|
||
|
|
.options(selectinload(AcmeLog.domain))
|
||
|
|
.order_by(AcmeLog.created_at.desc())
|
||
|
|
.limit(100)
|
||
|
|
)
|
||
|
|
if status:
|
||
|
|
query = query.where(AcmeLog.status == status)
|
||
|
|
result = await db.execute(query)
|
||
|
|
logs = result.scalars().all()
|
||
|
|
return [
|
||
|
|
{
|
||
|
|
"id": log.id,
|
||
|
|
"domain": log.domain.domain if log.domain else None,
|
||
|
|
"action": log.action,
|
||
|
|
"status": log.status,
|
||
|
|
"message": log.message,
|
||
|
|
"detail": log.detail,
|
||
|
|
"created_at": log.created_at.isoformat(),
|
||
|
|
}
|
||
|
|
for log in logs
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
# ──────────────── 证书信息 ────────────────
|
||
|
|
|
||
|
|
@router.get("/cert-info/{domain_id}")
|
||
|
|
async def get_cert_info(domain_id: int, db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)):
|
||
|
|
"""获取证书详细信息"""
|
||
|
|
from backend.acme_service import AcmeService
|
||
|
|
from backend.config import get_settings
|
||
|
|
|
||
|
|
result = await db.execute(select(Domain).where(Domain.id == domain_id))
|
||
|
|
domain = result.scalar_one_or_none()
|
||
|
|
if not domain:
|
||
|
|
raise HTTPException(404, "Domain not found")
|
||
|
|
|
||
|
|
result = await db.execute(select(AcmeConfig).where(AcmeConfig.id == 1))
|
||
|
|
config = result.scalar_one_or_none()
|
||
|
|
if not config:
|
||
|
|
config = AcmeConfig(id=1)
|
||
|
|
|
||
|
|
settings = get_settings()
|
||
|
|
service = AcmeService(config, settings.cert_dir)
|
||
|
|
|
||
|
|
info = service.get_cert_info(domain.domain)
|
||
|
|
need_renew, days_left = service.check_expiry(domain.domain)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"domain": domain.domain,
|
||
|
|
"cert_info": info,
|
||
|
|
"need_renew": need_renew,
|
||
|
|
"days_left": days_left,
|
||
|
|
}
|