perf: 手动上传证书部署流水线

pull/361/head
xiaojunnuo 2025-03-22 02:06:02 +08:00
parent fedf90ea78
commit fbb66f3c43
25 changed files with 329 additions and 511 deletions

View File

@ -4,6 +4,7 @@ import type { CertInfo } from "./acme.js";
import { CertReader } from "./cert-reader.js";
import JSZip from "jszip";
import { CertConverter } from "./convert.js";
export const EVENT_CERT_APPLY_SUCCESS = "CertApply.success";
export abstract class CertApplyBaseConvertPlugin extends AbstractTaskPlugin {
@TaskInput({
@ -76,6 +77,16 @@ export abstract class CertApplyBaseConvertPlugin extends AbstractTaskPlugin {
abstract onInit(): Promise<void>;
//必须output之后执行
async emitCertApplySuccess() {
const emitter = this.ctx.emitter;
const value = {
cert: this.cert,
file: this._result.files[0].path,
};
await emitter.emit(EVENT_CERT_APPLY_SUCCESS, value);
}
async output(certReader: CertReader, isNew: boolean) {
const cert: CertInfo = certReader.toCertInfo();
this.cert = cert;

View File

@ -1,15 +1,9 @@
import { NotificationBody, Step, TaskEmitter, TaskInput } from "@certd/pipeline";
import { NotificationBody, Step, TaskInput } from "@certd/pipeline";
import dayjs from "dayjs";
import { CertReader } from "./cert-reader.js";
import { pick } from "lodash-es";
import { CertApplyBaseConvertPlugin } from "./base-convert.js";
export const EVENT_CERT_APPLY_SUCCESS = "CertApply.success";
export async function emitCertApplySuccess(emitter: TaskEmitter, cert: CertReader) {
await emitter.emit(EVENT_CERT_APPLY_SUCCESS, cert);
}
export abstract class CertApplyBasePlugin extends CertApplyBaseConvertPlugin {
@TaskInput({
title: "邮箱",
@ -75,7 +69,7 @@ export abstract class CertApplyBasePlugin extends CertApplyBaseConvertPlugin {
if (cert != null) {
await this.output(cert, true);
await emitCertApplySuccess(this.ctx.emitter, cert);
await this.emitCertApplySuccess();
//清空后续任务的状态,让后续任务能够重新执行
this.clearLastStatus();

View File

@ -1,5 +0,0 @@
import { CertInfo } from "../acme";
export interface ICertApplyUploadService {
getCertInfo: (opts: { certId: number; userId: number }) => Promise<any>;
updateCert: (opts: { certId: number; cert: CertInfo; userId: number }) => Promise<any>;
}

View File

@ -2,9 +2,8 @@ import { IsTaskPlugin, pluginGroups, RunStrategy, Step, TaskInput, TaskOutput }
import type { CertInfo } from "../acme.js";
import { CertReader } from "../cert-reader.js";
import { CertApplyBaseConvertPlugin } from "../base-convert.js";
export * from "./d.js";
import dayjs from "dayjs";
import { ICertApplyUploadService } from "./d";
export { CertReader };
export type { CertInfo };
@IsTaskPlugin({
@ -84,7 +83,7 @@ export class CertApplyUploadPlugin extends CertApplyBaseConvertPlugin {
}
`,
})
certInfoId!: string;
uploadCert!: CertInfo;
@TaskOutput({
title: "证书MD5",
@ -100,14 +99,7 @@ export class CertApplyUploadPlugin extends CertApplyBaseConvertPlugin {
async onInit(): Promise<void> {}
async getCertFromStore() {
const certApplyUploadService: ICertApplyUploadService = await this.ctx.serviceGetter.get("CertApplyUploadService");
const certInfo = await certApplyUploadService.getCertInfo({
certId: Number(this.certInfoId),
userId: this.pipeline.userId,
});
const certReader = new CertReader(certInfo);
const certReader = new CertReader(this.uploadCert);
if (!certReader.expires && certReader.expires < new Date().getTime()) {
throw new Error("证书已过期,停止部署,请重新上传证书");
}
@ -121,39 +113,43 @@ export class CertApplyUploadPlugin extends CertApplyBaseConvertPlugin {
const leftDays = dayjs(certReader.expires).diff(dayjs(), "day");
this.logger.info(`证书过期时间${dayjs(certReader.expires).format("YYYY-MM-DD HH:mm:ss")},剩余${leftDays}`);
const lastCrtMd5 = this.lastStatus?.status?.output?.certMd5;
this.logger.info("证书MD5", crtMd5);
this.logger.info("上次证书MD5", lastCrtMd5);
if (lastCrtMd5 === crtMd5) {
this.logger.info("证书无变化,跳过");
//输出证书MD5
this.certMd5 = crtMd5;
await this.output(certReader, false);
return "skip";
if (!this.ctx.inputChanged) {
this.logger.info("输入参数无变化");
const lastCrtMd5 = this.lastStatus?.status?.output?.certMd5;
this.logger.info("证书MD5", crtMd5);
this.logger.info("上次证书MD5", lastCrtMd5);
if (lastCrtMd5 === crtMd5) {
this.logger.info("证书无变化,跳过");
//输出证书MD5
this.certMd5 = crtMd5;
await this.output(certReader, false);
return "skip";
}
this.logger.info("证书有变化,重新部署");
} else {
this.logger.info("输入参数有变化,重新部署");
}
this.logger.info("证书有变化,重新部署");
this.clearLastStatus();
//输出证书MD5
this.certMd5 = crtMd5;
await this.output(certReader, true);
//必须output之后执行
await this.emitCertApplySuccess();
return;
}
async onCertUpdate(data: any) {
const certApplyUploadService = await this.ctx.serviceGetter.get("CertApplyUploadService");
const res = await certApplyUploadService.updateCert({
certId: this.certInfoId,
userId: this.ctx.user.id,
cert: {
crt: data.crt,
key: data.key,
},
});
const certReader = new CertReader(data);
return {
input: {
domains: res.domains,
uploadCert: {
crt: data.crt,
key: data.key,
},
domains: certReader.getAllDomains(),
},
};
}

View File

@ -1,3 +1,5 @@
export { EVENT_CERT_APPLY_SUCCESS } from "./cert-plugin/base-convert.js";
export * from "./cert-plugin/index.js";
export * from "./cert-plugin/lego/index.js";
export * from "./cert-plugin/custom/index.js";

View File

@ -67,6 +67,7 @@
"lucide-vue-next": "^0.477.0",
"mitt": "^3.0.1",
"nanoid": "^4.0.0",
"node-forge": "^1.3.1",
"nprogress": "^0.2.0",
"object-assign": "^4.1.1",
"pinia": "2.1.7",

View File

@ -5,8 +5,8 @@ import OutputSelector from "/@/components/plugins/common/output-selector/index.v
import DnsProviderSelector from "/@/components/plugins/cert/dns-provider-selector/index.vue";
import DomainsVerifyPlanEditor from "/@/components/plugins/cert/domains-verify-plan-editor/index.vue";
import AccessSelector from "/@/views/certd/access/access-selector/index.vue";
import CertInfoUpdater from "/@/views/certd/monitor/cert/updater/index.vue";
import InputPassword from "./common/input-password.vue";
import CertInfoUpdater from "/@/views/certd/pipeline/cert-upload/index.vue";
import ApiTest from "./common/api-test.vue";
export * from "./cert/index.js";
export default {

View File

@ -109,24 +109,23 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
copy: { show: false },
edit: { show: false },
upload: {
show: compute(({ row }) => {
return row.fromType === "upload";
}),
order: 4,
title: "更新证书",
type: "link",
icon: "ion:upload",
async click({ row }) {
await openUpdateCertDialog({
id: row.id,
});
},
},
remove: {
order: 10,
show: false,
},
download: {
order: 9,
title: "下载证书",
type: "link",
icon: "ant-design:download-outlined",
async click({ row }) {
if (!row.certFile) {
notification.error({ message: "证书还未生成,请先运行流水线" });
return;
}
window.open("/api/monitor/cert/download?id=" + row.id);
},
},
},
},
columns: {

View File

@ -3,7 +3,7 @@
<template #header>
<div class="title">
证书仓库
<span class="sub">从流水线生成的证书后续将支持手动上传证书并部署</span>
<span class="sub">从流水线生成的证书</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
@ -11,12 +11,12 @@
</template>
<script lang="ts" setup>
import { defineComponent, onActivated, onMounted } from "vue";
import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { createApi } from "./api";
defineOptions({
name: "CertStore"
name: "CertStore",
});
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: {} });

View File

@ -8,7 +8,7 @@ export async function GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query
data: query,
});
}
@ -16,7 +16,7 @@ export async function AddObj(obj: any) {
return await request({
url: apiPrefix + "/add",
method: "post",
data: obj
data: obj,
});
}
@ -24,7 +24,7 @@ export async function UpdateObj(obj: any) {
return await request({
url: apiPrefix + "/update",
method: "post",
data: obj
data: obj,
});
}
@ -32,7 +32,7 @@ export async function DelObj(id: any) {
return await request({
url: apiPrefix + "/delete",
method: "post",
params: { id }
params: { id },
});
}
@ -40,7 +40,7 @@ export async function GetObj(id: any) {
return await request({
url: apiPrefix + "/info",
method: "post",
params: { id }
params: { id },
});
}
@ -48,7 +48,7 @@ export async function GetDetail(id: any) {
return await request({
url: apiPrefix + "/detail",
method: "post",
params: { id }
params: { id },
});
}
@ -56,7 +56,7 @@ export async function Save(pipelineEntity: any) {
return await request({
url: apiPrefix + "/save",
method: "post",
data: pipelineEntity
data: pipelineEntity,
});
}
@ -64,7 +64,7 @@ export async function Trigger(id: any, stepId?: string) {
return await request({
url: apiPrefix + "/trigger",
method: "post",
params: { id, stepId }
params: { id, stepId },
});
}
@ -72,7 +72,7 @@ export async function Cancel(historyId: any) {
return await request({
url: apiPrefix + "/cancel",
method: "post",
params: { historyId }
params: { historyId },
});
}
@ -80,7 +80,7 @@ export async function BatchUpdateGroup(pipelineIds: number[], groupId: number):
return await request({
url: apiPrefix + "/batchUpdateGroup",
method: "post",
data: { ids: pipelineIds, groupId }
data: { ids: pipelineIds, groupId },
});
}
@ -88,7 +88,7 @@ export async function BatchDelete(pipelineIds: number[]): Promise<CertInfo> {
return await request({
url: apiPrefix + "/batchDelete",
method: "post",
data: { ids: pipelineIds }
data: { ids: pipelineIds },
});
}
@ -96,14 +96,14 @@ export async function GetFiles(pipelineId: number) {
return await request({
url: historyApiPrefix + "/files",
method: "post",
params: { pipelineId }
params: { pipelineId },
});
}
export async function GetCount() {
return await request({
url: apiPrefix + "/count",
method: "post"
method: "post",
});
}
@ -119,6 +119,6 @@ export async function GetCert(pipelineId: number): Promise<CertInfo> {
return await request({
url: certApiPrefix + "/get",
method: "post",
params: { id: pipelineId }
params: { id: pipelineId },
});
}

View File

@ -1,24 +1,23 @@
<template>
<div class="cert-info-updater w-full flex items-center">
<div class="flex-o">
<fs-values-format :model-value="modelValue" :dict="certInfoDict" />
<a-tag>{{ domain }}</a-tag>
<fs-button type="primary" size="small" class="ml-1" icon="ion:upload" text="更新证书" @click="onUploadClick" />
</div>
</div>
</template>
<script lang="tsx" setup>
import { inject } from "vue";
import { dict } from "@fast-crud/fast-crud";
import { certInfoApi } from "../api";
import { useCertUpload } from "/@/views/certd/pipeline/cert-upload/use";
import { computed, inject } from "vue";
import { useCertUpload } from "./use";
import { getAllDomainsFromCrt } from "/@/views/certd/pipeline/utils";
defineOptions({
name: "CertInfoUpdater",
});
const props = defineProps<{
modelValue?: number | string;
modelValue?: { crt: string; key: string };
type?: string;
placeholder?: string;
size?: string;
@ -26,33 +25,28 @@ const props = defineProps<{
}>();
const emit = defineEmits(["updated", "update:modelValue"]);
const certInfoDict = dict({
value: "id",
label: "domain",
getNodesByValues: async (values: any[]) => {
const res = await certInfoApi.GetOptionsByIds(values);
if (res.length > 0) {
emit("updated", {
domains: res[0].domains,
});
}
return res;
},
const { openUpdateCertDialog } = useCertUpload();
const domain = computed(() => {
if (!props.modelValue?.crt) {
return "";
}
const domains = getAllDomainsFromCrt(props.modelValue?.crt);
return domains[0];
});
const { openUpdateCertDialog } = useCertUpload();
function onUpdated(res: any) {
if (!props.modelValue) {
emit("update:modelValue", res.id);
}
emit("updated", res);
function onUpdated(res: { uploadCert: any }) {
debugger;
emit("update:modelValue", res.uploadCert);
const domains = getAllDomainsFromCrt(res.uploadCert.crt);
emit("updated", { domains });
}
const pipeline: any = inject("pipeline");
function onUploadClick() {
debugger;
openUpdateCertDialog({
id: props.modelValue,
pipelineId: pipeline.id,
onSubmit: onUpdated,
});
}

View File

@ -1,23 +1,62 @@
import { compute, useFormWrapper } from "@fast-crud/fast-crud";
import NotificationSelector from "/@/views/certd/notification/notification-selector/index.vue";
import * as api from "./api";
import { omit, cloneDeep, set } from "lodash-es";
import { cloneDeep, omit } from "lodash-es";
import { useReference } from "/@/use/use-refrence";
import { ref } from "vue";
import * as pluginApi from "../api.plugin";
import { checkPipelineLimit } from "/@/views/certd/pipeline/utils";
import { notification } from "ant-design-vue";
import * as api from "../api";
import { checkPipelineLimit, getAllDomainsFromCrt } from "/@/views/certd/pipeline/utils";
import { useRouter } from "vue-router";
import { nanoid } from "nanoid";
export function useCertUpload() {
const { openCrudFormDialog } = useFormWrapper();
const router = useRouter();
const certInputs = {
"uploadCert.crt": {
title: "证书",
type: "text",
form: {
component: {
name: "pem-input",
vModel: "modelValue",
textarea: {
rows: 4,
placeholder: "-----BEGIN CERTIFICATE-----\n...\n...\n-----END CERTIFICATE-----",
},
},
helper: "选择pem格式证书文件或者粘贴到此",
rules: [{ required: true, message: "此项必填" }],
col: { span: 24 },
order: -9999,
},
},
"uploadCert.key": {
title: "证书私钥",
type: "text",
form: {
component: {
name: "pem-input",
vModel: "modelValue",
textarea: {
rows: 4,
placeholder: "-----BEGIN PRIVATE KEY-----\n...\n...\n-----END PRIVATE KEY----- ",
},
},
helper: "选择pem格式证书私钥文件或者粘贴到此",
rules: [{ required: true, message: "此项必填" }],
col: { span: 24 },
order: -9999,
},
},
};
async function buildUploadCertPluginInputs(getFormData: any) {
const plugin: any = await pluginApi.GetPluginDefine("CertApplyUpload");
const inputs: any = {};
for (const inputKey in plugin.input) {
if (inputKey === "certInfoId" || inputKey === "domains") {
if (inputKey === "uploadCert" || inputKey === "domains") {
continue;
}
const inputDefine = cloneDeep(plugin.input[inputKey]);
@ -66,42 +105,7 @@ export function useCertUpload() {
return {
crudOptions: {
columns: {
"cert.crt": {
title: "证书",
type: "text",
form: {
component: {
name: "pem-input",
vModel: "modelValue",
textarea: {
rows: 4,
placeholder: "-----BEGIN CERTIFICATE-----\n...\n...\n-----END CERTIFICATE-----",
},
},
helper: "选择pem格式证书文件或者粘贴到此",
rules: [{ required: true, message: "此项必填" }],
col: { span: 24 },
order: -9999,
},
},
"cert.key": {
title: "证书私钥",
type: "text",
form: {
component: {
name: "pem-input",
vModel: "modelValue",
textarea: {
rows: 4,
placeholder: "-----BEGIN PRIVATE KEY-----\n...\n...\n-----END PRIVATE KEY----- ",
},
},
helper: "选择pem格式证书私钥文件或者粘贴到此",
rules: [{ required: true, message: "此项必填" }],
col: { span: 24 },
order: -9999,
},
},
...cloneDeep(certInputs),
...inputs,
notification: {
title: "失败通知",
@ -128,6 +132,9 @@ export function useCertUpload() {
saveRemind: false,
},
async doSubmit({ form }: any) {
const cert = form.uploadCert;
const domains = getAllDomainsFromCrt(cert.crt);
const notifications = [];
if (form.notification != null) {
notifications.push({
@ -138,18 +145,54 @@ export function useCertUpload() {
});
}
const req = {
id: form.id,
cert: form.cert,
pipeline: {
input: omit(form, ["id", "cert", "notification", "notificationTarget"]),
notifications,
},
const pipelineTitle = domains[0] + "上传证书部署";
const input = omit(form, ["id", "cert", "notification", "notificationTarget"]);
const pipeline = {
title: pipelineTitle,
runnableType: "pipeline",
stages: [
{
id: nanoid(10),
title: "上传证书解析阶段",
maxTaskCount: 1,
runnableType: "stage",
tasks: [
{
id: nanoid(10),
title: "上传证书解析转换",
runnableType: "task",
steps: [
{
id: nanoid(10),
title: "上传证书解析转换",
runnableType: "step",
input: {
cert: cert,
domains: domains,
...input,
},
strategy: {
runStrategy: 0, // 正常执行
},
type: "CertApplyUpload",
},
],
},
],
},
],
notifications,
};
const res = await api.UploadCert(req);
const id = await api.Save({
title: pipeline.title,
content: JSON.stringify(pipeline),
keepHistoryCount: 30,
type: "cert_upload",
});
router.push({
path: "/certd/pipeline/detail",
query: { id: res.pipelineId, editMode: "true" },
query: { id: id, editMode: "true" },
});
},
},
@ -161,61 +204,22 @@ export function useCertUpload() {
wrapperRef.value = wrapper;
}
async function openUpdateCertDialog(opts: { id?: any; onSubmit?: any; pipelineId?: any }) {
async function openUpdateCertDialog(opts: { onSubmit?: any }) {
function createCrudOptions() {
return {
crudOptions: {
columns: {
"cert.crt": {
title: "证书",
type: "text",
form: {
component: {
name: "pem-input",
vModel: "modelValue",
textarea: {
rows: 4,
placeholder: "-----BEGIN CERTIFICATE-----\n...\n...\n-----END CERTIFICATE-----",
},
},
rules: [{ required: true, message: "此项必填" }],
col: { span: 24 },
},
},
"cert.key": {
title: "私钥",
type: "textarea",
form: {
component: {
name: "pem-input",
vModel: "modelValue",
textarea: {
rows: 4,
placeholder: "-----BEGIN PRIVATE KEY-----\n...\n...\n-----END PRIVATE KEY----- ",
},
},
rules: [{ required: true, message: "此项必填" }],
col: { span: 24 },
},
},
...cloneDeep(certInputs),
},
form: {
wrapper: {
title: "更新证书",
title: "手动上传证书",
saveRemind: false,
},
async afterSubmit() {
notification.success({ message: "更新成功" });
},
async afterSubmit() {},
async doSubmit({ form }: any) {
const req = {
id: opts.id,
pipelineId: opts.pipelineId,
cert: form.cert,
};
const res = await api.UploadCert(req);
if (opts.onSubmit) {
await opts.onSubmit(res);
await opts.onSubmit(form);
}
},
},

View File

@ -1,4 +1,4 @@
import { checkPipelineLimit } from "/@/views/certd/pipeline/utils";
import { checkPipelineLimit, readCertDetail } from "/@/views/certd/pipeline/utils";
import { omit } from "lodash-es";
import * as api from "/@/views/certd/pipeline/api";
import { message } from "ant-design-vue";
@ -52,6 +52,7 @@ export function useCertd(certdFormRef: any) {
await checkPipelineLimit();
certdFormRef.value.open(async ({ form }: any) => {
const certDetail = readCertDetail(form.cert.crt);
// 添加certd pipeline
const triggers = [];
if (form.triggerCron) {

View File

@ -22,6 +22,7 @@ const props = defineProps<{
form: any;
input: any;
pluginName: string;
stepId: string;
}>();
const attrs = useAttrs();
@ -85,7 +86,7 @@ const doPluginFormSubmit = async (formData: any) => {
if (res.input) {
const { save, findStep } = getPipelineScope();
const step = findStep(res.input);
const step = findStep(props.stepId);
if (step) {
//
mergeWith(step.input, res.input, (objValue, srcValue) => {

View File

@ -41,6 +41,7 @@ async function init() {
...shortcut,
pluginName: stepType,
input: step.input,
stepId: step.id,
});
}
}

View File

@ -104,7 +104,7 @@
</template>
<span class="flex-o w-100">
<span class="ellipsis flex-1 task-title" :class="{ 'in-edit': editMode, deleted: task.disabled }">{{ task.title }}</span>
<pi-status-show :status="task.status?.result"></pi-status-show>
<pi-status-show v-if="!editMode" :status="task.status?.result"></pi-status-show>
</span>
</a-popover>
</a-button>
@ -273,6 +273,7 @@ import { FsIcon } from "@fast-crud/fast-crud";
import { useSettingStore } from "/@/store/modules/settings";
import { useUserStore } from "/@/store/modules/user";
import TaskShortcuts from "./component/shortcut/task-shortcuts.vue";
import { eachSteps, findStep } from "../utils";
export default defineComponent({
name: "PipelineEdit",
// eslint-disable-next-line vue/no-unused-components
@ -648,22 +649,6 @@ export default defineComponent({
errors.push(error);
}
function eachSteps(pp: any, callback: any) {
if (pp.stages) {
for (const stage of pp.stages) {
if (stage.tasks) {
for (const task of stage.tasks) {
if (task.steps) {
for (const step of task.steps) {
callback(step, task, stage);
}
}
}
}
}
}
}
function doValidate() {
validateErrors.value = {};
@ -748,15 +733,8 @@ export default defineComponent({
toggleEditMode(false);
};
function findStep(id: string) {
let found = null;
const pp = pipeline.value;
eachSteps(pp, (step: any, task: any, stage: any) => {
if (step.id === id) {
found = step;
}
});
return found;
function fundStepFromPipeline(id: string) {
return findStep(pipeline.value, id);
}
return {
@ -766,7 +744,7 @@ export default defineComponent({
cancel,
saveLoading,
hasValidateError,
findStep,
findStep: fundStepFromPipeline,
};
}

View File

@ -2,7 +2,8 @@ import { forEach } from "lodash-es";
import { mySuiteApi } from "/@/views/certd/suite/mine/api";
import { notification } from "ant-design-vue";
import { useSettingStore } from "/@/store/modules/settings";
//@ts-ignore
import forge from "node-forge";
export function eachStages(list: any[], exec: (item: any, runnableType: string) => void, runnableType: string = "stage") {
if (!list || list.length <= 0) {
return;
@ -17,6 +18,42 @@ export function eachStages(list: any[], exec: (item: any, runnableType: string)
});
}
export function eachSteps(pipeline: any, callback: any) {
const pp = pipeline;
if (pp.stages) {
for (const stage of pp.stages) {
if (stage.tasks) {
for (const task of stage.tasks) {
if (task.steps) {
for (const step of task.steps) {
callback(step, task, stage);
}
}
}
}
}
}
}
export function findStep(pipeline: any, id: string) {
const pp = pipeline;
if (pp.stages) {
for (const stage of pp.stages) {
if (stage.tasks) {
for (const task of stage.tasks) {
if (task.steps) {
for (const step of task.steps) {
if (step.id === id) {
return step;
}
}
}
}
}
}
}
}
export async function checkPipelineLimit() {
const settingsStore = useSettingStore();
if (settingsStore.isComm && settingsStore.suiteSetting.enabled) {
@ -32,3 +69,34 @@ export async function checkPipelineLimit() {
}
}
}
export function readCertDetail(crt: string) {
const detail = forge.pki.certificateFromPem(crt);
const expires = detail.notAfter;
return { detail, expires };
}
export function getAllDomainsFromCrt(crt: string) {
const { detail } = readCertDetail(crt);
const domains = [];
// 1. 提取SAN中的DNS名称
const sanExtension = detail.extensions.find((ext: any) => ext.name === "subjectAltName");
if (sanExtension) {
sanExtension.altNames.forEach((altName: any) => {
if (altName.type === 2) {
// type=2 表示DNS名称
domains.push(altName.value);
}
});
}
// 2. 如果没有SAN回退到CN通用名称
if (domains.length === 0) {
const cnAttr = detail.subject.attributes.find((attr: any) => attr.name === "commonName");
if (cnAttr) {
domains.push(cnAttr.value);
}
}
return domains;
}

View File

@ -1,11 +1,11 @@
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
import { Constants, CrudController } from '@certd/lib-server';
import { AuthService } from '../../../modules/sys/authority/service/auth-service.js';
import { CertInfoService } from '../../../modules/monitor/index.js';
import { PipelineService } from '../../../modules/pipeline/service/pipeline-service.js';
import { ALL, Body, Controller, Get, Inject, Post, Provide, Query } from "@midwayjs/core";
import { CommonException, Constants, CrudController } from "@certd/lib-server";
import { AuthService } from "../../../modules/sys/authority/service/auth-service.js";
import { CertInfoService } from "../../../modules/monitor/index.js";
import { PipelineService } from "../../../modules/pipeline/service/pipeline-service.js";
import { SelectQueryBuilder } from "typeorm";
import { CertUploadService } from "../../../modules/monitor/service/cert-upload-service.js";
import { CertInfo } from "@certd/plugin-cert";
import { logger } from "@certd/basic";
import fs from "fs";
/**
*/
@ -17,8 +17,6 @@ export class CertInfoController extends CrudController<CertInfoService> {
@Inject()
authService: AuthService;
@Inject()
certUploadService: CertUploadService;
@Inject()
pipelineService: PipelineService;
getService(): CertInfoService {
@ -131,26 +129,28 @@ export class CertInfoController extends CrudController<CertInfoService> {
return this.ok(certInfo);
}
@Post('/upload', { summary: Constants.per.authOnly })
async upload(@Body(ALL) body: {cert: CertInfo, pipeline: any, id?: number}) {
if (body.id) {
//修改
await this.service.checkUserId(body.id, this.getUserId());
await this.certUploadService.updateCert({
id: body.id,
userId: this.getUserId(),
cert: body.cert,
});
return this.ok();
}else{
//添加
const res = await this.certUploadService.createUploadCertPipeline({
userId: this.getUserId(),
cert: body.cert,
pipeline: body.pipeline,
});
return this.ok(res)
@Get('/download', { summary: Constants.per.authOnly })
async download(@Query('id') id: number) {
const certInfo = await this.service.info(id)
if (certInfo == null) {
throw new CommonException('file not found');
}
if (certInfo.userId !== this.getUserId()) {
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');
}
logger.info(`download:${path}`);
// 以流的形式下载文件
this.ctx.attachment(path);
this.ctx.set('Content-Type', 'application/octet-stream');
return fs.createReadStream(path);
}
}

View File

@ -13,7 +13,6 @@ import {
import {EmailService} from '../../../modules/basic/service/email-service.js';
import {http, HttpRequestConfig, logger, mergeUtils, utils} from '@certd/basic';
import {NotificationService} from '../../../modules/pipeline/service/notification-service.js';
import {CertApplyUploadService} from "../../../modules/pipeline/service/cert-apply-upload-service.js";
@Provide()
@Controller('/api/pi/handle')
@ -24,8 +23,6 @@ export class HandleController extends BaseController {
@Inject()
emailService: EmailService;
@Inject()
certApplyUploadService: CertApplyUploadService;
@Inject()
notificationService: NotificationService;
@ -97,7 +94,6 @@ export class HandleController extends BaseController {
};
const serviceContainer:any = {
CertApplyUploadService:this.certApplyUploadService
}
const serviceGetter = {
get:(name: string) => {

View File

@ -1,8 +1,8 @@
import { Autoload, Init, Inject, Scope, ScopeEnum } from '@midwayjs/core';
import { CertInfoService } from '../monitor/index.js';
import { pipelineEmitter } from '@certd/pipeline';
import { CertReader, EVENT_CERT_APPLY_SUCCESS } from '@certd/plugin-cert';
import { PipelineEvent } from '@certd/pipeline/dist/service/emit.js';
import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/core";
import { CertInfoService } from "../monitor/index.js";
import { pipelineEmitter } from "@certd/pipeline";
import { CertInfo, EVENT_CERT_APPLY_SUCCESS } from "@certd/plugin-cert";
import { PipelineEvent } from "@certd/pipeline/dist/service/emit.js";
@Autoload()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@ -15,8 +15,8 @@ export class AutoEPipelineEmitterRegister {
await this.onCertApplySuccess();
}
async onCertApplySuccess() {
pipelineEmitter.on(EVENT_CERT_APPLY_SUCCESS, async (event: PipelineEvent<CertReader>) => {
await this.certInfoService.updateCertByPipelineId(event.pipeline.id, event.event);
pipelineEmitter.on(EVENT_CERT_APPLY_SUCCESS, async (event: PipelineEvent<{cert:CertInfo,file:string}>) => {
await this.certInfoService.updateCertByPipelineId(event.pipeline.id, event.event.cert, event.event.file);
});
}
}

View File

@ -11,6 +11,7 @@ export type UploadCertReq = {
certReader: CertReader;
fromType?: string;
userId?: number;
file?:any
};
@ -38,7 +39,7 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
});
}
async updateDomains(pipelineId: number, userId: number, domains: string[]) {
async updateDomains(pipelineId: number, userId: number, domains: string[],fromType?:string) {
const found = await this.repository.findOne({
where: {
pipelineId,
@ -53,6 +54,7 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
//create
bean.pipelineId = pipelineId;
bean.userId = userId;
bean.fromType = fromType
if (!domains || domains.length === 0) {
return;
}
@ -133,7 +135,7 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
return certReader.toCertInfo();
}
async updateCertByPipelineId(pipelineId: number, certReader: CertReader, fromType = 'pipeline') {
async updateCertByPipelineId(pipelineId: number, cert: CertInfo,file?:string,fromType = 'pipeline') {
const found = await this.repository.findOne({
where: {
pipelineId,
@ -141,8 +143,9 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
});
const bean = await this.updateCert({
id: found?.id,
certReader,
certReader: new CertReader(cert),
fromType,
file
});
return bean;
}
@ -165,6 +168,9 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
bean.expiresTime = certReader.expires;
bean.certProvider = certReader.detail.issuer.commonName;
bean.userId = userId
if(req.file){
bean.certFile = req.file
}
await this.addOrUpdate(bean);
return bean;
}

View File

@ -1,203 +0,0 @@
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { BaseService, CommonException } from "@certd/lib-server";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { EntityManager, Repository } from "typeorm";
import { CertInfoEntity } from "../entity/cert-info.js";
import { CertInfo, CertReader } from "@certd/plugin-cert";
import { PipelineService } from "../../pipeline/service/pipeline-service.js";
import { CertInfoService } from "./cert-info-service.js";
import { PipelineEntity } from "../../pipeline/entity/pipeline.js";
import { nanoid } from "nanoid";
export type UploadCertReq = {
id?: number;
certReader: CertReader;
fromType?: string;
userId?: number;
pipelineId?: number;
};
export type UpdateCertReq = {
id: number;
cert: CertInfo;
userId?: number;
};
export type CreateUploadPipelineReq = {
cert: CertInfo;
userId: number;
pipelineId?: number;
pipeline?:{
input?:any;
notifications?:any[]
}
};
@Provide("CertUploadService")
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class CertUploadService extends BaseService<CertInfoEntity> {
@InjectEntityModel(CertInfoEntity)
repository: Repository<CertInfoEntity>;
@Inject()
pipelineService: PipelineService;
@Inject()
certInfoService: CertInfoService;
//@ts-ignore
getRepository() {
return this.repository;
}
/**
* 线
* @param req
*/
async updateCert(req: UpdateCertReq) {
const certInfoEntity = await this.certInfoService.info(req.id);
if (!certInfoEntity) {
throw new CommonException("cert not found");
}
if(certInfoEntity.fromType !== 'upload') {
throw new CommonException("cert can't be custom upload");
}
await this.uploadCert(this.repository.manager,{
id: req.id,
fromType: 'upload',
userId: req.userId,
certReader: new CertReader(req.cert)
})
return {
id: certInfoEntity.id,
domains: certInfoEntity.domains.split(','),
pipelineId: certInfoEntity.pipelineId,
fromType: certInfoEntity.fromType,
updateTime: certInfoEntity.updateTime,
}
}
async createUploadCertPipeline(body:CreateUploadPipelineReq) {
const { userId, cert } = body;
if (!cert) {
throw new CommonException("cert can't be empty");
}
const certReader = new CertReader(cert)
return await this.transaction(async (tx:EntityManager)=>{
let pipelineId = body.pipelineId;
const newCertInfo = await this.uploadCert(tx,{
certReader: certReader,
fromType: 'upload',
userId,
pipelineId
});
if(!pipelineId){
const pipelineTitle = certReader.getAllDomains()[0] +"上传证书自动部署";
const notifications = body.pipeline?.notifications ||[];
if(notifications.length === 0){
notifications.push({
type: "custom",
when: ["error", "turnToSuccess", "success"],
notificationId: 0,
title: "默认通知",
});
}
let pipeline = {
title: pipelineTitle,
runnableType: "pipeline",
stages: [
{
id: nanoid(10),
title: "上传证书解析阶段",
maxTaskCount: 1,
runnableType: "stage",
tasks: [
{
id: nanoid(10),
title: "上传证书解析转换",
runnableType: "task",
steps: [
{
id: nanoid(10),
title: "上传证书解析转换",
runnableType: "step",
input: {
certInfoId: newCertInfo.id,
domains: newCertInfo.domains.split(','),
...body.pipeline?.input
},
strategy: {
runStrategy: 0, // 正常执行
},
type: "CertApplyUpload",
},
],
},
],
},
],
triggers:[],
notifications,
}
const newPipeline = await tx.getRepository(PipelineEntity).save({
userId,
title: pipelineTitle,
type:"cert_upload",
content: JSON.stringify(pipeline),
keepHistory:20,
})
newCertInfo.pipelineId = newPipeline.id;
await tx.getRepository(CertInfoEntity).save({
id: newCertInfo.id,
pipelineId: newPipeline.id
});
pipelineId = newPipeline.id;
}
return {
id:newCertInfo.id,
pipelineId: pipelineId,
domains: newCertInfo.domains.split(','),
fromType: newCertInfo.fromType,
updateTime: newCertInfo.updateTime,
}
})
}
private async uploadCert(tx:EntityManager,req: UploadCertReq) {
const bean = new CertInfoEntity();
const { id, fromType,userId, certReader } = req;
if (id) {
bean.id = id;
} else {
bean.fromType = fromType;
bean.userId = userId
}
const certInfo = certReader.toCertInfo();
bean.certInfo = JSON.stringify(certInfo);
bean.applyTime = new Date().getTime();
const domains = certReader.detail.domains.altNames;
bean.domains = domains.join(',');
bean.domain = domains[0];
bean.domainCount = domains.length;
bean.expiresTime = certReader.expires;
bean.certProvider = certReader.detail.issuer.commonName;
if (req.pipelineId){
bean.pipelineId = req.pipelineId;
}
await tx.getRepository(CertInfoEntity).save(bean);
return bean;
}
}

View File

@ -1,28 +0,0 @@
import {ICertApplyUploadService} from "@certd/plugin-cert";
import {IMidwayContext, Inject, Provide} from "@midwayjs/core";
import {CertInfoService} from "../../monitor/index.js";
import {CertUploadService} from "../../monitor/service/cert-upload-service.js";
@Provide("CertApplyUploadService")
export class CertApplyUploadService implements ICertApplyUploadService {
@Inject()
ctx : IMidwayContext
async getCertInfo(opts: { certId: number; userId: number; }) {
const certInfoService = this.ctx.getApp().getApplicationContext().get<CertInfoService>("CertInfoService")
const { certId, userId } = opts;
return await certInfoService.getCertInfo({
certId,
userId: Number(userId),
});
};
async updateCert(opts: { certId: any; userId: any; cert: any; }){
const certUploadService = this.ctx.getApp().getApplicationContext().get<CertUploadService>("CertUploadService")
return await certUploadService.updateCert({
id:opts.certId,
userId:opts.userId,
cert:opts.cert
});
}
}

View File

@ -36,7 +36,6 @@ import { NotificationService } from "./notification-service.js";
import { NotificationGetter } from "./notification-getter.js";
import { UserSuiteEntity, UserSuiteService } from "@certd/commercial-core";
import { CertInfoService } from "../../monitor/service/cert-info-service.js";
import {CertApplyUploadService} from "./cert-apply-upload-service.js";
const runningTasks: Map<string | number, Executor> = new Map();
@ -93,8 +92,6 @@ export class PipelineService extends BaseService<PipelineEntity> {
@Inject()
certInfoService: CertInfoService;
@Inject()
certApplyUploadService: CertApplyUploadService;
//@ts-ignore
getRepository() {
return this.repository;
@ -196,10 +193,11 @@ export class PipelineService extends BaseService<PipelineEntity> {
await this.registerTriggerById(bean.id);
//保存域名信息到certInfo表
if(bean.from !== 'cert_upload'){
await this.certInfoService.updateDomains(pipeline.id, pipeline.userId || bean.userId, domains);
let fromType = 'pipeline';
if(bean.type === 'cert_upload') {
fromType = 'upload';
}
await this.certInfoService.updateDomains(pipeline.id, pipeline.userId || bean.userId, domains,fromType);
return bean;
}
@ -483,9 +481,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
const siteInfo = await this.sysSettingsService.getSetting<SysSiteInfo>(SysSiteInfo);
sysInfo.title = siteInfo.title;
}
const serviceContainer = {
CertApplyUploadService: this.certApplyUploadService
}
const serviceContainer = {}
const serviceGetter = {
get:(name: string) => {
return serviceContainer[name]

View File

@ -1072,6 +1072,9 @@ importers:
cropperjs:
specifier: ^1.6.1
version: 1.6.2
crypto-js:
specifier: ^4.2.0
version: 4.2.0
cssnano:
specifier: ^7.0.6
version: 7.0.6(postcss@8.5.3)
@ -1102,6 +1105,9 @@ importers:
nanoid:
specifier: ^4.0.0
version: 4.0.2
node-forge:
specifier: ^1.3.1
version: 1.3.1
nprogress:
specifier: ^0.2.0
version: 0.2.0