From 22cdb730d78078d3b4a7484f4f7446d8edd3466f Mon Sep 17 00:00:00 2001 From: sans Date: Mon, 20 Jul 2026 11:29:46 +0800 Subject: [PATCH] =?UTF-8?q?=E9=97=AE=E9=A2=98=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/routers/admin.py | 1 + backend/routers/api.py | 58 +++++++++++------------ backend/templates_cert/deploy-cert.ps1.j2 | 12 +++-- backend/templates_cert/deploy-cert.sh.j2 | 14 ++++-- frontend/src/api/index.js | 4 -- 5 files changed, 44 insertions(+), 45 deletions(-) diff --git a/backend/routers/admin.py b/backend/routers/admin.py index 4428b8f..204b13c 100644 --- a/backend/routers/admin.py +++ b/backend/routers/admin.py @@ -668,6 +668,7 @@ async def _render_script(domain: str, server_name: str, os: str, db: AsyncSessio cert_dir=d.cert_dir, check_cmd=d.check_cmd, reload_cmd=d.reload_cmd, + acme_config_id=d.acme_config_id or "", ) return script, server, d diff --git a/backend/routers/api.py b/backend/routers/api.py index 99a8a65..f42a5d4 100644 --- a/backend/routers/api.py +++ b/backend/routers/api.py @@ -28,19 +28,30 @@ async def verify_token( 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() + server = result.scalars().first() if not server: raise HTTPException(status_code=401, detail="Invalid token") return server -@router.get("/version") -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() +async def _find_domain(db: AsyncSession, domain: str, server_id: int | None = None, acme_config_id: int | None = None) -> Domain: + """根据条件查找域名记录,acme_config_id 精确匹配""" + conditions = [Domain.domain == domain] + if server_id is not None: + conditions.append(Domain.server_id == server_id) + if acme_config_id is not None: + conditions.append(Domain.acme_config_id == acme_config_id) + result = await db.execute(select(Domain).where(*conditions)) + d = result.scalars().first() if not d: raise HTTPException(status_code=404, detail="Domain not found") + return d + + +@router.get("/version") +async def get_version(domain: str, acme_config_id: int | None = None, db: AsyncSession = Depends(get_db)): + """返回当前证书版本号(无需认证,方便客户端轻量检查)""" + d = await _find_domain(db, domain, acme_config_id=acme_config_id) return PlainTextResponse(d.version) @@ -52,16 +63,12 @@ def _cert_store_dir(domain: str) -> str: @router.get("/cert/fullchain") async def get_fullchain( domain: str, + acme_config_id: int | None = None, 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") + await _find_domain(db, domain, server_id=server.id, acme_config_id=acme_config_id) cert_path = Path(settings.cert_dir) / _cert_store_dir(domain) / "fullchain.pem" if not cert_path.exists(): raise HTTPException(status_code=404, detail="Certificate file not found") @@ -71,16 +78,12 @@ async def get_fullchain( @router.get("/cert/private") async def get_private_key( domain: str, + acme_config_id: int | None = None, 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") + await _find_domain(db, domain, server_id=server.id, acme_config_id=acme_config_id) key_path = Path(settings.cert_dir) / _cert_store_dir(domain) / "private.key" if not key_path.exists(): raise HTTPException(status_code=404, detail="Private key file not found") @@ -91,22 +94,18 @@ async def get_private_key( async def generate_script( domain: str, server_name: str, + acme_config_id: int | None = None, db: AsyncSession = Depends(get_db), ): """根据 server_name 生成对应的部署脚本""" # 查找服务器 result = await db.execute(select(Server).where(Server.name == server_name)) - server = result.scalar_one_or_none() + server = result.scalars().first() 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") + d = await _find_domain(db, domain, server_id=server.id, acme_config_id=acme_config_id) # 选择模板 if server.platform == "windows": @@ -122,6 +121,7 @@ async def generate_script( cert_dir=d.cert_dir, check_cmd=d.check_cmd, reload_cmd=d.reload_cmd, + acme_config_id=d.acme_config_id or "", ) media_type = "text/plain" if server.platform == "windows" else "application/x-sh" @@ -134,16 +134,12 @@ async def report_deploy( status: str, message: str = "", hostname: str = "", + acme_config_id: int | None = None, 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") + d = await _find_domain(db, domain, server_id=server.id, acme_config_id=acme_config_id) log = DeployLog(domain_id=d.id, server_id=server.id, hostname=hostname, status=status, message=message) db.add(log) diff --git a/backend/templates_cert/deploy-cert.ps1.j2 b/backend/templates_cert/deploy-cert.ps1.j2 index a84f28c..e51ad63 100644 --- a/backend/templates_cert/deploy-cert.ps1.j2 +++ b/backend/templates_cert/deploy-cert.ps1.j2 @@ -7,9 +7,11 @@ $Token = "{{ token }}" $CertDir = "{{ cert_dir }}" $CheckCmd = "{{ check_cmd }}" $ReloadCmd = "{{ reload_cmd }}" +$ConfigId = "{{ acme_config_id }}" # =========================== $EncodedDomain = [System.Uri]::EscapeDataString($Domain) +$ConfigParam = if ($ConfigId) { "&acme_config_id=$ConfigId" } else { "" } $TmpDir = "$env:TEMP\cert-sync-$($Domain -replace '[*\.]','_')" $VersionFile = "$CertDir\.version" @@ -20,13 +22,13 @@ $headers = @{ Authorization = "Bearer $Token" } [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } # 1. 检查版本 -$remote = (Invoke-WebRequest -Uri "$BaseUrl/api/version?domain=$EncodedDomain" -Headers $headers -UseBasicParsing -SkipCertificateCheck).Content.Trim() +$remote = (Invoke-WebRequest -Uri "$BaseUrl/api/version?domain=$EncodedDomain$ConfigParam" -Headers $headers -UseBasicParsing -SkipCertificateCheck).Content.Trim() $local = if (Test-Path $VersionFile) { (Get-Content $VersionFile).Trim() } else { "0" } if ($remote -eq $local) { exit 0 } # 2. 下载证书 -Invoke-WebRequest -Uri "$BaseUrl/api/cert/fullchain?domain=$EncodedDomain" -Headers $headers -OutFile "$TmpDir\fullchain.pem" -UseBasicParsing -SkipCertificateCheck -Invoke-WebRequest -Uri "$BaseUrl/api/cert/private?domain=$EncodedDomain" -Headers $headers -OutFile "$TmpDir\private.key" -UseBasicParsing -SkipCertificateCheck +Invoke-WebRequest -Uri "$BaseUrl/api/cert/fullchain?domain=$EncodedDomain$ConfigParam" -Headers $headers -OutFile "$TmpDir\fullchain.pem" -UseBasicParsing -SkipCertificateCheck +Invoke-WebRequest -Uri "$BaseUrl/api/cert/private?domain=$EncodedDomain$ConfigParam" -Headers $headers -OutFile "$TmpDir\private.key" -UseBasicParsing -SkipCertificateCheck # 3. 备份旧证书 Copy-Item "$CertDir\fullchain.pem" "$CertDir\fullchain.pem.bak" -ErrorAction SilentlyContinue @@ -43,11 +45,11 @@ try { $remote | Out-File -NoNewline -Encoding ascii $VersionFile Invoke-Expression $ReloadCmd Write-Host "[$(Get-Date -Format o)] updated: $Domain -> $remote" - try { Invoke-WebRequest -Uri "$BaseUrl/api/report?domain=$EncodedDomain&status=success&hostname=$HostName&message=deployed+v$remote" -Headers $headers -Method POST -UseBasicParsing -SkipCertificateCheck | Out-Null } catch {} + try { Invoke-WebRequest -Uri "$BaseUrl/api/report?domain=$EncodedDomain&status=success&hostname=$HostName&message=deployed+v$remote$ConfigParam" -Headers $headers -Method POST -UseBasicParsing -SkipCertificateCheck | Out-Null } catch {} } catch { Move-Item "$CertDir\fullchain.pem.bak" "$CertDir\fullchain.pem" -Force -ErrorAction SilentlyContinue Move-Item "$CertDir\private.key.bak" "$CertDir\private.key" -Force -ErrorAction SilentlyContinue Write-Error "[$(Get-Date -Format o)] FAILED: $Domain, rolled back" - try { Invoke-WebRequest -Uri "$BaseUrl/api/report?domain=$EncodedDomain&status=failed&hostname=$HostName&message=check+failed+rolled+back" -Headers $headers -Method POST -UseBasicParsing -SkipCertificateCheck | Out-Null } catch {} + try { Invoke-WebRequest -Uri "$BaseUrl/api/report?domain=$EncodedDomain&status=failed&hostname=$HostName&message=check+failed+rolled+back$ConfigParam" -Headers $headers -Method POST -UseBasicParsing -SkipCertificateCheck | Out-Null } catch {} exit 1 } diff --git a/backend/templates_cert/deploy-cert.sh.j2 b/backend/templates_cert/deploy-cert.sh.j2 index fe4c672..d1cee49 100644 --- a/backend/templates_cert/deploy-cert.sh.j2 +++ b/backend/templates_cert/deploy-cert.sh.j2 @@ -8,11 +8,15 @@ TOKEN="{{ token }}" CERT_DIR="{{ cert_dir }}" CHECK_CMD="{{ check_cmd }}" RELOAD_CMD="{{ reload_cmd }}" +CONFIG_ID="{{ acme_config_id }}" # =========================== -VERSION_URL="${BASE_URL}/api/version?domain=${DOMAIN}" -FULLCHAIN_URL="${BASE_URL}/api/cert/fullchain?domain=${DOMAIN}" -PRIVATE_URL="${BASE_URL}/api/cert/private?domain=${DOMAIN}" +CONFIG_PARAM="" +[[ -n "${CONFIG_ID}" ]] && CONFIG_PARAM="&acme_config_id=${CONFIG_ID}" + +VERSION_URL="${BASE_URL}/api/version?domain=${DOMAIN}${CONFIG_PARAM}" +FULLCHAIN_URL="${BASE_URL}/api/cert/fullchain?domain=${DOMAIN}${CONFIG_PARAM}" +PRIVATE_URL="${BASE_URL}/api/cert/private?domain=${DOMAIN}${CONFIG_PARAM}" TMP_DIR="/tmp/cert-sync-${DOMAIN//\*/_}" VERSION_FILE="${CERT_DIR}/.version" @@ -47,11 +51,11 @@ if ${CHECK_CMD}; then echo "${REMOTE}" > "${VERSION_FILE}" ${RELOAD_CMD} echo "[$(date -Is)] updated: ${DOMAIN} -> ${REMOTE}" - curl "${curl_opts[@]}" "${auth[@]}" -X POST "${BASE_URL}/api/report?domain=${DOMAIN}&status=success&hostname=${HOSTNAME}&message=deployed+v${REMOTE}" 2>/dev/null || true + curl "${curl_opts[@]}" "${auth[@]}" -X POST "${BASE_URL}/api/report?domain=${DOMAIN}&status=success&hostname=${HOSTNAME}&message=deployed+v${REMOTE}${CONFIG_PARAM}" 2>/dev/null || true else mv -f "${CERT_DIR}/fullchain.pem.bak" "${CERT_DIR}/fullchain.pem" 2>/dev/null || true mv -f "${CERT_DIR}/private.key.bak" "${CERT_DIR}/private.key" 2>/dev/null || true echo "[$(date -Is)] FAILED: ${DOMAIN}, rolled back" >&2 - curl "${curl_opts[@]}" "${auth[@]}" -X POST "${BASE_URL}/api/report?domain=${DOMAIN}&status=failed&hostname=${HOSTNAME}&message=check+failed+rolled+back" 2>/dev/null || true + curl "${curl_opts[@]}" "${auth[@]}" -X POST "${BASE_URL}/api/report?domain=${DOMAIN}&status=failed&hostname=${HOSTNAME}&message=check+failed+rolled+back${CONFIG_PARAM}" 2>/dev/null || true exit 1 fi diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index dff0818..1d9063c 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -51,8 +51,4 @@ export const getLogs = (status) => api.get('/logs', { params: { status } }) export const issueCert = (domainId) => api.post(`/acme/issue/${domainId}`) export const renewCert = (domainId) => api.post(`/acme/renew/${domainId}`) -// Script (通过管理接口获取脚本内容) -export const getScript = (domain, serverName) => - axios.get('/api/script', { params: { domain, server_name: serverName } }) - export default api