|
|
@@ -1124,6 +1124,28 @@
|
|
|
) {
|
|
|
softChanged = true;
|
|
|
}
|
|
|
+ if (
|
|
|
+ prev &&
|
|
|
+ (prev === "running" || prev === "pending") &&
|
|
|
+ now === "success" &&
|
|
|
+ (t.type || "") === "ssl.letsencrypt"
|
|
|
+ ) {
|
|
|
+ softChanged = true;
|
|
|
+ if (state.siteSettings && state.siteSettingsModal) {
|
|
|
+ const name = state.siteSettings.name;
|
|
|
+ refresh().then(() => {
|
|
|
+ const latest = state.sites.find((s) => s.name === name);
|
|
|
+ if (latest) {
|
|
|
+ Object.assign(state.siteSettings, latest);
|
|
|
+ fillSiteSettingsForm(state.siteSettings);
|
|
|
+ $("#site-set-ssl-enable").checked = true;
|
|
|
+ setSiteCertSubTab("pem");
|
|
|
+ loadSiteSslPem().catch(() => {});
|
|
|
+ toast("证书已申请并写入本站 ssl/");
|
|
|
+ }
|
|
|
+ }).catch(() => {});
|
|
|
+ }
|
|
|
+ }
|
|
|
state.taskStatusMap[t.id] = now;
|
|
|
}
|
|
|
state.tasks = tasks;
|
|
|
@@ -1549,11 +1571,62 @@
|
|
|
function setSiteSettingsTab(tab) {
|
|
|
$$(".site-settings-tab").forEach((b) => b.classList.toggle("is-active", b.dataset.siteTab === tab));
|
|
|
$$(".site-settings-panel").forEach((p) => p.classList.toggle("is-active", p.dataset.sitePanel === tab));
|
|
|
- if (tab === "cert") renderSslDomainChecklist(selectedSslHosts());
|
|
|
+ if (tab === "cert") {
|
|
|
+ setSiteCertSubTab(state.siteCertTab || "pem");
|
|
|
+ loadSiteSslPem().catch(() => {});
|
|
|
+ renderSslDomainChecklist(selectedSslHosts());
|
|
|
+ }
|
|
|
if (tab === "nginx") loadSiteNginxConf().catch((err) => toast(err.message || String(err), "err"));
|
|
|
if (tab === "logs") loadSiteLog().catch((err) => toast(err.message || String(err), "err"));
|
|
|
}
|
|
|
|
|
|
+ function setSiteCertSubTab(tab) {
|
|
|
+ const which = tab === "apply" ? "apply" : "pem";
|
|
|
+ state.siteCertTab = which;
|
|
|
+ $("#site-cert-tab-pem")?.classList.toggle("active", which === "pem");
|
|
|
+ $("#site-cert-tab-apply")?.classList.toggle("active", which === "apply");
|
|
|
+ const pem = $("#site-cert-pane-pem");
|
|
|
+ const apply = $("#site-cert-pane-apply");
|
|
|
+ if (pem) pem.hidden = which !== "pem";
|
|
|
+ if (apply) apply.hidden = which !== "apply";
|
|
|
+ if (which === "apply") renderSslDomainChecklist(selectedSslHosts());
|
|
|
+ }
|
|
|
+
|
|
|
+ function siteSslPaths(site) {
|
|
|
+ const sslDir = joinPath(site.root || "", "ssl");
|
|
|
+ return {
|
|
|
+ dir: sslDir,
|
|
|
+ cert: joinPath(sslDir, "fullchain.pem"),
|
|
|
+ key: joinPath(sslDir, "privkey.pem"),
|
|
|
+ readCert: site.ssl_cert || joinPath(sslDir, "fullchain.pem"),
|
|
|
+ readKey: site.ssl_key || joinPath(sslDir, "privkey.pem"),
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ async function readFileText(path) {
|
|
|
+ if (!path) return "";
|
|
|
+ try {
|
|
|
+ const data = await api(`/api/files/read?path=${encodeURIComponent(path)}`);
|
|
|
+ return (data && data.content) || "";
|
|
|
+ } catch (_) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async function loadSiteSslPem() {
|
|
|
+ const site = state.siteSettings;
|
|
|
+ if (!site || !site.root) return;
|
|
|
+ const paths = siteSslPaths(site);
|
|
|
+ const [pem, key] = await Promise.all([
|
|
|
+ readFileText(paths.readCert),
|
|
|
+ readFileText(paths.readKey),
|
|
|
+ ]);
|
|
|
+ const pemEl = $("#site-set-ssl-pem");
|
|
|
+ const keyEl = $("#site-set-ssl-key-pem");
|
|
|
+ if (pemEl) pemEl.value = pem;
|
|
|
+ if (keyEl) keyEl.value = key;
|
|
|
+ }
|
|
|
+
|
|
|
function domainsFromText(text) {
|
|
|
return String(text || "")
|
|
|
.split(/[\s,;]+/)
|
|
|
@@ -1635,8 +1708,6 @@
|
|
|
$("#site-set-domain").value = domains.join("\n");
|
|
|
$("#site-set-ssl-enable").checked = !!site.ssl_enable;
|
|
|
$("#site-set-ssl-port").value = site.ssl_port || 443;
|
|
|
- $("#site-set-ssl-cert").value = site.ssl_cert || "";
|
|
|
- $("#site-set-ssl-key").value = site.ssl_key || "";
|
|
|
$("#site-set-conf-path").textContent = site.conf || "";
|
|
|
$("#site-set-nginx-conf").value = "";
|
|
|
state.siteLogKind = "access";
|
|
|
@@ -1650,6 +1721,7 @@
|
|
|
const site = state.sites.find((s) => s.name === name);
|
|
|
if (!site) throw new Error("未找到网站");
|
|
|
fillSiteSettingsForm(site);
|
|
|
+ state.siteCertTab = "pem";
|
|
|
setSiteSettingsTab("domain");
|
|
|
state.siteSettingsModal.show();
|
|
|
}
|
|
|
@@ -1775,12 +1847,29 @@
|
|
|
|
|
|
async function saveSiteCert() {
|
|
|
const site = state.siteSettings;
|
|
|
- if (!site) return;
|
|
|
+ if (!site || !site.root) return;
|
|
|
const domains = domainsFromText($("#site-set-domain").value);
|
|
|
const ssl_enable = $("#site-set-ssl-enable").checked;
|
|
|
const ssl_port = Number($("#site-set-ssl-port").value || 443);
|
|
|
- const ssl_cert = $("#site-set-ssl-cert").value.trim();
|
|
|
- const ssl_key = $("#site-set-ssl-key").value.trim();
|
|
|
+ const pem = ($("#site-set-ssl-pem")?.value || "").trim();
|
|
|
+ const key = ($("#site-set-ssl-key-pem")?.value || "").trim();
|
|
|
+ if (ssl_enable && (!pem || !key)) {
|
|
|
+ throw new Error("启用 HTTPS 时请填写证书 PEM 与私钥 KEY");
|
|
|
+ }
|
|
|
+ const paths = siteSslPaths(site);
|
|
|
+ await api("/api/files/mkdir", { method: "POST", body: { path: paths.dir } }).catch(() => {});
|
|
|
+ if (pem) {
|
|
|
+ await api("/api/files/write", {
|
|
|
+ method: "POST",
|
|
|
+ body: { path: paths.cert, content: pem.endsWith("\n") ? pem : pem + "\n" },
|
|
|
+ });
|
|
|
+ }
|
|
|
+ if (key) {
|
|
|
+ await api("/api/files/write", {
|
|
|
+ method: "POST",
|
|
|
+ body: { path: paths.key, content: key.endsWith("\n") ? key : key + "\n" },
|
|
|
+ });
|
|
|
+ }
|
|
|
const updated = await api("/api/websites/update", {
|
|
|
method: "POST",
|
|
|
body: {
|
|
|
@@ -1788,13 +1877,14 @@
|
|
|
domains: domains.length ? domains : domainsFromText(site.domain),
|
|
|
ssl_enable,
|
|
|
ssl_port,
|
|
|
- ssl_cert,
|
|
|
- ssl_key,
|
|
|
+ ssl_cert: paths.cert,
|
|
|
+ ssl_key: paths.key,
|
|
|
},
|
|
|
});
|
|
|
Object.assign(site, updated || {});
|
|
|
fillSiteSettingsForm(site);
|
|
|
- toast("证书设置已保存");
|
|
|
+ await loadSiteSslPem();
|
|
|
+ toast("证书已保存并重载");
|
|
|
await refresh();
|
|
|
}
|
|
|
|
|
|
@@ -1817,21 +1907,6 @@
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- async function uploadSiteSslFile(kind, file) {
|
|
|
- const site = state.siteSettings;
|
|
|
- if (!site || !site.root) throw new Error("网站目录未知");
|
|
|
- const sslDir = joinPath(site.root, "ssl");
|
|
|
- await api("/api/files/mkdir", { method: "POST", body: { path: sslDir } }).catch(() => {});
|
|
|
- const dest = joinPath(sslDir, file.name);
|
|
|
- await api("/api/files/upload", {
|
|
|
- method: "POST",
|
|
|
- body: { path: dest, content_base64: await fileToBase64(file) },
|
|
|
- });
|
|
|
- if (kind === "cert") $("#site-set-ssl-cert").value = dest;
|
|
|
- else $("#site-set-ssl-key").value = dest;
|
|
|
- toast("证书文件已上传");
|
|
|
- }
|
|
|
-
|
|
|
function destroyAnalyticsCharts() {
|
|
|
(state.analyticsCharts || []).forEach((c) => {
|
|
|
try { c.destroy(); } catch (_) {}
|
|
|
@@ -2142,6 +2217,11 @@
|
|
|
$("#site-set-cert-save").addEventListener("click", (e) => {
|
|
|
withBusy(async () => saveSiteCert(), e.currentTarget);
|
|
|
});
|
|
|
+ $("#site-cert-tab-pem").addEventListener("click", () => {
|
|
|
+ setSiteCertSubTab("pem");
|
|
|
+ loadSiteSslPem().catch(() => {});
|
|
|
+ });
|
|
|
+ $("#site-cert-tab-apply").addEventListener("click", () => setSiteCertSubTab("apply"));
|
|
|
$("#site-set-ssl-apply").addEventListener("click", (e) => {
|
|
|
withBusy(async () => applyLetsEncrypt(), e.currentTarget);
|
|
|
});
|
|
|
@@ -2181,21 +2261,6 @@
|
|
|
state.filesPath = site.log_dir;
|
|
|
setView("files");
|
|
|
});
|
|
|
- $("#site-set-ssl-cert-upload").addEventListener("click", () => {
|
|
|
- state.siteSslUploadTarget = "cert";
|
|
|
- $("#site-ssl-file-input").click();
|
|
|
- });
|
|
|
- $("#site-set-ssl-key-upload").addEventListener("click", () => {
|
|
|
- state.siteSslUploadTarget = "key";
|
|
|
- $("#site-ssl-file-input").click();
|
|
|
- });
|
|
|
- $("#site-ssl-file-input").addEventListener("change", async (e) => {
|
|
|
- const file = e.target.files && e.target.files[0];
|
|
|
- const kind = state.siteSslUploadTarget;
|
|
|
- e.target.value = "";
|
|
|
- if (!file || !kind) return;
|
|
|
- await withBusy(async () => uploadSiteSslFile(kind, file));
|
|
|
- });
|
|
|
}
|
|
|
|
|
|
function bindEvents() {
|