脚本一键化

This commit is contained in:
2026-07-19 00:21:23 +08:00
parent 7a138559de
commit 700db4efb9
3 changed files with 91 additions and 71 deletions
+2 -1
View File
@@ -19,7 +19,7 @@ from backend.routers.admin import (
list_logs,
list_acme_configs, get_acme_config, create_acme_config, update_acme_config, delete_acme_config,
issue_cert, renew_cert, auto_renew_all, list_acme_logs, get_cert_info,
download_cert_file, admin_generate_script, admin_download_script,
download_cert_file, admin_generate_script, admin_download_script, admin_setup_script,
)
logging.basicConfig(level=logging.DEBUG)
@@ -95,6 +95,7 @@ app.get("/admin/api/cert-info/{domain_id}")(get_cert_info)
app.get("/admin/api/cert-download/{domain_id}/{file_type}")(download_cert_file)
app.get("/admin/api/script")(admin_generate_script)
app.get("/admin/api/script/download")(admin_download_script)
app.get("/admin/api/script/setup")(admin_setup_script)
logger.info("=== 路由注册完成 ===")
for route in app.routes:
+43 -2
View File
@@ -1,5 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException, Header, Cookie
from fastapi.responses import JSONResponse
from fastapi import APIRouter, Depends, HTTPException, Header, Cookie, Request
from fastapi.responses import JSONResponse, PlainTextResponse
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from sqlalchemy.orm import selectinload
@@ -652,6 +652,47 @@ async def admin_download_script(
)
@router.get("/script/setup")
async def admin_setup_script(
request: Request,
domain: str,
server_name: str,
os: str = "linux",
authorization: str = Header(None),
db: AsyncSession = Depends(get_db),
_: bool = Depends(require_auth),
):
"""生成一键安装脚本(纯文本下载)"""
from jinja2 import Environment, FileSystemLoader
template_dir = Path(__file__).parent.parent / "templates_cert"
jinja_env = Environment(loader=FileSystemLoader(str(template_dir)))
settings = get_settings()
base_url = str(request.base_url).rstrip("/")
ext = "ps1" if os == "windows" else "sh"
download_url = f"{base_url}/admin/api/script/download?domain={domain}&server_name={server_name}&os={os}"
# 提取 admin token 用于嵌入脚本
admin_token = ""
if authorization and authorization.startswith("Bearer "):
admin_token = authorization[7:]
template_name = "setup-cert.ps1.j2" if os == "windows" else "setup-cert.sh.j2"
tpl = jinja_env.get_template(template_name)
script = tpl.render(
domain=domain,
server_name=server_name,
download_url=download_url,
admin_token=admin_token,
)
filename = f"setup-{domain.replace('*', '_')}.{ext}"
return PlainTextResponse(
script,
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)
# ──────────────── 证书信息 ────────────────
@router.get("/cert-info/{domain_id}")
+46 -68
View File
@@ -111,65 +111,31 @@
</el-radio-group>
</div>
<!-- Linux 部署步骤 -->
<template v-if="scriptOs === 'linux'">
<el-steps direction="vertical" :active="3" style="margin-bottom: 12px;">
<el-step title="下载脚本" status="process">
<template #description>
<div style="display: flex; align-items: center; gap: 8px; margin-top: 4px;">
<code style="flex: 1; background: #1d1e1f; color: #e5eaf3; padding: 8px 12px; border-radius: 6px; font-size: 12px; word-break: break-all;">curl -fsSLk -H "Authorization: Bearer {{ adminToken }}" {{ scriptDownloadUrl }} -o /opt/deploy-{{ scriptDomain?.domain?.replace('*.', '') }}.sh</code>
<el-button type="primary" link size="small" :icon="DocumentCopy" @click="copyText(`curl -fsSLk -H 'Authorization: Bearer ${adminToken}' ${scriptDownloadUrl} -o /opt/deploy-${scriptDomain?.domain?.replace('*.', '')}.sh`)" />
</div>
</template>
</el-step>
<el-step title="赋予执行权限" status="process">
<template #description>
<div style="display: flex; align-items: center; gap: 8px; margin-top: 4px;">
<code style="flex: 1; background: #1d1e1f; color: #e5eaf3; padding: 8px 12px; border-radius: 6px; font-size: 12px;">chmod +x /opt/deploy-{{ scriptDomain?.domain?.replace('*.', '') }}.sh</code>
<el-button type="primary" link size="small" :icon="DocumentCopy" @click="copyText(`chmod +x /opt/deploy-${scriptDomain?.domain?.replace('*.', '')}.sh`)" />
</div>
</template>
</el-step>
<el-step title="手动执行一次,然后添加定时任务" status="process">
<template #description>
<div style="display: flex; align-items: center; gap: 8px; margin-top: 4px;">
<code style="flex: 1; background: #1d1e1f; color: #e5eaf3; padding: 8px 12px; border-radius: 6px; font-size: 12px;">/opt/deploy-{{ scriptDomain?.domain?.replace('*.', '') }}.sh && echo "0 * * * * /opt/deploy-{{ scriptDomain?.domain?.replace('*.', '') }}.sh >> /var/log/cert-deploy.log 2>&1" | crontab -</code>
<el-button type="primary" link size="small" :icon="DocumentCopy" @click="copyText(`/opt/deploy-${scriptDomain?.domain?.replace('*.', '')}.sh && echo '0 * * * * /opt/deploy-${scriptDomain?.domain?.replace('*.', '')}.sh >> /var/log/cert-deploy.log 2>&1' | crontab -`)" />
</div>
</template>
</el-step>
</el-steps>
</template>
<!-- 一键安装 -->
<el-card shadow="never" style="margin-bottom: 20px; background: #f0f9eb; border-color: #e1f3d8;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div>
<div style="font-weight: 600; font-size: 15px; margin-bottom: 4px;">一键安装</div>
<div style="color: #909399; font-size: 13px;">下载安装脚本到服务器执行自动完成脚本下载提权首次执行和定时任务配置</div>
</div>
<el-button type="success" round @click="downloadSetupScript">
<el-icon style="margin-right: 4px;"><Download /></el-icon>
下载安装脚本
</el-button>
</div>
</el-card>
<!-- Windows 部署步骤 -->
<template v-if="scriptOs === 'windows'">
<el-steps direction="vertical" :active="3" style="margin-bottom: 12px;">
<el-step title="下载脚本" status="process">
<template #description>
<div style="display: flex; align-items: center; gap: 8px; margin-top: 4px;">
<code style="flex: 1; background: #1d1e1f; color: #e5eaf3; padding: 8px 12px; border-radius: 6px; font-size: 12px; word-break: break-all;">Invoke-WebRequest -Uri "{{ scriptDownloadUrl }}" -Headers @{Authorization="Bearer {{ adminToken }}"} -OutFile "C:\cert\deploy-{{ scriptDomain?.domain?.replace('*.', '') }}.ps1" -SkipCertificateCheck</code>
<el-button type="primary" link size="small" :icon="DocumentCopy" @click="copyText(`Invoke-WebRequest -Uri '${scriptDownloadUrl}' -Headers @{Authorization='Bearer ${adminToken}'} -OutFile 'C:\\cert\\deploy-${scriptDomain?.domain?.replace('*.', '')}.ps1' -SkipCertificateCheck`)" />
</div>
</template>
</el-step>
<el-step title="测试运行" status="process">
<template #description>
<div style="display: flex; align-items: center; gap: 8px; margin-top: 4px;">
<code style="flex: 1; background: #1d1e1f; color: #e5eaf3; padding: 8px 12px; border-radius: 6px; font-size: 12px;">powershell -ExecutionPolicy Bypass -File "C:\cert\deploy-{{ scriptDomain?.domain?.replace('*.', '') }}.ps1"</code>
<el-button type="primary" link size="small" :icon="DocumentCopy" @click="copyText(`powershell -ExecutionPolicy Bypass -File 'C:\\cert\\deploy-${scriptDomain?.domain?.replace('*.', '')}.ps1'`)" />
</div>
</template>
</el-step>
<el-step title="手动执行一次,然后添加计划任务" status="process">
<template #description>
<div style="display: flex; align-items: center; gap: 8px; margin-top: 4px;">
<code style="flex: 1; background: #1d1e1f; color: #e5eaf3; padding: 8px 12px; border-radius: 6px; font-size: 12px;">powershell -ExecutionPolicy Bypass -File "C:\cert\deploy-{{ scriptDomain?.domain?.replace('*.', '') }}.ps1"; schtasks /create /tn "CertDeploy-{{ scriptDomain?.domain?.replace('*.', '') }}" /tr "powershell -ExecutionPolicy Bypass -File C:\cert\deploy-{{ scriptDomain?.domain?.replace('*.', '') }}.ps1" /sc hourly /f</code>
<el-button type="primary" link size="small" :icon="DocumentCopy" @click="copyText(`powershell -ExecutionPolicy Bypass -File 'C:\\cert\\deploy-${scriptDomain?.domain?.replace('*.', '')}.ps1'; schtasks /create /tn 'CertDeploy-${scriptDomain?.domain?.replace('*.', '')}' /tr 'powershell -ExecutionPolicy Bypass -File C:\\cert\\deploy-${scriptDomain?.domain?.replace('*.', '')}.ps1' /sc hourly /f`)" />
</div>
</template>
</el-step>
</el-steps>
</template>
<p style="color: #909399; font-size: 13px; margin-bottom: 8px;">
在服务器上执行安装脚本
</p>
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;" v-if="scriptOs === 'linux'">
<code style="flex: 1; background: #1d1e1f; color: #e5eaf3; padding: 8px 12px; border-radius: 6px; font-size: 12px;">bash setup-{{ scriptDomain?.domain?.replace('*.', '') }}.sh</code>
<el-button type="primary" link size="small" :icon="DocumentCopy" @click="copyText(`bash setup-${scriptDomain?.domain?.replace('*.', '')}.sh`)" />
</div>
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;" v-else>
<code style="flex: 1; background: #1d1e1f; color: #e5eaf3; padding: 8px 12px; border-radius: 6px; font-size: 12px;">powershell -ExecutionPolicy Bypass -File setup-{{ scriptDomain?.domain?.replace('*.', '') }}.ps1</code>
<el-button type="primary" link size="small" :icon="DocumentCopy" @click="copyText(`powershell -ExecutionPolicy Bypass -File setup-${scriptDomain?.domain?.replace('*.', '')}.ps1`)" />
</div>
<!-- 脚本内容 -->
<div style="display: flex; align-items: center; justify-content: space-between; margin: 16px 0 8px;">
@@ -213,8 +179,6 @@ const scriptDialogVisible = ref(false)
const scriptDomain = ref(null)
const scriptOs = ref('linux')
const scriptContent = ref('')
const scriptDownloadUrl = ref('')
const adminToken = ref(localStorage.getItem('admin_token') || '')
const load = async () => {
const { data } = await getDomains()
@@ -279,10 +243,6 @@ const openScript = async (row) => {
scriptDomain.value = row
scriptOs.value = 'linux'
scriptContent.value = ''
// 构建脚本下载 URL(供 curl 使用)
const base = window.location.origin
const params = new URLSearchParams({ domain: row.domain, server_name: row.server_name, os: 'linux' })
scriptDownloadUrl.value = `${base}/admin/api/script/download?${params}`
scriptDialogVisible.value = true
await loadScript()
}
@@ -297,10 +257,6 @@ const loadScript = async () => {
},
})
scriptContent.value = resp.data.script
// 更新下载 URL 的 os 参数
const base = window.location.origin
const params = new URLSearchParams({ domain: scriptDomain.value.domain, server_name: scriptDomain.value.server_name, os: scriptOs.value })
scriptDownloadUrl.value = `${base}/admin/api/script/download?${params}`
} catch (e) {
scriptContent.value = '# 加载失败: ' + (e.response?.data?.detail || e.message)
}
@@ -328,6 +284,28 @@ const downloadScript = () => {
URL.revokeObjectURL(link.href)
}
const downloadSetupScript = async () => {
try {
const resp = await api.get('/script/setup', {
params: {
domain: scriptDomain.value.domain,
server_name: scriptDomain.value.server_name,
os: scriptOs.value,
},
responseType: 'blob',
})
const ext = scriptOs.value === 'windows' ? '.ps1' : '.sh'
const blob = new Blob([resp.data], { type: 'text/plain' })
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = `setup-${scriptDomain.value.domain.replace('*.', '')}${ext}`
link.click()
URL.revokeObjectURL(link.href)
} catch (e) {
ElMessage.error('下载安装脚本失败')
}
}
// ---- 证书下载 ----
const handleCertAction = async (cmd, row) => {
try {