pull/361/head
xiaojunnuo 2025-03-21 23:11:58 +08:00
parent 656cb89fe8
commit d558d50102
12 changed files with 207 additions and 263 deletions

View File

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

View File

@ -3,7 +3,6 @@ import type { CertInfo } from "../acme.js";
import { CertReader } from "../cert-reader.js"; import { CertReader } from "../cert-reader.js";
import { CertApplyBaseConvertPlugin } from "../base-convert.js"; import { CertApplyBaseConvertPlugin } from "../base-convert.js";
export * from "./d.js"; export * from "./d.js";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { ICertApplyUploadService } from "./d"; import { ICertApplyUploadService } from "./d";
export { CertReader }; export { CertReader };
@ -22,7 +21,7 @@ export type { CertInfo };
shortcut: { shortcut: {
certUpdate: { certUpdate: {
title: "更新证书", title: "更新证书",
icon: "ph:upload", icon: "ion:upload",
action: "onCertUpdate", action: "onCertUpdate",
form: { form: {
columns: { columns: {
@ -65,20 +64,19 @@ export type { CertInfo };
}) })
export class CertApplyUploadPlugin extends CertApplyBaseConvertPlugin { export class CertApplyUploadPlugin extends CertApplyBaseConvertPlugin {
@TaskInput({ @TaskInput({
title: "证书仓库ID", title: "手动上传证书",
component: { component: {
name: "cert-info-selector", name: "cert-info-updater",
vModel: "modelValue", vModel: "modelValue",
}, },
helper: "请不要随意修改", helper: "手动上传证书",
order: -9999, order: -9999,
required: true, required: true,
mergeScript: ` mergeScript: `
return { return {
component:{ component:{
on:{ on:{
selectedChange(scope){ updated(scope){
console.log(scope)
scope.form.input.domains = scope.$event?.domains scope.form.input.domains = scope.$event?.domains
} }
} }
@ -144,7 +142,7 @@ export class CertApplyUploadPlugin extends CertApplyBaseConvertPlugin {
async onCertUpdate(data: any) { async onCertUpdate(data: any) {
const certApplyUploadService = await this.ctx.serviceGetter.get("CertApplyUploadService"); const certApplyUploadService = await this.ctx.serviceGetter.get("CertApplyUploadService");
await certApplyUploadService.updateCert({ const res = await certApplyUploadService.updateCert({
certId: this.certInfoId, certId: this.certInfoId,
userId: this.ctx.user.id, userId: this.ctx.user.id,
cert: { cert: {
@ -152,6 +150,12 @@ export class CertApplyUploadPlugin extends CertApplyBaseConvertPlugin {
key: data.key, key: data.key,
}, },
}); });
return {
input: {
domains: res.domains,
},
};
} }
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<AConfigProvider :locale="locale" :theme="tokenTheme"> <AConfigProvider :locale="locale" :theme="tokenTheme">
<contextHolder />
<fs-form-provider> <fs-form-provider>
<contextHolder />
<router-view /> <router-view />
</fs-form-provider> </fs-form-provider>
</AConfigProvider> </AConfigProvider>
@ -21,7 +21,7 @@ import AConfigProvider from "ant-design-vue/es/config-provider";
import { Modal } from "ant-design-vue"; import { Modal } from "ant-design-vue";
defineOptions({ defineOptions({
name: "App" name: "App",
}); });
const [modal, contextHolder] = Modal.useModal(); const [modal, contextHolder] = Modal.useModal();
provide("modal", modal); provide("modal", modal);
@ -59,7 +59,7 @@ const tokenTheme = computed(() => {
return { return {
algorithm, algorithm,
token: tokens token: tokens,
}; };
}); });
// //

View File

@ -5,7 +5,7 @@ import OutputSelector from "/@/components/plugins/common/output-selector/index.v
import DnsProviderSelector from "/@/components/plugins/cert/dns-provider-selector/index.vue"; import DnsProviderSelector from "/@/components/plugins/cert/dns-provider-selector/index.vue";
import DomainsVerifyPlanEditor from "/@/components/plugins/cert/domains-verify-plan-editor/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 AccessSelector from "/@/views/certd/access/access-selector/index.vue";
import CertInfoSelector from "/@/views/certd/monitor/cert/selector/index.vue"; import CertInfoUpdater from "/@/views/certd/monitor/cert/updater/index.vue";
import InputPassword from "./common/input-password.vue"; import InputPassword from "./common/input-password.vue";
import ApiTest from "./common/api-test.vue"; import ApiTest from "./common/api-test.vue";
export * from "./cert/index.js"; export * from "./cert/index.js";
@ -15,7 +15,7 @@ export default {
app.component("DnsProviderSelector", DnsProviderSelector); app.component("DnsProviderSelector", DnsProviderSelector);
app.component("DomainsVerifyPlanEditor", DomainsVerifyPlanEditor); app.component("DomainsVerifyPlanEditor", DomainsVerifyPlanEditor);
app.component("AccessSelector", AccessSelector); app.component("AccessSelector", AccessSelector);
app.component("CertInfoSelector", CertInfoSelector); app.component("CertInfoUpdater", CertInfoUpdater);
app.component("ApiTest", ApiTest); app.component("ApiTest", ApiTest);

View File

@ -7,6 +7,7 @@ import { useRouter } from "vue-router";
import { useModal } from "/@/use/use-modal"; import { useModal } from "/@/use/use-modal";
import { notification } from "ant-design-vue"; import { notification } from "ant-design-vue";
import CertView from "/@/views/certd/pipeline/cert-view.vue"; import CertView from "/@/views/certd/pipeline/cert-view.vue";
import { useCertUpload } from "/@/views/certd/pipeline/cert-upload/use";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet { export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const api = certInfoApi; const api = certInfoApi;
@ -51,69 +52,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}); });
}; };
async function openUpload(id?: any) { const { openUploadCreateDialog, openUpdateCertDialog } = useCertUpload();
function createCrudOptions() {
return {
crudOptions: {
request: {
// addRequest: async (form: any) => {
// return await api.Upload(form);
// },
// editRequest: async (form: any) => {
// return await api.Upload(form);
// }
},
columns: {
id: {
title: "ID",
type: "number",
form: {
show: false,
},
},
"cert.crt": {
title: "证书",
type: "textarea",
form: {
component: {
rows: 4,
},
rules: [{ required: true, message: "此项必填" }],
col: { span: 24 },
},
},
"cert.key": {
title: "私钥",
type: "textarea",
form: {
component: {
rows: 4,
},
rules: [{ required: true, message: "此项必填" }],
col: { span: 24 },
},
},
},
form: {
wrapper: {
title: "上传自定义证书",
},
async doSubmit({ form }: any) {
if (!id) {
delete form.id;
} else {
form.id = id;
}
return await api.Upload(form);
},
},
},
};
}
const { crudOptions } = createCrudOptions();
const wrapperRef = await openCrudFormDialog({ crudOptions });
}
return { return {
crudOptions: { crudOptions: {
request: { request: {
@ -145,7 +84,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
type: "primary", type: "primary",
show: true, show: true,
async click() { async click() {
await openUpload(); await openUploadCreateDialog();
}, },
}, },
}, },
@ -179,7 +118,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
type: "link", type: "link",
icon: "ph:upload", icon: "ph:upload",
async click({ row }) { async click({ row }) {
await openUpload(row.id); await openUpdateCertDialog(row.id);
}, },
}, },
remove: { remove: {

View File

@ -1,123 +0,0 @@
<template>
<div class="cert-info-selector w-full">
<div class="flex-o w-full">
<fs-table-select
ref="tableSelectRef"
class="flex-0"
:model-value="modelValue"
:dict="optionsDictRef"
:create-crud-options="createCrudOptions"
:crud-options-override="{
search: { show: false },
table: {
scroll: {
x: 540,
},
},
}"
:show-current="false"
:show-select="false"
:dialog="{ width: 960 }"
:destroy-on-close="false"
height="400px"
@update:model-value="onChange"
@dialog-closed="doRefresh"
@selected-change="onSelectedChange"
/>
</div>
</div>
</template>
<script lang="tsx" setup>
import { inject, ref, Ref, watch } from "vue";
import { message } from "ant-design-vue";
import createCrudOptions from "../crud";
import { dict } from "@fast-crud/fast-crud";
import { certInfoApi } from "../api";
defineOptions({
name: "CertInfoSelector",
});
const props = defineProps<{
modelValue?: number | string;
type?: string;
placeholder?: string;
size?: string;
disabled?: boolean;
}>();
const onChange = async (value: number) => {
await emitValue(value);
};
const emit = defineEmits(["update:modelValue", "selectedChange", "change"]);
const tableSelectRef = ref();
const optionsDictRef = dict({
value: "id",
label: "domain",
getNodesByValues: async (values: any[]) => {
return await certInfoApi.GetOptionsByIds(values);
},
});
// async function openTableSelectDialog() {
// await tableSelectRef.value.open({});
// await tableSelectRef.value.crudExpose.openAdd({});
// }
const target: Ref<any> = ref({});
function clear() {
if (props.disabled) {
return;
}
emitValue(null);
}
async function emitValue(value: any) {
target.value = optionsDictRef.dataMap[value];
if (value !== 0 && pipeline?.value && target && pipeline.value.userId !== target.value.userId) {
message.error("对不起您不能修改他人流水线的证书仓库ID");
return;
}
emit("change", value);
emit("update:modelValue", value);
}
function onSelectedChange(value: any) {
if (value && value.length > 0) {
emit("selectedChange", value[0]);
} else {
emit("selectedChange", null);
}
}
// watch(
// () => {
// return props.modelValue;
// },
// async value => {
// await optionsDictRef.loadDict();
// target.value = optionsDictRef.dataMap[value];
// emit("selectedChange", target.value);
// },
// {
// immediate: true,
// }
// );
//pipeline
const pipeline = inject("pipeline", null);
async function doRefresh() {
await optionsDictRef.reloadDict();
}
</script>
<style lang="less">
.cert-info-selector {
width: 100%;
}
</style>

View File

@ -0,0 +1,55 @@
<template>
<div class="cert-info-updater w-full flex items-center">
<div class="flex-o">
<fs-values-format :model-value="modelValue" :dict="certInfoDict" />
<fs-button v-if="modelValue" 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";
defineOptions({
name: "CertInfoUpdater",
});
const props = defineProps<{
modelValue?: number | string;
type?: string;
placeholder?: string;
size?: string;
disabled?: boolean;
}>();
const emit = defineEmits(["updated"]);
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();
function onUpdated(res: any) {
emit("updated", res);
}
function onUploadClick() {
openUpdateCertDialog(props.modelValue, onUpdated);
}
</script>
<style lang="less">
.cert-info-selector {
width: 100%;
}
</style>

View File

@ -48,25 +48,6 @@ export function useCertUpload() {
} }
return inputs; return inputs;
} }
function topRender({ form, key }: any) {
function onChange(e: any) {
const file = e.target.files[0];
const size = file.size;
if (size > 100 * 1024) {
notification.error({
message: "文件超过100k请选择正确的证书文件",
});
return;
}
const fileReader = new FileReader();
fileReader.onload = function (e: any) {
const value = e.target.result;
set(form, key, value);
};
fileReader.readAsText(file); // 以文本形式读取文件
}
return <file-input class="mb-5" type="primary" text={"选择文件"} onChange={onChange} />;
}
async function openUploadCreateDialog() { async function openUploadCreateDialog() {
//检查是否流水线数量超出限制 //检查是否流水线数量超出限制
@ -87,32 +68,38 @@ export function useCertUpload() {
columns: { columns: {
"cert.crt": { "cert.crt": {
title: "证书", title: "证书",
type: "textarea", type: "text",
form: { form: {
component: { component: {
rows: 4, name: "pem-input",
placeholder: "-----BEGIN CERTIFICATE-----\n...\n...\n-----END CERTIFICATE-----", vModel: "modelValue",
textarea: {
rows: 4,
placeholder: "-----BEGIN CERTIFICATE-----\n...\n...\n-----END CERTIFICATE-----",
},
}, },
helper: "选择pem格式证书文件或者粘贴到此", helper: "选择pem格式证书文件或者粘贴到此",
rules: [{ required: true, message: "此项必填" }], rules: [{ required: true, message: "此项必填" }],
col: { span: 24 }, col: { span: 24 },
order: -9999, order: -9999,
topRender,
}, },
}, },
"cert.key": { "cert.key": {
title: "证书私钥", title: "证书私钥",
type: "textarea", type: "text",
form: { form: {
component: { component: {
rows: 4, name: "pem-input",
placeholder: "-----BEGIN PRIVATE KEY-----\n...\n...\n-----END PRIVATE KEY----- ", vModel: "modelValue",
textarea: {
rows: 4,
placeholder: "-----BEGIN PRIVATE KEY-----\n...\n...\n-----END PRIVATE KEY----- ",
},
}, },
helper: "选择pem格式证书私钥文件或者粘贴到此", helper: "选择pem格式证书私钥文件或者粘贴到此",
rules: [{ required: true, message: "此项必填" }], rules: [{ required: true, message: "此项必填" }],
col: { span: 24 }, col: { span: 24 },
order: -9999, order: -9999,
topRender,
}, },
}, },
...inputs, ...inputs,
@ -174,17 +161,22 @@ export function useCertUpload() {
wrapperRef.value = wrapper; wrapperRef.value = wrapper;
} }
async function openUpdateCertDialog(id: any) { async function openUpdateCertDialog(id: any, onSubmit?: any) {
function createCrudOptions() { function createCrudOptions() {
return { return {
crudOptions: { crudOptions: {
columns: { columns: {
"cert.crt": { "cert.crt": {
title: "证书", title: "证书",
type: "textarea", type: "text",
form: { form: {
component: { component: {
rows: 4, name: "pem-input",
vModel: "modelValue",
textarea: {
rows: 4,
placeholder: "-----BEGIN CERTIFICATE-----\n...\n...\n-----END CERTIFICATE-----",
},
}, },
rules: [{ required: true, message: "此项必填" }], rules: [{ required: true, message: "此项必填" }],
col: { span: 24 }, col: { span: 24 },
@ -195,7 +187,12 @@ export function useCertUpload() {
type: "textarea", type: "textarea",
form: { form: {
component: { component: {
rows: 4, name: "pem-input",
vModel: "modelValue",
textarea: {
rows: 4,
placeholder: "-----BEGIN PRIVATE KEY-----\n...\n...\n-----END PRIVATE KEY----- ",
},
}, },
rules: [{ required: true, message: "此项必填" }], rules: [{ required: true, message: "此项必填" }],
col: { span: 24 }, col: { span: 24 },
@ -212,7 +209,10 @@ export function useCertUpload() {
id: id, id: id,
cert: form.cert, cert: form.cert,
}; };
return await api.UploadCert(form); const res = await api.UploadCert(req);
if (onSubmit) {
await onSubmit(res);
}
}, },
}, },
}, },

View File

@ -7,10 +7,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { doRequest } from "/@/components/plugins/lib"; import { doRequest } from "/@/components/plugins/lib";
import { ref, useAttrs } from "vue"; import { ref, useAttrs, inject } from "vue";
import { useFormWrapper } from "@fast-crud/fast-crud"; import { useFormWrapper } from "@fast-crud/fast-crud";
import { notification } from "ant-design-vue"; import { notification } from "ant-design-vue";
import { merge } from "lodash-es";
defineOptions({ defineOptions({
name: "TaskShortcut", name: "TaskShortcut",
}); });
@ -32,7 +32,22 @@ async function openDialog() {
function createCrudOptions() { function createCrudOptions() {
return { return {
crudOptions: { crudOptions: {
columns: props.form.columns, columns: {
...props.form.columns,
immediateRun: {
title: "立即运行",
type: "switch",
span: 24,
form: {
value: true,
component: {
name: "a-switch",
vModel: "checked",
},
helper: "保存后是否立即触发运行流水线",
},
},
},
form: { form: {
wrapper: { wrapper: {
title: props.title, title: props.title,
@ -52,6 +67,7 @@ async function openDialog() {
await openCrudFormDialog({ crudOptions }); await openCrudFormDialog({ crudOptions });
} }
const getPipelineScope: any = inject("getPipelineScope");
const doPluginFormSubmit = async (formData: any) => { const doPluginFormSubmit = async (formData: any) => {
if (loading.value) { if (loading.value) {
return; return;
@ -66,6 +82,27 @@ const doPluginFormSubmit = async (formData: any) => {
input: props.input, input: props.input,
data: formData, data: formData,
}); });
if (res.input) {
const { save, findStep } = getPipelineScope();
const step = findStep(res.input);
if (step) {
//
mergeWith(step.input, res.input, (objValue, srcValue) => {
if (isArray(objValue)) {
return srcValue;
}
});
//
save(false);
}
}
if (formData.immediateRun) {
const { run } = getPipelineScope();
run();
}
return res; return res;
} finally { } finally {
loading.value = false; loading.value = false;

View File

@ -647,31 +647,34 @@ export default defineComponent({
validateErrors.value[taskId] = errors; validateErrors.value[taskId] = errors;
errors.push(error); errors.push(error);
} }
function doValidate() {
validateErrors.value = {};
const stepIds: string[] = []; function eachSteps(pp: any, callback: any) {
//output id if (pp.stages) {
const pp = pipeline.value; for (const stage of pp.stages) {
function eachSteps(callback: any) { if (stage.tasks) {
if (pp.stages) { for (const task of stage.tasks) {
for (const stage of pp.stages) { if (task.steps) {
if (stage.tasks) { for (const step of task.steps) {
for (const task of stage.tasks) { callback(step, task, stage);
if (task.steps) {
for (const step of task.steps) {
callback(step, task, stage);
}
} }
} }
} }
} }
} }
} }
}
function doValidate() {
validateErrors.value = {};
const stepIds: string[] = [];
//output id
const pp = pipeline.value;
//stepid //stepid
let hasError = false; let hasError = false;
let errorMessage = ""; let errorMessage = "";
eachSteps((step: any, task: any, stage: any) => { eachSteps(pp, (step: any, task: any, stage: any) => {
stepIds.push(step.id); stepIds.push(step.id);
if (step.input) { if (step.input) {
for (const key in step.input) { for (const key in step.input) {
@ -709,7 +712,7 @@ export default defineComponent({
function hasValidateError(taskId: string) { function hasValidateError(taskId: string) {
return validateErrors.value[taskId] != null; return validateErrors.value[taskId] != null;
} }
const save = async () => { const save = async (offEdit = true) => {
doValidate(); doValidate();
saveLoading.value = true; saveLoading.value = true;
@ -728,7 +731,9 @@ export default defineComponent({
await props.options.doSave(pipeline.value); await props.options.doSave(pipeline.value);
} }
toggleEditMode(false); if (offEdit) {
toggleEditMode(false);
}
} finally { } finally {
saveLoading.value = false; saveLoading.value = false;
} }
@ -743,6 +748,17 @@ export default defineComponent({
toggleEditMode(false); 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;
}
return { return {
run, run,
save, save,
@ -750,6 +766,7 @@ export default defineComponent({
cancel, cancel,
saveLoading, saveLoading,
hasValidateError, hasValidateError,
findStep,
}; };
} }
@ -821,6 +838,17 @@ export default defineComponent({
const settingStore = useSettingStore(); const settingStore = useSettingStore();
const userStore = useUserStore(); const userStore = useUserStore();
const actions = useActions();
const trigger = useTrigger();
provide("getPipelineScope", () => {
return {
run: actions.run,
pipeline: pipeline,
save: actions.save,
findStep: actions.findStep,
};
});
return { return {
pipeline, pipeline,
currentHistory, currentHistory,
@ -830,8 +858,8 @@ export default defineComponent({
settingStore, settingStore,
...useTaskRet, ...useTaskRet,
...useStageRet, ...useStageRet,
...useTrigger(), ...trigger,
...useActions(), ...actions,
...useHistory(), ...useHistory(),
...useNotification(), ...useNotification(),
...useScroll(), ...useScroll(),

View File

@ -3,7 +3,6 @@ import { BaseService, CommonException } from "@certd/lib-server";
import { InjectEntityModel } from "@midwayjs/typeorm"; import { InjectEntityModel } from "@midwayjs/typeorm";
import { EntityManager, Repository } from "typeorm"; import { EntityManager, Repository } from "typeorm";
import { CertInfoEntity } from "../entity/cert-info.js"; import { CertInfoEntity } from "../entity/cert-info.js";
import { logger } from "@certd/basic";
import { CertInfo, CertReader } from "@certd/plugin-cert"; import { CertInfo, CertReader } from "@certd/plugin-cert";
import { PipelineService } from "../../pipeline/service/pipeline-service.js"; import { PipelineService } from "../../pipeline/service/pipeline-service.js";
import { CertInfoService } from "./cert-info-service.js"; import { CertInfoService } from "./cert-info-service.js";
@ -69,9 +68,12 @@ export class CertUploadService extends BaseService<CertInfoEntity> {
certReader: new CertReader(req.cert) certReader: new CertReader(req.cert)
}) })
if (certInfoEntity.pipelineId) { return {
logger.info( `触发流水线部署:${certInfoEntity.pipelineId}`) id: certInfoEntity.id,
await this.pipelineService.trigger(certInfoEntity.pipelineId) domains: certInfoEntity.domains.split(','),
pipelineId: certInfoEntity.pipelineId,
fromType: certInfoEntity.fromType,
updateTime: certInfoEntity.updateTime,
} }
} }
@ -153,7 +155,10 @@ export class CertUploadService extends BaseService<CertInfoEntity> {
return { return {
id:newCertInfo.id, id:newCertInfo.id,
pipelineId: newPipeline.id pipelineId: newPipeline.id,
domains: newCertInfo.domains.split(','),
fromType: newCertInfo.fromType,
updateTime: newCertInfo.updateTime,
} }
}) })

View File

@ -9,7 +9,7 @@ export type DnslaRecord = {
// 这里通过IsDnsProvider注册一个dnsProvider // 这里通过IsDnsProvider注册一个dnsProvider
@IsDnsProvider({ @IsDnsProvider({
name: 'dnsla', name: 'dnsla',
title: 'dnsla', title: 'dns.la',
desc: 'dns.la', desc: 'dns.la',
icon: 'arcticons:dns-changer-3', icon: 'arcticons:dns-changer-3',
// 这里是对应的 cloudflare的access类型名称 // 这里是对应的 cloudflare的access类型名称
@ -146,7 +146,7 @@ export class DnslaDnsProvider extends AbstractDnsProvider<DnslaRecord> {
type: 16, type: 16,
host: fullRecord, host: fullRecord,
data: value, data: value,
ttl: 300, ttl: 1,
}); });
return res.data; return res.data;