脚本一键化

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_logs,
list_acme_configs, get_acme_config, create_acme_config, update_acme_config, delete_acme_config, 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, 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) 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/cert-download/{domain_id}/{file_type}")(download_cert_file)
app.get("/admin/api/script")(admin_generate_script) app.get("/admin/api/script")(admin_generate_script)
app.get("/admin/api/script/download")(admin_download_script) app.get("/admin/api/script/download")(admin_download_script)
app.get("/admin/api/script/setup")(admin_setup_script)
logger.info("=== 路由注册完成 ===") logger.info("=== 路由注册完成 ===")
for route in app.routes: for route in app.routes:
+43 -2
View File
@@ -1,5 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException, Header, Cookie from fastapi import APIRouter, Depends, HTTPException, Header, Cookie, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse, PlainTextResponse
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func from sqlalchemy import select, func
from sqlalchemy.orm import selectinload 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}") @router.get("/cert-info/{domain_id}")
+46 -68
View File
@@ -111,65 +111,31 @@
</el-radio-group> </el-radio-group>
</div> </div>
<!-- Linux 部署步骤 --> <!-- 一键安装 -->
<template v-if="scriptOs === 'linux'"> <el-card shadow="never" style="margin-bottom: 20px; background: #f0f9eb; border-color: #e1f3d8;">
<el-steps direction="vertical" :active="3" style="margin-bottom: 12px;"> <div style="display: flex; align-items: center; justify-content: space-between;">
<el-step title="下载脚本" status="process"> <div>
<template #description> <div style="font-weight: 600; font-size: 15px; margin-bottom: 4px;">一键安装</div>
<div style="display: flex; align-items: center; gap: 8px; margin-top: 4px;"> <div style="color: #909399; font-size: 13px;">下载安装脚本到服务器执行自动完成脚本下载提权首次执行和定时任务配置</div>
<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> </div>
<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`)" /> <el-button type="success" round @click="downloadSetupScript">
</div> <el-icon style="margin-right: 4px;"><Download /></el-icon>
</template> 下载安装脚本
</el-step> </el-button>
<el-step title="赋予执行权限" status="process"> </div>
<template #description> </el-card>
<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>
<!-- Windows 部署步骤 --> <p style="color: #909399; font-size: 13px; margin-bottom: 8px;">
<template v-if="scriptOs === 'windows'"> 在服务器上执行安装脚本
<el-steps direction="vertical" :active="3" style="margin-bottom: 12px;"> </p>
<el-step title="下载脚本" status="process"> <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;" v-if="scriptOs === 'linux'">
<template #description> <code style="flex: 1; background: #1d1e1f; color: #e5eaf3; padding: 8px 12px; border-radius: 6px; font-size: 12px;">bash setup-{{ scriptDomain?.domain?.replace('*.', '') }}.sh</code>
<div style="display: flex; align-items: center; gap: 8px; margin-top: 4px;"> <el-button type="primary" link size="small" :icon="DocumentCopy" @click="copyText(`bash setup-${scriptDomain?.domain?.replace('*.', '')}.sh`)" />
<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> </div>
<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 style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;" v-else>
</div> <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>
</template> <el-button type="primary" link size="small" :icon="DocumentCopy" @click="copyText(`powershell -ExecutionPolicy Bypass -File setup-${scriptDomain?.domain?.replace('*.', '')}.ps1`)" />
</el-step> </div>
<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>
<!-- 脚本内容 --> <!-- 脚本内容 -->
<div style="display: flex; align-items: center; justify-content: space-between; margin: 16px 0 8px;"> <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 scriptDomain = ref(null)
const scriptOs = ref('linux') const scriptOs = ref('linux')
const scriptContent = ref('') const scriptContent = ref('')
const scriptDownloadUrl = ref('')
const adminToken = ref(localStorage.getItem('admin_token') || '')
const load = async () => { const load = async () => {
const { data } = await getDomains() const { data } = await getDomains()
@@ -279,10 +243,6 @@ const openScript = async (row) => {
scriptDomain.value = row scriptDomain.value = row
scriptOs.value = 'linux' scriptOs.value = 'linux'
scriptContent.value = '' 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 scriptDialogVisible.value = true
await loadScript() await loadScript()
} }
@@ -297,10 +257,6 @@ const loadScript = async () => {
}, },
}) })
scriptContent.value = resp.data.script 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) { } catch (e) {
scriptContent.value = '# 加载失败: ' + (e.response?.data?.detail || e.message) scriptContent.value = '# 加载失败: ' + (e.response?.data?.detail || e.message)
} }
@@ -328,6 +284,28 @@ const downloadScript = () => {
URL.revokeObjectURL(link.href) 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) => { const handleCertAction = async (cmd, row) => {
try { try {