From 7cff1a98424120585205889874b3ef4956a30583 Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Tue, 30 Jun 2026 23:41:59 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E3=80=90=E7=A0=B4=E5=9D=8F=E6=80=A7?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E3=80=91=20=E8=AF=81=E4=B9=A6=E5=8E=8B?= =?UTF-8?q?=E7=BC=A9=E5=8C=85=E4=B8=8D=E5=86=8D=E7=94=9F=E6=88=90=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=AD=98=E5=82=A8=EF=BC=8C=E8=80=8C=E6=98=AF=E5=AE=9E?= =?UTF-8?q?=E6=97=B6=E6=89=93=E5=8C=85=E4=B8=8B=E8=BD=BD=EF=BC=8C=E8=AF=81?= =?UTF-8?q?=E4=B9=A6=E7=94=B3=E8=AF=B7=E6=8F=92=E4=BB=B6=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E8=BE=93=E5=87=BAcertZip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 自定义插件需要压缩包时可以调用new CertReader(certInfo).buildZip() 方式获取 --- AGENTS.md | 24 ++-- .../src/system/basic/service/plus-service.ts | 50 ++++---- packages/plugins/plugin-lib/.mocharc.json | 2 +- packages/plugins/plugin-lib/package.json | 9 +- .../plugin-lib/src/cert/cert-reader.ts | 67 +++++++++++ .../plugin-lib/test/cert-reader.test.ts | 109 ++++++++++++++++++ .../src/views/certd/monitor/cert/crud.tsx | 4 - .../src/views/certd/pipeline/use.tsx | 44 ++----- .../metadata/deploy_CertApply.yaml | 3 - .../deploy_CertApplyGetFormAliyun.yaml | 3 - .../metadata/deploy_CertApplyLego.yaml | 3 - .../metadata/deploy_CertApplyUpload.yaml | 3 - .../deploy_DeployCertToMailPlugin.yaml | 9 -- packages/ui/certd-server/src/configuration.ts | 1 - .../user/monitor/cert-info-controller.ts | 33 +++--- .../user/pipeline/cert-controller.ts | 37 +++++- .../plugin/cert-plugin/base-convert.ts | 61 +--------- .../plugins/plugin-deploy-to-mail.ts | 25 ++-- 18 files changed, 289 insertions(+), 198 deletions(-) create mode 100644 packages/plugins/plugin-lib/test/cert-reader.test.ts diff --git a/AGENTS.md b/AGENTS.md index efb9fd375..df2c07d4b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -77,6 +77,20 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web - 注释优先使用中文,尤其是业务规则、兼容逻辑、协议细节和隐藏风险;文件已有英文风格或引用外部术语时可保持一致。 - 遵守 DRY 和单一职责;第三次出现的业务规则、字段转换、权限判断、Repository 选择、事务传播、金额计算等逻辑,应优先抽成合适 helper 或 service 方法。 + +## 测试与验证 + +- 务必写单元测试,覆盖主要业务逻辑。 +- 实现新功能或修复行为缺陷前,优先补单元测试并先确认红灯,再实现并跑聚焦验证。 +- 确实不适合先写测试时,在回复中说明原因和替代验证方式。 +- 后补单元测试时,按正确行为写预期;若红灯需要修改既有实现,先向用户确认这是 bug 还是既有需求,避免未经确认改变行为。 +- 后端纯单测放在 `src/**/*.test.ts`,尽量与被测文件相邻;`test:unit` 只跑这些文件,构建/打包应排除 `*.test.ts`。 +- 单测需要 mock ESM 静态 import 时,优先使用 `esmock`,不要为了测试改业务代码结构。 +- 各包 `test:unit` 脚本应显式设置 `NODE_ENV=unittest`。 +- 单包单测优先用 `cd <包目录> && npm run test:unit`,例如 `cd packages\ui\certd-server && npm run test:unit`。 +- 优先对改动包运行聚焦测试或格式化/ESLint;只有跨包影响明显时再考虑更大范围构建。 + + ## 后端规则 - 后端主包是 `packages/ui/certd-server`,使用 Node.js、ESM、TypeScript、MidwayJS 3、Koa、TypeORM 和 SQL 迁移。 @@ -196,13 +210,3 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web - 后端业务数据、接口、实体、权限、迁移:改 `packages/ui/certd-server/src/modules` 与 `src/controller`。 - 表单、列表、插件配置 UI:改 `packages/ui/certd-client/src/views/certd` 及对应 `src/api`。 -## 测试与验证 - -- 实现新功能或修复行为缺陷前,优先补单元测试并先确认红灯,再实现并跑聚焦验证。 -- 确实不适合先写测试时,在回复中说明原因和替代验证方式。 -- 后补单元测试时,按正确行为写预期;若红灯需要修改既有实现,先向用户确认这是 bug 还是既有需求,避免未经确认改变行为。 -- 后端纯单测放在 `src/**/*.test.ts`,尽量与被测文件相邻;`test:unit` 只跑这些文件,构建/打包应排除 `*.test.ts`。 -- 单测需要 mock ESM 静态 import 时,优先使用 `esmock`,不要为了测试改业务代码结构。 -- 各包 `test:unit` 脚本应显式设置 `NODE_ENV=unittest`。 -- 单包单测优先用 `cd <包目录> && npm run test:unit`,例如 `cd packages\ui\certd-server && npm run test:unit`。 -- 优先对改动包运行聚焦测试或格式化/ESLint;只有跨包影响明显时再考虑更大范围构建。 diff --git a/packages/libs/lib-server/src/system/basic/service/plus-service.ts b/packages/libs/lib-server/src/system/basic/service/plus-service.ts index 71d3b1c43..ad5b9c1ad 100644 --- a/packages/libs/lib-server/src/system/basic/service/plus-service.ts +++ b/packages/libs/lib-server/src/system/basic/service/plus-service.ts @@ -1,9 +1,9 @@ -import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core'; -import { AppKey, PlusRequestService } from '@certd/plus-core'; -import { cache, http, HttpRequestConfig, logger } from '@certd/basic'; -import { SysInstallInfo, SysLicenseInfo, SysSettingsService } from '../../settings/index.js'; -import { merge } from 'lodash-es'; -import fs from 'fs'; +import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core"; +import { AppKey, PlusRequestService } from "@certd/plus-core"; +import { cache, http, HttpRequestConfig, logger } from "@certd/basic"; +import { SysInstallInfo, SysLicenseInfo, SysSettingsService } from "../../settings/index.js"; +import { merge } from "lodash-es"; +import fs from "fs"; @Provide("plusService") @Scope(ScopeEnum.Request, { allowDowngrade: true }) export class PlusService { @@ -54,9 +54,9 @@ export class PlusService { await plusRequestService.verify({ license: licenseInfo.license }); } - async bindUrl(url: string, url2?:string) { + async bindUrl(url: string, url2?: string) { const plusRequestService = await this.getPlusRequestService(); - const res = await plusRequestService.bindUrl(url,url2); + const res = await plusRequestService.bindUrl(url, url2); this.plusRequestService = null; return res; } @@ -66,7 +66,7 @@ export class PlusService { const licenseInfo: SysLicenseInfo = await this.sysSettingsService.getSetting(SysLicenseInfo); if (!licenseInfo.license) { await plusRequestService.register(); - logger.info('站点注册成功'); + logger.info("站点注册成功"); this.plusRequestService = null; } } @@ -74,8 +74,8 @@ export class PlusService { async userPreBind(userId: number) { const plusRequestService = await this.getPlusRequestService(); await plusRequestService.requestWithoutSign({ - url: '/activation/subject/preBind', - method: 'POST', + url: "/activation/subject/preBind", + method: "POST", data: { userId, appKey: AppKey, @@ -91,9 +91,9 @@ export class PlusService { if (attachments.length > 0) { const newAttachments: any[] = []; attachments.forEach((item: any) => { - const name = item.filename || item.path.split('/').pop(); + const name = item.filename || item.path.split("/").pop(); const body = item.content || fs.readFileSync(item.path); - const bodyBase64 = Buffer.from(body).toString('base64'); + const bodyBase64 = Buffer.from(body).toString("base64"); item = { name, body: bodyBase64, @@ -104,7 +104,7 @@ export class PlusService { } await plusRequestService.request({ - url: '/activation/emailSend', + url: "/activation/emailSend", data: { subject: email.subject, to: email.receivers, @@ -116,7 +116,7 @@ export class PlusService { } async getAccessToken() { - const cacheKey = 'certd:subject:access_token'; + const cacheKey = "certd:subject:access_token"; const token = cache.get(cacheKey); if (token) { return token; @@ -131,15 +131,15 @@ export class PlusService { return res.accessToken; } - async getVipTrial(vipType= "plus") { + async getVipTrial(vipType = "plus") { await this.register(); const plusRequestService = await this.getPlusRequestService(); const res = await plusRequestService.request({ - url: '/activation/subject/vip/trialGet', - method: 'POST', - data:{ - vipType - } + url: "/activation/subject/vip/trialGet", + method: "POST", + data: { + vipType, + }, }); if (res.license) { await this.updateLicense(res.license); @@ -147,14 +147,14 @@ export class PlusService { duration: res.duration, }; } else { - throw new Error('您已经领取过VIP试用了'); + throw new Error("您已经领取过VIP试用了"); } } - async getTodayOrderCount () { + async getTodayOrderCount() { await this.register(); const plusRequestService = await this.getPlusRequestService(); - return await plusRequestService.getOrderCount() + return await plusRequestService.getOrderCount(); } async requestWithToken(config: HttpRequestConfig) { @@ -162,7 +162,7 @@ export class PlusService { const token = await this.getAccessToken(); merge(config, { baseURL: plusRequestService.getBaseURL(), - method: 'post', + method: "post", headers: { Authorization: `Berear ${token}`, }, diff --git a/packages/plugins/plugin-lib/.mocharc.json b/packages/plugins/plugin-lib/.mocharc.json index 24b75f3e1..afecbe905 100644 --- a/packages/plugins/plugin-lib/.mocharc.json +++ b/packages/plugins/plugin-lib/.mocharc.json @@ -1,5 +1,5 @@ { "extension": ["ts"], "spec": "test/**/*.test.ts", - "require": "ts-node/register" + "node-option": ["loader=ts-node/esm", "no-warnings"] } \ No newline at end of file diff --git a/packages/plugins/plugin-lib/package.json b/packages/plugins/plugin-lib/package.json index 127cf6b6e..e93160db9 100644 --- a/packages/plugins/plugin-lib/package.json +++ b/packages/plugins/plugin-lib/package.json @@ -10,10 +10,7 @@ "before-build": "rimraf dist && rimraf tsconfig.tsbuildinfo && rimraf .rollup.cache", "build": "npm run before-build && tsc -p tsconfig.build.json --skipLibCheck", "dev-build": "npm run build", - "build3": "rollup -c", - "build2": "vue-tsc --noEmit && vite build", - "preview": "vite preview", - "test:unit": "cross-env NODE_ENV=unittest mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"", + "test:unit": "cross-env NODE_ENV=unittest mocha", "pub": "npm publish", "compile": "tsc --skipLibCheck --watch", "format": "prettier --write src", @@ -24,12 +21,13 @@ "@certd/basic": "^1.41.4", "@certd/pipeline": "^1.41.4", "dayjs": "^1.11.7", + "jszip": "^3.10.1", "lodash-es": "^4.17.21", "psl": "^1.15.0", "punycode.js": "^2.3.1" }, "devDependencies": { - "rimraf": "^5.0.5", + "rimraf": "^5.0.5", "@types/chai": "^4.3.12", "@types/mocha": "^10.0.6", "@typescript-eslint/eslint-plugin": "^8.26.1", @@ -41,6 +39,7 @@ "eslint-plugin-prettier": "^5.1.3", "esmock": "^2.7.5", "mocha": "^10.6.0", + "node-forge": "^1.3.1", "prettier": "3.3.3", "ts-node": "^10.9.2", "tslib": "^2.8.1", diff --git a/packages/plugins/plugin-lib/src/cert/cert-reader.ts b/packages/plugins/plugin-lib/src/cert/cert-reader.ts index 49cfb8c63..f748ed769 100644 --- a/packages/plugins/plugin-lib/src/cert/cert-reader.ts +++ b/packages/plugins/plugin-lib/src/cert/cert-reader.ts @@ -6,6 +6,7 @@ import cryptoLib from "crypto"; import { ILogger } from "@certd/basic"; import dayjs from "dayjs"; import { uniq } from "lodash-es"; +import JSZip from "jszip"; export interface ICertInfoGetter { getByPipelineId: (pipelineId: number) => Promise; @@ -301,4 +302,70 @@ export class CertReader { static buildCertName(cert: CertInfo, useHash: boolean = false) { return new CertReader(cert).buildCertName("", useHash); } + + async buildZip(): Promise { + const cert = this.cert; + + const zip = new JSZip(); + + if (cert.crt) { + zip.file("证书.pem", cert.crt); + } + if (cert.key) { + zip.file("私钥.pem", cert.key); + } + if (cert.ic) { + zip.file("中间证书.pem", cert.ic); + } + if (cert.crt) { + zip.file("cert.crt", cert.crt); + } + if (cert.key) { + zip.file("cert.key", cert.key); + } + if (cert.ic) { + zip.file("intermediate.crt", cert.ic); + } + if (cert.oc) { + zip.file("origin.crt", cert.oc); + } + if (cert.one) { + zip.file("one.pem", cert.one); + } + if (cert.p7b) { + zip.file("cert.p7b", cert.p7b); + } + if (cert.pfx) { + zip.file("cert.pfx", Buffer.from(cert.pfx, "base64")); + } + if (cert.der) { + zip.file("cert.der", Buffer.from(cert.der, "base64")); + } + if (cert.jks) { + zip.file("cert.jks", Buffer.from(cert.jks, "base64")); + } + + zip.file( + "说明.txt", + `证书文件说明 +cert.crt:证书文件,包含证书链,pem格式 +cert.key:私钥文件,pem格式 +intermediate.crt:中间证书文件,pem格式 +origin.crt:原始证书文件,不含证书链,pem格式 +one.pem: 证书和私钥简单合并成一个文件,pem格式,crt正文+key正文 +cert.pfx:pfx格式证书文件,iis服务器使用 +cert.der:der格式证书文件 +cert.jks:jks格式证书文件,java服务器使用 + ` + ); + + return zip.generateAsync({ type: "nodebuffer" }); + } + + buildZipFilename(prefix = "cert"): string { + let domain = this.getMainDomain(); + domain = domain.replaceAll(".", "_").replaceAll("*", "_"); + const timeStr = dayjs().format("YYYYMMDDHHmmss"); + return `${prefix}_${domain}_${timeStr}.zip`; + } } diff --git a/packages/plugins/plugin-lib/test/cert-reader.test.ts b/packages/plugins/plugin-lib/test/cert-reader.test.ts new file mode 100644 index 000000000..0d0d123f2 --- /dev/null +++ b/packages/plugins/plugin-lib/test/cert-reader.test.ts @@ -0,0 +1,109 @@ +/// +/// + +import assert from "node:assert/strict"; + +import { CertReader } from "../src/cert/cert-reader.js"; +import type { CertInfo } from "../src/cert/cert-reader.js"; + +// @ts-ignore +import forge from "node-forge"; + +/** + * Generate a minimal self-signed X.509 cert + key in PEM format for testing. + */ +function createSelfSignedCert(commonName: string): { crt: string; key: string } { + const keypair = forge.pki.rsa.generateKeyPair(2048); + + const cert = forge.pki.createCertificate(); + cert.publicKey = keypair.publicKey; + cert.serialNumber = "01"; + cert.validFrom = new Date("2025-01-01").toISOString(); + cert.validTo = new Date("2026-01-01").toISOString(); + + const attrs = [{ name: "commonName", value: commonName }]; + cert.setSubject(attrs); + cert.setIssuer(attrs); + cert.sign(keypair.privateKey, forge.md.sha256.create()); + + return { + crt: forge.pki.certificateToPem(cert), + key: forge.pki.privateKeyToPem(keypair.privateKey), + }; +} + +const testCert = createSelfSignedCert("example.com"); +const testCertIc = createSelfSignedCert("intermediate.ca"); +const mockCertInfo: CertInfo = { + crt: testCert.crt + "\n" + testCertIc.crt, + key: testCert.key, + oc: testCert.crt, + ic: testCertIc.crt, + one: testCert.crt + "\n" + testCert.key, + p7b: "PKCS7 test content", + pfx: Buffer.from("fake-pfx-data").toString("base64"), + der: Buffer.from("fake-der-data").toString("base64"), + jks: Buffer.from("fake-jks-data").toString("base64"), +}; + +describe("CertReader.buildZip", () => { + it("returns a non-empty Buffer", async () => { + const reader = new CertReader(mockCertInfo); + const buf = await reader.buildZip(); + assert.ok(Buffer.isBuffer(buf)); + assert.ok(buf.length > 0); + }); + + it("produces a valid zip containing expected files", async () => { + const reader = new CertReader(mockCertInfo); + const buf = await reader.buildZip(); + const { default: JSZip } = await import("jszip"); + const zip = await JSZip.loadAsync(buf); + + assert.ok(zip.file("证书.pem"), "should contain 证书.pem"); + assert.ok(zip.file("私钥.pem"), "should contain 私钥.pem"); + assert.ok(zip.file("中间证书.pem"), "should contain 中间证书.pem"); + assert.ok(zip.file("cert.crt"), "should contain cert.crt"); + assert.ok(zip.file("cert.key"), "should contain cert.key"); + assert.ok(zip.file("intermediate.crt"), "should contain intermediate.crt"); + assert.ok(zip.file("origin.crt"), "should contain origin.crt"); + assert.ok(zip.file("one.pem"), "should contain one.pem"); + assert.ok(zip.file("cert.p7b"), "should contain cert.p7b"); + assert.ok(zip.file("cert.pfx"), "should contain cert.pfx"); + assert.ok(zip.file("cert.der"), "should contain cert.der"); + assert.ok(zip.file("cert.jks"), "should contain cert.jks"); + assert.ok(zip.file("说明.txt"), "should contain 说明.txt"); + + const pemContent = await zip.file("证书.pem").async("string"); + assert.ok(pemContent.includes("-----BEGIN CERTIFICATE-----")); + + const pfx = await zip.file("cert.pfx").async("nodebuffer"); + assert.equal(pfx.toString(), "fake-pfx-data"); + }); +}); + +describe("CertReader.buildZipFilename", () => { + it("includes the main domain and timestamp", () => { + const reader = new CertReader(mockCertInfo); + const name = reader.buildZipFilename("cert"); + assert.ok(name.startsWith("cert_example_com_")); + assert.ok(name.endsWith(".zip")); + const tsPart = name.replace("cert_example_com_", "").replace(".zip", ""); + assert.match(tsPart, /^\d{14}$/); + }); + + it("uses the default prefix when not provided", () => { + const reader = new CertReader(mockCertInfo); + const name = reader.buildZipFilename(); + assert.ok(name.startsWith("cert_example_com_")); + }); + + it("wildcard domain replaces asterisk", () => { + const wildcardCert = createSelfSignedCert("*.example.com"); + const wcInfo: CertInfo = { crt: wildcardCert.crt, key: wildcardCert.key }; + const reader = new CertReader(wcInfo); + const name = reader.buildZipFilename("cert"); + assert.ok(name.startsWith("cert___example_com_"), "asterisk should be replaced, got: " + name); + assert.ok(!name.includes("*")); + }); +}); \ No newline at end of file diff --git a/packages/ui/certd-client/src/views/certd/monitor/cert/crud.tsx b/packages/ui/certd-client/src/views/certd/monitor/cert/crud.tsx index 69b5d44ed..9ffd90f3f 100644 --- a/packages/ui/certd-client/src/views/certd/monitor/cert/crud.tsx +++ b/packages/ui/certd-client/src/views/certd/monitor/cert/crud.tsx @@ -138,10 +138,6 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat type: "link", icon: "ant-design:download-outlined", async click({ row }) { - if (!row.certFile) { - notification.error({ message: t("certd.certificateNotGenerated") }); - return; - } let url = "/api/monitor/cert/download?id=" + row.id; if (projectStore.isEnterprise) { url += `&projectId=${projectStore.currentProject?.id}`; diff --git a/packages/ui/certd-client/src/views/certd/pipeline/use.tsx b/packages/ui/certd-client/src/views/certd/pipeline/use.tsx index 889dfecfa..b9231d962 100644 --- a/packages/ui/certd-client/src/views/certd/pipeline/use.tsx +++ b/packages/ui/certd-client/src/views/certd/pipeline/use.tsx @@ -1,10 +1,10 @@ -import * as api from "/@/views/certd/pipeline/api"; import { notification } from "ant-design-vue"; import CertView from "/@/views/certd/pipeline/cert-view.vue"; import { env } from "/@/utils/util.env"; import { useModal } from "/@/use/use-modal"; import { useProjectStore } from "/@/store/project"; import { useUserStore } from "/@/store/user"; +import * as api from "/@/views/certd/pipeline/api"; export function useCertViewer() { const projectStore = useProjectStore(); @@ -29,42 +29,12 @@ export function useCertViewer() { }; const downloadCert = async (id: any) => { - const files = await api.GetFiles(id); - model.success({ - title: "点击链接下载", - maskClosable: true, - okText: "关闭", - content: () => { - const children = []; - for (const file of files) { - let downloadUrl = `${env.API}/pi/history/download?pipelineId=${id}&fileId=${file.id}`; - if (projectStore.isEnterprise) { - downloadUrl += `&projectId=${projectStore.currentProject?.id}`; - } - downloadUrl += `&token=${userStore.getToken}`; - children.push( - - ); - } - - if (children.length === 0) { - return
暂无文件下载
; - } - - return ( -
-
{children}
-
- ); - }, - }); + let downloadUrl = `${env.API}/pi/cert/downloadZip?id=${id}`; + if (projectStore.isEnterprise) { + downloadUrl += `&projectId=${projectStore.currentProject?.id}`; + } + downloadUrl += `&token=${userStore.getToken}`; + window.open(downloadUrl); }; return { viewCert, diff --git a/packages/ui/certd-server/metadata/deploy_CertApply.yaml b/packages/ui/certd-server/metadata/deploy_CertApply.yaml index a5bc07159..76ae82505 100644 --- a/packages/ui/certd-server/metadata/deploy_CertApply.yaml +++ b/packages/ui/certd-server/metadata/deploy_CertApply.yaml @@ -556,9 +556,6 @@ output: cert: title: 域名证书 type: cert - certZip: - title: 域名证书压缩文件 - type: certZip pluginType: deploy type: builtIn scriptFilePath: /plugins/plugin-cert/plugin/cert-plugin/apply.js diff --git a/packages/ui/certd-server/metadata/deploy_CertApplyGetFormAliyun.yaml b/packages/ui/certd-server/metadata/deploy_CertApplyGetFormAliyun.yaml index 8c91b9bfc..defdbf398 100644 --- a/packages/ui/certd-server/metadata/deploy_CertApplyGetFormAliyun.yaml +++ b/packages/ui/certd-server/metadata/deploy_CertApplyGetFormAliyun.yaml @@ -146,9 +146,6 @@ output: cert: title: 域名证书 type: cert - certZip: - title: 域名证书压缩文件 - type: certZip pluginType: deploy type: builtIn scriptFilePath: /plugins/plugin-cert/plugin/cert-plugin/getter/aliyun.js diff --git a/packages/ui/certd-server/metadata/deploy_CertApplyLego.yaml b/packages/ui/certd-server/metadata/deploy_CertApplyLego.yaml index 4931e8c60..dcf8e7ff5 100644 --- a/packages/ui/certd-server/metadata/deploy_CertApplyLego.yaml +++ b/packages/ui/certd-server/metadata/deploy_CertApplyLego.yaml @@ -168,9 +168,6 @@ output: cert: title: 域名证书 type: cert - certZip: - title: 域名证书压缩文件 - type: certZip pluginType: deploy type: builtIn scriptFilePath: /plugins/plugin-cert/plugin/cert-plugin/lego/index.js diff --git a/packages/ui/certd-server/metadata/deploy_CertApplyUpload.yaml b/packages/ui/certd-server/metadata/deploy_CertApplyUpload.yaml index 1c36ef916..bcc00965d 100644 --- a/packages/ui/certd-server/metadata/deploy_CertApplyUpload.yaml +++ b/packages/ui/certd-server/metadata/deploy_CertApplyUpload.yaml @@ -142,9 +142,6 @@ output: cert: title: 域名证书 type: cert - certZip: - title: 域名证书压缩文件 - type: certZip certMd5: title: 证书MD5 type: certMd5 diff --git a/packages/ui/certd-server/metadata/deploy_DeployCertToMailPlugin.yaml b/packages/ui/certd-server/metadata/deploy_DeployCertToMailPlugin.yaml index fda99b32c..712d0ccbc 100644 --- a/packages/ui/certd-server/metadata/deploy_DeployCertToMailPlugin.yaml +++ b/packages/ui/certd-server/metadata/deploy_DeployCertToMailPlugin.yaml @@ -17,15 +17,6 @@ input: - ':cert:' required: true order: 0 - certZip: - title: 证书压缩文件 - helper: 请选择前置任务输出的域名证书压缩文件 - component: - name: output-selector - from: - - ':certZip:' - required: true - order: 0 email: title: 接收邮箱 component: diff --git a/packages/ui/certd-server/src/configuration.ts b/packages/ui/certd-server/src/configuration.ts index b46563455..fda204f11 100644 --- a/packages/ui/certd-server/src/configuration.ts +++ b/packages/ui/certd-server/src/configuration.ts @@ -131,7 +131,6 @@ export class MainConfiguration { setLogger((text: string) => { logger.info(text); }); - logger.info("当前环境:", this.app.getEnv()); // prod } } diff --git a/packages/ui/certd-server/src/controller/user/monitor/cert-info-controller.ts b/packages/ui/certd-server/src/controller/user/monitor/cert-info-controller.ts index 9c536694a..df86f0662 100644 --- a/packages/ui/certd-server/src/controller/user/monitor/cert-info-controller.ts +++ b/packages/ui/certd-server/src/controller/user/monitor/cert-info-controller.ts @@ -5,7 +5,6 @@ import { CertInfoService } from "../../../modules/monitor/index.js"; import { PipelineService } from "../../../modules/pipeline/service/pipeline-service.js"; import { SelectQueryBuilder } from "typeorm"; import { logger } from "@certd/basic"; -import fs from "fs"; import dayjs from "dayjs"; import { ApiTags } from "@midwayjs/swagger"; import { CertReader } from "@certd/plugin-lib"; @@ -167,29 +166,29 @@ export class CertInfoController extends CrudController { @Get("/download", { description: Constants.per.authOnly, summary: "下载证书文件" }) async download(@Query("id") id: number) { const { userId, projectId } = await this.checkOwner(this.getService(), id, "read"); - const certInfo = await this.getService().info(id); - if (certInfo == null) { + const certInfoEntity = await this.getService().info(id); + if (certInfoEntity == null) { throw new CommonException("file not found"); } - if (certInfo.userId !== userId) { + if (certInfoEntity.userId !== userId) { throw new CommonException("file not found"); } - if (projectId && certInfo.projectId !== projectId) { + if (projectId && certInfoEntity.projectId !== projectId) { throw new CommonException("file not found"); } - // koa send file - // 下载文件的名称 - // const filename = file.filename; - // 要下载的文件的完整路径 - const path = certInfo.certFile; - if (!path) { - throw new CommonException("file not found"); + if (!certInfoEntity.certInfo) { + throw new CommonException("证书数据未生成"); } - logger.info(`download:${path}`); - // 以流的形式下载文件 - this.ctx.attachment(path); - this.ctx.set("Content-Type", "application/octet-stream"); - return fs.createReadStream(path); + const certInfo = JSON.parse(certInfoEntity.certInfo); + const certReader = new CertReader(certInfo); + const zipBuffer = await certReader.buildZip(); + const filename = certReader.buildZipFilename("cert"); + + logger.info(`download cert zip: ${filename}, size: ${zipBuffer.length}`); + + this.ctx.attachment(filename); + this.ctx.set("Content-Type", "application/zip"); + this.ctx.body = zipBuffer; } } diff --git a/packages/ui/certd-server/src/controller/user/pipeline/cert-controller.ts b/packages/ui/certd-server/src/controller/user/pipeline/cert-controller.ts index 21718b760..a7d9708b6 100644 --- a/packages/ui/certd-server/src/controller/user/pipeline/cert-controller.ts +++ b/packages/ui/certd-server/src/controller/user/pipeline/cert-controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/core"; +import { Body, Controller, Get, Inject, Post, Provide, Query } from "@midwayjs/core"; import { PipelineService } from "../../../modules/pipeline/service/pipeline-service.js"; import { BaseController, Constants, PermissionException } from "@certd/lib-server"; import { StorageService } from "../../../modules/pipeline/service/storage-service.js"; @@ -6,6 +6,7 @@ import { CertReader } from "@certd/plugin-cert"; import { UserSettingsService } from "../../../modules/mine/service/user-settings-service.js"; import { UserGrantSetting } from "../../../modules/mine/service/models.js"; import { ApiTags } from "@midwayjs/swagger"; +import { logger } from "@certd/basic"; @Provide() @Controller("/api/pi/cert") @@ -57,4 +58,38 @@ export class CertController extends BaseController { const certDetail = CertReader.readCertDetail(crt); return this.ok(certDetail); } + + @Get("/downloadZip", { description: Constants.per.authOnly, summary: "下载流水线证书压缩包" }) + async downloadZip(@Query("id") id: number) { + const { userId } = await this.getProjectUserIdRead(); + + const pipelineUserId = await this.pipelineService.getPipelineUserId(id); + + if (pipelineUserId !== userId) { + const isAdmin = await this.isAdmin(); + if (!isAdmin) { + throw new PermissionException(); + } + const setting = await this.userSettingsService.getSetting(pipelineUserId, null, UserGrantSetting, false); + if (setting?.allowAdminViewCerts !== true) { + throw new PermissionException("该流水线的用户还未授权管理员下载证书,请先让用户在”设置->授权委托“中打开开关"); + } + } + + const privateVars = await this.storeService.getPipelinePrivateVars(id); + const certInfo = privateVars.cert; + if (!certInfo?.crt) { + throw new Error("该流水线还未生成证书,请先运行一次流水线"); + } + + const certReader = new CertReader(certInfo); + const zipBuffer = await certReader.buildZip(); + const filename = certReader.buildZipFilename("cert"); + + logger.info(`download pipeline cert zip: ${filename}, size: ${zipBuffer.length}`); + + this.ctx.attachment(filename); + this.ctx.set("Content-Type", "application/zip"); + this.ctx.body = zipBuffer; + } } diff --git a/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/base-convert.ts b/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/base-convert.ts index a1990434a..1314e38d5 100644 --- a/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/base-convert.ts +++ b/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/base-convert.ts @@ -1,8 +1,7 @@ -import { AbstractTaskPlugin, FileItem, IContext, Step, TaskInput, TaskOutput } from "@certd/pipeline"; +import { AbstractTaskPlugin, IContext, Step, TaskInput, TaskOutput } from "@certd/pipeline"; +import { CertConverter, CertReader, EVENT_CERT_APPLY_SUCCESS } from "@certd/plugin-lib"; import dayjs from "dayjs"; import type { CertInfo } from "./acme.js"; -import { CertReader, CertConverter, EVENT_CERT_APPLY_SUCCESS } from "@certd/plugin-lib"; -import JSZip from "jszip"; export abstract class CertApplyBaseConvertPlugin extends AbstractTaskPlugin { @TaskInput({ @@ -72,12 +71,6 @@ export abstract class CertApplyBaseConvertPlugin extends AbstractTaskPlugin { }) cert?: CertInfo; - @TaskOutput({ - title: "域名证书压缩文件", - type: "certZip", - }) - certZip?: FileItem; - async onInstance() { this.userContext = this.ctx.userContext; this.lastStatus = this.ctx.lastStatus as Step; @@ -108,7 +101,7 @@ export abstract class CertApplyBaseConvertPlugin extends AbstractTaskPlugin { } this._result.pipelinePrivateVars.cert = cert; - if (isNew) { + if (isNew || !cert.pfx) { try { const converter = new CertConverter({ logger: this.logger }); const res = await converter.convert({ @@ -137,54 +130,6 @@ export abstract class CertApplyBaseConvertPlugin extends AbstractTaskPlugin { this.logger.error("转换证书格式失败", e); } } - - if (isNew) { - const zipFileName = certReader.buildCertFileName("zip", certReader.detail.notBefore); - await this.zipCert(cert, zipFileName); - } else { - this.extendsFiles(); - } - this.certZip = this._result.files[0]; - } - - async zipCert(cert: CertInfo, filename: string) { - const zip = new JSZip(); - zip.file("证书.pem", cert.crt); - zip.file("私钥.pem", cert.key); - zip.file("中间证书.pem", cert.ic); - zip.file("cert.crt", cert.crt); - zip.file("cert.key", cert.key); - zip.file("intermediate.crt", cert.ic); - zip.file("origin.crt", cert.oc); - zip.file("one.pem", cert.one); - zip.file("cert.p7b", cert.p7b); - if (cert.pfx) { - zip.file("cert.pfx", Buffer.from(cert.pfx, "base64")); - } - if (cert.der) { - zip.file("cert.der", Buffer.from(cert.der, "base64")); - } - if (cert.jks) { - zip.file("cert.jks", Buffer.from(cert.jks, "base64")); - } - - zip.file( - "说明.txt", - `证书文件说明 -cert.crt:证书文件,包含证书链,pem格式 -cert.key:私钥文件,pem格式 -intermediate.crt:中间证书文件,pem格式 -origin.crt:原始证书文件,不含证书链,pem格式 -one.pem: 证书和私钥简单合并成一个文件,pem格式,crt正文+key正文 -cert.pfx:pfx格式证书文件,iis服务器使用 -cert.der:der格式证书文件 -cert.jks:jks格式证书文件,java服务器使用 - ` - ); - - const content = await zip.generateAsync({ type: "nodebuffer" }); - this.saveFile(filename, content); - this.logger.info(`已保存文件:${filename}`); } formatCert(pem: string) { diff --git a/packages/ui/certd-server/src/plugins/plugin-other/plugins/plugin-deploy-to-mail.ts b/packages/ui/certd-server/src/plugins/plugin-other/plugins/plugin-deploy-to-mail.ts index 233134727..3285161e6 100644 --- a/packages/ui/certd-server/src/plugins/plugin-other/plugins/plugin-deploy-to-mail.ts +++ b/packages/ui/certd-server/src/plugins/plugin-other/plugins/plugin-deploy-to-mail.ts @@ -1,4 +1,4 @@ -import { AbstractTaskPlugin, FileItem, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; +import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; import { CertInfo, CertReader } from "@certd/plugin-cert"; import dayjs from "dayjs"; import { get } from "lodash-es"; @@ -28,17 +28,6 @@ export class DeployCertToMailPlugin extends AbstractTaskPlugin { }) cert!: CertInfo; - @TaskInput({ - title: "证书压缩文件", - helper: "请选择前置任务输出的域名证书压缩文件", - component: { - name: "output-selector", - from: [":certZip:"], - }, - required: true, - }) - certZip!: FileItem; - @TaskInput({ title: "接收邮箱", component: { @@ -146,18 +135,18 @@ export class DeployCertToMailPlugin extends AbstractTaskPlugin { `; data.content = content; data.title = title; - const file = this.certZip; - if (!file) { - throw new Error("证书压缩文件还未生成,重新运行证书任务"); - } + + const zipBuffer = await certReader.buildZip(); + const zipFilename = certReader.buildZipFilename("cert"); + await this.ctx.emailService.sendByTemplate({ type: "sendCert", data, receivers: this.email, attachments: [ { - filename: file.filename, - path: file.path, + filename: zipFilename, + content: zipBuffer, }, ], });