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

View File

@ -1,7 +1,7 @@
<template>
<AConfigProvider :locale="locale" :theme="tokenTheme">
<contextHolder />
<fs-form-provider>
<contextHolder />
<router-view />
</fs-form-provider>
</AConfigProvider>
@ -21,7 +21,7 @@ import AConfigProvider from "ant-design-vue/es/config-provider";
import { Modal } from "ant-design-vue";
defineOptions({
name: "App"
name: "App",
});
const [modal, contextHolder] = Modal.useModal();
provide("modal", modal);
@ -59,7 +59,7 @@ const tokenTheme = computed(() => {
return {
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 DomainsVerifyPlanEditor from "/@/components/plugins/cert/domains-verify-plan-editor/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 ApiTest from "./common/api-test.vue";
export * from "./cert/index.js";
@ -15,7 +15,7 @@ export default {
app.component("DnsProviderSelector", DnsProviderSelector);
app.component("DomainsVerifyPlanEditor", DomainsVerifyPlanEditor);
app.component("AccessSelector", AccessSelector);
app.component("CertInfoSelector", CertInfoSelector);
app.component("CertInfoUpdater", CertInfoUpdater);
app.component("ApiTest", ApiTest);

View File

@ -7,6 +7,7 @@ import { useRouter } from "vue-router";
import { useModal } from "/@/use/use-modal";
import { notification } from "ant-design-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 {
const api = certInfoApi;
@ -51,69 +52,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
});
};
async function openUpload(id?: any) {
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 });
}
const { openUploadCreateDialog, openUpdateCertDialog } = useCertUpload();
return {
crudOptions: {
request: {
@ -145,7 +84,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
type: "primary",
show: true,
async click() {
await openUpload();
await openUploadCreateDialog();
},
},
},
@ -179,7 +118,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
type: "link",
icon: "ph:upload",
async click({ row }) {
await openUpload(row.id);
await openUpdateCertDialog(row.id);
},
},
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;
}
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() {
//检查是否流水线数量超出限制
@ -87,32 +68,38 @@ export function useCertUpload() {
columns: {
"cert.crt": {
title: "证书",
type: "textarea",
type: "text",
form: {
component: {
rows: 4,
placeholder: "-----BEGIN CERTIFICATE-----\n...\n...\n-----END CERTIFICATE-----",
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,
topRender,
},
},
"cert.key": {
title: "证书私钥",
type: "textarea",
type: "text",
form: {
component: {
rows: 4,
placeholder: "-----BEGIN PRIVATE KEY-----\n...\n...\n-----END PRIVATE KEY----- ",
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,
topRender,
},
},
...inputs,
@ -174,17 +161,22 @@ export function useCertUpload() {
wrapperRef.value = wrapper;
}
async function openUpdateCertDialog(id: any) {
async function openUpdateCertDialog(id: any, onSubmit?: any) {
function createCrudOptions() {
return {
crudOptions: {
columns: {
"cert.crt": {
title: "证书",
type: "textarea",
type: "text",
form: {
component: {
rows: 4,
name: "pem-input",
vModel: "modelValue",
textarea: {
rows: 4,
placeholder: "-----BEGIN CERTIFICATE-----\n...\n...\n-----END CERTIFICATE-----",
},
},
rules: [{ required: true, message: "此项必填" }],
col: { span: 24 },
@ -195,7 +187,12 @@ export function useCertUpload() {
type: "textarea",
form: {
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: "此项必填" }],
col: { span: 24 },
@ -212,7 +209,10 @@ export function useCertUpload() {
id: id,
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>
<script setup lang="ts">
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 { notification } from "ant-design-vue";
import { merge } from "lodash-es";
defineOptions({
name: "TaskShortcut",
});
@ -32,7 +32,22 @@ async function openDialog() {
function createCrudOptions() {
return {
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: {
wrapper: {
title: props.title,
@ -52,6 +67,7 @@ async function openDialog() {
await openCrudFormDialog({ crudOptions });
}
const getPipelineScope: any = inject("getPipelineScope");
const doPluginFormSubmit = async (formData: any) => {
if (loading.value) {
return;
@ -66,6 +82,27 @@ const doPluginFormSubmit = async (formData: any) => {
input: props.input,
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;
} finally {
loading.value = false;

View File

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

View File

@ -3,7 +3,6 @@ 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 { logger } from "@certd/basic";
import { CertInfo, CertReader } from "@certd/plugin-cert";
import { PipelineService } from "../../pipeline/service/pipeline-service.js";
import { CertInfoService } from "./cert-info-service.js";
@ -69,9 +68,12 @@ export class CertUploadService extends BaseService<CertInfoEntity> {
certReader: new CertReader(req.cert)
})
if (certInfoEntity.pipelineId) {
logger.info( `触发流水线部署:${certInfoEntity.pipelineId}`)
await this.pipelineService.trigger(certInfoEntity.pipelineId)
return {
id: certInfoEntity.id,
domains: certInfoEntity.domains.split(','),
pipelineId: certInfoEntity.pipelineId,
fromType: certInfoEntity.fromType,
updateTime: certInfoEntity.updateTime,
}
}
@ -153,7 +155,10 @@ export class CertUploadService extends BaseService<CertInfoEntity> {
return {
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({
name: 'dnsla',
title: 'dnsla',
title: 'dns.la',
desc: 'dns.la',
icon: 'arcticons:dns-changer-3',
// 这里是对应的 cloudflare的access类型名称
@ -146,7 +146,7 @@ export class DnslaDnsProvider extends AbstractDnsProvider<DnslaRecord> {
type: 16,
host: fullRecord,
data: value,
ttl: 300,
ttl: 1,
});
return res.data;