From 700db4efb9118a27b5a9add2de3daf23904cfc9f Mon Sep 17 00:00:00 2001 From: sans Date: Sun, 19 Jul 2026 00:21:23 +0800 Subject: [PATCH] =?UTF-8?q?=E8=84=9A=E6=9C=AC=E4=B8=80=E9=94=AE=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/main.py | 3 +- backend/routers/admin.py | 45 ++++++++++++- frontend/src/views/Domains.vue | 114 +++++++++++++-------------------- 3 files changed, 91 insertions(+), 71 deletions(-) diff --git a/backend/main.py b/backend/main.py index fec0dc8..1d488a1 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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: diff --git a/backend/routers/admin.py b/backend/routers/admin.py index 2ba1d5d..da2fd68 100644 --- a/backend/routers/admin.py +++ b/backend/routers/admin.py @@ -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}") diff --git a/frontend/src/views/Domains.vue b/frontend/src/views/Domains.vue index af6f93b..3420e72 100644 --- a/frontend/src/views/Domains.vue +++ b/frontend/src/views/Domains.vue @@ -111,65 +111,31 @@ - - + + +
+
+
一键安装
+
下载安装脚本到服务器执行,自动完成脚本下载、提权、首次执行和定时任务配置
+
+ + + 下载安装脚本 + +
+
- - +

+ 在服务器上执行安装脚本: +

+
+ bash setup-{{ scriptDomain?.domain?.replace('*.', '') }}.sh + +
+
+ powershell -ExecutionPolicy Bypass -File setup-{{ scriptDomain?.domain?.replace('*.', '') }}.ps1 + +
@@ -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 {