mirror of https://github.com/certd/certd
feat: 邮件通知
parent
64afebecd4
commit
937e3fac19
|
@ -2,9 +2,9 @@
|
||||||
"name": "@certd/pipeline",
|
"name": "@certd/pipeline",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"main": "./dist/bundle.js",
|
"main": "./src",
|
||||||
"module": "./dist/pipeline.mjs",
|
"module": "./src",
|
||||||
"types": "./dist/d/index.d.ts",
|
"types": "./src",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"main": "./dist/bundle.js",
|
"main": "./dist/bundle.js",
|
||||||
"module": "./dist/bundle.mjs",
|
"module": "./dist/bundle.mjs",
|
||||||
|
@ -19,6 +19,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
"node-forge": "^1.3.1",
|
"node-forge": "^1.3.1",
|
||||||
|
"nodemailer": "^6.9.3",
|
||||||
"qs": "^6.11.2"
|
"qs": "^6.11.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ConcurrencyStrategy, NotificationType, NotificationWhen, Pipeline, ResultType, Runnable, RunStrategy, Stage, Step, Task } from "../d.ts";
|
import { ConcurrencyStrategy, NotificationWhen, Pipeline, ResultType, Runnable, RunStrategy, Stage, Step, Task } from "../d.ts";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { RunHistory, RunnableCollection } from "./run-history";
|
import { RunHistory, RunnableCollection } from "./run-history";
|
||||||
import { AbstractTaskPlugin, PluginDefine, pluginRegistry } from "../plugin";
|
import { AbstractTaskPlugin, PluginDefine, pluginRegistry } from "../plugin";
|
||||||
|
@ -216,13 +216,13 @@ export class Executor {
|
||||||
let subject = "";
|
let subject = "";
|
||||||
let content = "";
|
let content = "";
|
||||||
if (when === "start") {
|
if (when === "start") {
|
||||||
subject = `【CertD】${this.pipeline.title} 开始执行,buildId:${this.runtime.id}`;
|
subject = `【CertD】开始执行,${this.pipeline.title}, buildId:${this.runtime.id}`;
|
||||||
content = `【CertD】${this.pipeline.title} 开始执行,buildId:${this.runtime.id}`;
|
content = subject;
|
||||||
} else if (when === "success") {
|
} else if (when === "success") {
|
||||||
subject = `【CertD】${this.pipeline.title} 执行成功,buildId:${this.runtime.id}`;
|
subject = `【CertD】执行成功,${this.pipeline.title}, buildId:${this.runtime.id}`;
|
||||||
content = `【CertD】${this.pipeline.title} 执行成功,buildId:${this.runtime.id}`;
|
content = subject;
|
||||||
} else if (when === "error") {
|
} else if (when === "error") {
|
||||||
subject = `【CertD】${this.pipeline.title} 执行失败,buildId:${this.runtime.id}`;
|
subject = `【CertD】执行失败,${this.pipeline.title}, buildId:${this.runtime.id}`;
|
||||||
content = `<pre>${error.message}</pre>`;
|
content = `<pre>${error.message}</pre>`;
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
|
@ -234,6 +234,7 @@ export class Executor {
|
||||||
}
|
}
|
||||||
if (notification.type === "email") {
|
if (notification.type === "email") {
|
||||||
this.options.emailService?.send({
|
this.options.emailService?.send({
|
||||||
|
userId: this.pipeline.userId,
|
||||||
subject,
|
subject,
|
||||||
content,
|
content,
|
||||||
receivers: notification.options.receivers,
|
receivers: notification.options.receivers,
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
export type EmailSend = {
|
||||||
|
userId: number;
|
||||||
|
subject: string;
|
||||||
|
content: string;
|
||||||
|
receivers: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IEmailService {
|
||||||
|
send(email: EmailSend): Promise<void>;
|
||||||
|
}
|
|
@ -1,9 +1 @@
|
||||||
export type EmailSend = {
|
export * from "./email";
|
||||||
subject: string;
|
|
||||||
content: string;
|
|
||||||
receivers: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface IEmailService {
|
|
||||||
send(email: EmailSend): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
VITE_APP_API=/api
|
VITE_APP_API=/api
|
||||||
#登录与权限关闭
|
#登录与权限关闭
|
||||||
VITE_APP_PM_ENABLED=true
|
VITE_APP_PM_ENABLED=true
|
||||||
|
VITE_APP_TITLE=Certd
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/logo.svg" />
|
<link rel="icon" href="/logo.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>antdv-fast-crud</title>
|
<title>Certd-让你的证书永不过期</title>
|
||||||
<link rel="stylesheet" type="text/css" href="/index.css" />
|
<link rel="stylesheet" type="text/css" href="/index.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -106,8 +106,8 @@ function createService() {
|
||||||
* @description 创建请求方法
|
* @description 创建请求方法
|
||||||
* @param {Object} service axios 实例
|
* @param {Object} service axios 实例
|
||||||
*/
|
*/
|
||||||
function createRequestFunction(service) {
|
function createRequestFunction(service: any) {
|
||||||
return function (config) {
|
return function (config: any) {
|
||||||
const configDefault = {
|
const configDefault = {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": get(config, "headers.Content-Type", "application/json")
|
"Content-Type": get(config, "headers.Content-Type", "application/json")
|
||||||
|
|
|
@ -48,7 +48,10 @@ export function responseError(data = {}, msg = "请求失败", code = 500) {
|
||||||
* @description 记录和显示错误
|
* @description 记录和显示错误
|
||||||
* @param {Error} error 错误对象
|
* @param {Error} error 错误对象
|
||||||
*/
|
*/
|
||||||
export function errorLog(error) {
|
export function errorLog(error: any) {
|
||||||
|
if (error?.response?.data?.message) {
|
||||||
|
error.message = error?.response?.data?.message;
|
||||||
|
}
|
||||||
// 打印到控制台
|
// 打印到控制台
|
||||||
console.error(error);
|
console.error(error);
|
||||||
// 显示提示
|
// 显示提示
|
||||||
|
@ -59,8 +62,6 @@ export function errorLog(error) {
|
||||||
* @description 创建一个错误
|
* @description 创建一个错误
|
||||||
* @param {String} msg 错误信息
|
* @param {String} msg 错误信息
|
||||||
*/
|
*/
|
||||||
export function errorCreate(msg) {
|
export function errorCreate(msg: string) {
|
||||||
const error = new Error(msg);
|
throw new Error(msg);
|
||||||
errorLog(error);
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,28 @@ export const certdResources = [
|
||||||
meta: {
|
meta: {
|
||||||
icon: "ion:disc-outline"
|
icon: "ion:disc-outline"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "设置",
|
||||||
|
name: "certdSettings",
|
||||||
|
path: "/certd/settings",
|
||||||
|
redirect: "/certd/settings/email",
|
||||||
|
meta: {
|
||||||
|
icon: "ion:settings-outline",
|
||||||
|
auth: true
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
title: "邮箱设置",
|
||||||
|
name: "email",
|
||||||
|
path: "/certd/settings/email",
|
||||||
|
component: "/certd/settings/email-setting.vue",
|
||||||
|
meta: {
|
||||||
|
icon: "ion:mail-outline",
|
||||||
|
auth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
|
// @ts-ignore
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
export function getEnvValue(key) {
|
export function getEnvValue(key: string) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return import.meta.env["VITE_APP_" + key];
|
return import.meta.env["VITE_APP_" + key];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EnvConfig {
|
export class EnvConfig {
|
||||||
API;
|
API: string;
|
||||||
MODE;
|
MODE: string;
|
||||||
STORAGE;
|
STORAGE: string;
|
||||||
TITLE;
|
TITLE: string;
|
||||||
PM_ENABLED;
|
PM_ENABLED: string;
|
||||||
constructor() {
|
constructor() {
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
@ -19,6 +20,7 @@ export class EnvConfig {
|
||||||
_.forEach(import.meta.env, (value, key) => {
|
_.forEach(import.meta.env, (value, key) => {
|
||||||
if (key.startsWith("VITE_APP")) {
|
if (key.startsWith("VITE_APP")) {
|
||||||
key = key.replace("VITE_APP_", "");
|
key = key.replace("VITE_APP_", "");
|
||||||
|
//@ts-ignore
|
||||||
this[key] = value;
|
this[key] = value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -26,7 +28,8 @@ export class EnvConfig {
|
||||||
this.MODE = import.meta.env.MODE;
|
this.MODE = import.meta.env.MODE;
|
||||||
}
|
}
|
||||||
|
|
||||||
get(key, defaultValue) {
|
get(key: string, defaultValue: string) {
|
||||||
|
//@ts-ignore
|
||||||
return this[key] ?? defaultValue;
|
return this[key] ?? defaultValue;
|
||||||
}
|
}
|
||||||
isDev() {
|
isDev() {
|
||||||
|
|
|
@ -2,9 +2,9 @@ import { env } from "./util.env";
|
||||||
export const site = {
|
export const site = {
|
||||||
/**
|
/**
|
||||||
* @description 更新标题
|
* @description 更新标题
|
||||||
* @param {String} title 标题
|
* @param titleText
|
||||||
*/
|
*/
|
||||||
title: function (titleText) {
|
title: function (titleText: string) {
|
||||||
const processTitle = env.TITLE || "FsAdmin";
|
const processTitle = env.TITLE || "FsAdmin";
|
||||||
window.document.title = `${processTitle}${titleText ? ` | ${titleText}` : ""}`;
|
window.document.title = `${processTitle}${titleText ? ` | ${titleText}` : ""}`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { RunHistory } from "/@/views/certd/pipeline/pipeline/type";
|
||||||
|
|
||||||
const apiPrefix = "/pi/history";
|
const apiPrefix = "/pi/history";
|
||||||
|
|
||||||
export async function GetList(query) {
|
export async function GetList(query: any) {
|
||||||
const list = await request({
|
const list = await request({
|
||||||
url: apiPrefix + "/list",
|
url: apiPrefix + "/list",
|
||||||
method: "post",
|
method: "post",
|
||||||
|
@ -18,7 +18,7 @@ export async function GetList(query) {
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GetDetail(query): Promise<RunHistory> {
|
export async function GetDetail(query: any): Promise<RunHistory> {
|
||||||
const detail = await request({
|
const detail = await request({
|
||||||
url: apiPrefix + "/detail",
|
url: apiPrefix + "/detail",
|
||||||
method: "post",
|
method: "post",
|
||||||
|
|
|
@ -0,0 +1,200 @@
|
||||||
|
<template>
|
||||||
|
<a-drawer v-model:visible="notificationDrawerVisible" placement="right" :closable="true" width="600px" class="pi-notification-form" :after-visible-change="notificationDrawerOnAfterVisibleChange">
|
||||||
|
<template #title>
|
||||||
|
编辑触发器
|
||||||
|
<a-button v-if="mode === 'edit'" @click="notificationDelete()">
|
||||||
|
<template #icon><DeleteOutlined /></template>
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
<template v-if="currentNotification">
|
||||||
|
<pi-container>
|
||||||
|
<a-form ref="notificationFormRef" class="notification-form" :model="currentNotification" :label-col="labelCol" :wrapper-col="wrapperCol">
|
||||||
|
<fs-form-item
|
||||||
|
v-model="currentNotification.type"
|
||||||
|
:item="{
|
||||||
|
title: '类型',
|
||||||
|
key: 'type',
|
||||||
|
value: 'email',
|
||||||
|
component: {
|
||||||
|
name: 'a-select',
|
||||||
|
vModel: 'value',
|
||||||
|
disabled: !editMode,
|
||||||
|
options: [{ value: 'email', label: '邮件' }]
|
||||||
|
},
|
||||||
|
rules: [{ required: true, message: '此项必填' }]
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<fs-form-item
|
||||||
|
v-model="currentNotification.when"
|
||||||
|
:item="{
|
||||||
|
title: '触发时机',
|
||||||
|
key: 'type',
|
||||||
|
value: ['error'],
|
||||||
|
component: {
|
||||||
|
name: 'a-select',
|
||||||
|
vModel: 'value',
|
||||||
|
disabled: !editMode,
|
||||||
|
mode: 'multiple',
|
||||||
|
options: [
|
||||||
|
{ value: 'start', label: '开始时' },
|
||||||
|
{ value: 'success', label: '成功时' },
|
||||||
|
{ value: 'error', label: '错误时' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
rules: [{ required: true, message: '此项必填' }]
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<pi-notification-form-email ref="optionsRef" v-model:options="currentNotification.options"></pi-notification-form-email>
|
||||||
|
</a-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<a-form-item v-if="editMode" :wrapper-col="{ span: 14, offset: 4 }">
|
||||||
|
<a-button type="primary" @click="notificationSave"> 确定 </a-button>
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
</pi-container>
|
||||||
|
</template>
|
||||||
|
</a-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Modal } from "ant-design-vue";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import PiNotificationFormEmail from "./pi-notification-form-email.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "PiNotificationForm",
|
||||||
|
components: { PiNotificationFormEmail },
|
||||||
|
props: {
|
||||||
|
editMode: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ["update"],
|
||||||
|
setup(props: any, context: any) {
|
||||||
|
/**
|
||||||
|
* notification drawer
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function useNotificationForm() {
|
||||||
|
const mode = ref("add");
|
||||||
|
const callback = ref();
|
||||||
|
const currentNotification = ref({ type: undefined, when: [], options: {} });
|
||||||
|
const currentPlugin = ref({});
|
||||||
|
const notificationFormRef = ref(null);
|
||||||
|
const notificationDrawerVisible = ref(false);
|
||||||
|
const optionsRef = ref();
|
||||||
|
const rules = ref({
|
||||||
|
type: [
|
||||||
|
{
|
||||||
|
type: "string",
|
||||||
|
required: true,
|
||||||
|
message: "请选择类型"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
when: [
|
||||||
|
{
|
||||||
|
type: "string",
|
||||||
|
required: true,
|
||||||
|
message: "请选择通知时机"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const notificationDrawerShow = () => {
|
||||||
|
notificationDrawerVisible.value = true;
|
||||||
|
};
|
||||||
|
const notificationDrawerClose = () => {
|
||||||
|
notificationDrawerVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const notificationDrawerOnAfterVisibleChange = (val: any) => {
|
||||||
|
console.log("notificationDrawerOnAfterVisibleChange", val);
|
||||||
|
};
|
||||||
|
|
||||||
|
const notificationOpen = (notification: any, emit: any) => {
|
||||||
|
callback.value = emit;
|
||||||
|
currentNotification.value = _.cloneDeep(notification);
|
||||||
|
console.log("currentNotificationOpen", currentNotification.value);
|
||||||
|
notificationDrawerShow();
|
||||||
|
};
|
||||||
|
|
||||||
|
const notificationAdd = (emit: any) => {
|
||||||
|
mode.value = "add";
|
||||||
|
const notification = { id: nanoid(), type: "email", when: ["error"] };
|
||||||
|
notificationOpen(notification, emit);
|
||||||
|
};
|
||||||
|
|
||||||
|
const notificationEdit = (notification: any, emit: any) => {
|
||||||
|
mode.value = "edit";
|
||||||
|
notificationOpen(notification, emit);
|
||||||
|
};
|
||||||
|
|
||||||
|
const notificationView = (notification: any, emit: any) => {
|
||||||
|
mode.value = "view";
|
||||||
|
notificationOpen(notification, emit);
|
||||||
|
};
|
||||||
|
|
||||||
|
const notificationSave = async (e: any) => {
|
||||||
|
currentNotification.value.options = await optionsRef.value.getValue();
|
||||||
|
console.log("currentNotificationSave", currentNotification.value);
|
||||||
|
try {
|
||||||
|
await notificationFormRef.value.validate();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("表单验证失败:", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback.value("save", currentNotification.value);
|
||||||
|
notificationDrawerClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const notificationDelete = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: "确认",
|
||||||
|
content: `确定要删除此触发器吗?`,
|
||||||
|
async onOk() {
|
||||||
|
callback.value("delete");
|
||||||
|
notificationDrawerClose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const blankFn = () => {
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
notificationFormRef,
|
||||||
|
mode,
|
||||||
|
notificationAdd,
|
||||||
|
notificationEdit,
|
||||||
|
notificationView,
|
||||||
|
notificationDrawerShow,
|
||||||
|
notificationDrawerVisible,
|
||||||
|
notificationDrawerOnAfterVisibleChange,
|
||||||
|
currentNotification,
|
||||||
|
currentPlugin,
|
||||||
|
notificationSave,
|
||||||
|
notificationDelete,
|
||||||
|
rules,
|
||||||
|
blankFn,
|
||||||
|
optionsRef
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...useNotificationForm(),
|
||||||
|
labelCol: { span: 6 },
|
||||||
|
wrapperCol: { span: 16 }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.pi-notification-form {
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,57 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<fs-form-item
|
||||||
|
v-model="optionsFormState.receivers"
|
||||||
|
:item="{
|
||||||
|
title: '收件邮箱',
|
||||||
|
key: 'type',
|
||||||
|
component: {
|
||||||
|
name: 'a-select',
|
||||||
|
vModel: 'value',
|
||||||
|
mode: 'tags'
|
||||||
|
},
|
||||||
|
rules: [{ required: true, message: '此项必填' }]
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Ref, ref, watch } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
options: {
|
||||||
|
type: Object as PropType<any>,
|
||||||
|
default: () => {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const optionsFormState: Ref<any> = ref({});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => {
|
||||||
|
return props.options;
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
optionsFormState.value = {
|
||||||
|
...props.options
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits(["change"]);
|
||||||
|
function doEmit() {
|
||||||
|
emit("change", { ...optionsFormState.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValue() {
|
||||||
|
return { ...optionsFormState.value };
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
doEmit,
|
||||||
|
getValue
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -2,7 +2,6 @@
|
||||||
<fs-page v-if="pipeline" class="page-pipeline-edit">
|
<fs-page v-if="pipeline" class="page-pipeline-edit">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<fs-button icon="ion:left" @click="goBack" />
|
|
||||||
<pi-editable v-model="pipeline.title" :hover-show="false" :disabled="!editMode"></pi-editable>
|
<pi-editable v-model="pipeline.title" :hover-show="false" :disabled="!editMode"></pi-editable>
|
||||||
</div>
|
</div>
|
||||||
<div class="more">
|
<div class="more">
|
||||||
|
@ -63,7 +62,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-for="(stage, index) of pipeline.stages" :key="stage.id" class="stage" :class="{ 'last-stage': !editMode && index === pipeline.stages.length - 1 }">
|
<div v-for="(stage, index) of pipeline.stages" :key="stage.id" class="stage" :class="{ 'last-stage': isLastStage(index) }">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<pi-editable v-model="stage.title" :disabled="!editMode"></pi-editable>
|
<pi-editable v-model="stage.title" :disabled="!editMode"></pi-editable>
|
||||||
</div>
|
</div>
|
||||||
|
@ -118,6 +117,48 @@
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-for="(item, ii) of pipeline.notifications" :key="ii" class="task-container">
|
||||||
|
<div class="line">
|
||||||
|
<div class="flow-line"></div>
|
||||||
|
</div>
|
||||||
|
<div class="task">
|
||||||
|
<a-button shape="round" @click="notificationEdit(item, ii as number)">
|
||||||
|
<fs-icon icon="ion:notifications"></fs-icon>
|
||||||
|
【通知】 {{ item.type }}
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="task-container">
|
||||||
|
<div class="line">
|
||||||
|
<div class="flow-line"></div>
|
||||||
|
</div>
|
||||||
|
<div class="task">
|
||||||
|
<a-button shape="round" type="dashed" @click="notificationAdd()">
|
||||||
|
<fs-icon icon="ion:add-circle-outline"></fs-icon>
|
||||||
|
|
||||||
|
添加通知
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="stage last-stage">
|
||||||
|
<div class="title">
|
||||||
|
<pi-editable model-value="结束" :disabled="true" />
|
||||||
|
</div>
|
||||||
|
<div class="tasks">
|
||||||
|
<div v-for="(item, index) of pipeline.notifications" :key="index" class="task-container" :class="{ 'first-task': index == 0 }">
|
||||||
|
<div class="line">
|
||||||
|
<div class="flow-line"></div>
|
||||||
|
</div>
|
||||||
|
<div class="task">
|
||||||
|
<a-button shape="round" @click="notificationEdit(item, index)">
|
||||||
|
<fs-icon icon="ion:notifications"></fs-icon>
|
||||||
|
|
||||||
|
【通知】 {{ item.type }}
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -140,6 +181,7 @@
|
||||||
<pi-task-form ref="taskFormRef" :edit-mode="editMode"></pi-task-form>
|
<pi-task-form ref="taskFormRef" :edit-mode="editMode"></pi-task-form>
|
||||||
<pi-trigger-form ref="triggerFormRef" :edit-mode="editMode"></pi-trigger-form>
|
<pi-trigger-form ref="triggerFormRef" :edit-mode="editMode"></pi-trigger-form>
|
||||||
<pi-task-view ref="taskViewRef"></pi-task-view>
|
<pi-task-view ref="taskViewRef"></pi-task-view>
|
||||||
|
<PiNotificationForm ref="notificationFormRef" :edit-mode="editMode"></PiNotificationForm>
|
||||||
</fs-page>
|
</fs-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -148,9 +190,10 @@ import { defineComponent, ref, provide, Ref, watch } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import PiTaskForm from "./component/task-form/index.vue";
|
import PiTaskForm from "./component/task-form/index.vue";
|
||||||
import PiTriggerForm from "./component/trigger-form/index.vue";
|
import PiTriggerForm from "./component/trigger-form/index.vue";
|
||||||
|
import PiNotificationForm from "./component/notification-form/index.vue";
|
||||||
import PiTaskView from "./component/task-view/index.vue";
|
import PiTaskView from "./component/task-view/index.vue";
|
||||||
import PiStatusShow from "./component/status-show.vue";
|
import PiStatusShow from "./component/status-show.vue";
|
||||||
import _ from "lodash-es";
|
import _ from "lodash";
|
||||||
import { message, Modal, notification } from "ant-design-vue";
|
import { message, Modal, notification } from "ant-design-vue";
|
||||||
import { pluginManager } from "/@/views/certd/pipeline/pipeline/plugin";
|
import { pluginManager } from "/@/views/certd/pipeline/pipeline/plugin";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
@ -159,7 +202,7 @@ import PiHistoryTimelineItem from "/@/views/certd/pipeline/pipeline/component/hi
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "PipelineEdit",
|
name: "PipelineEdit",
|
||||||
// eslint-disable-next-line vue/no-unused-components
|
// eslint-disable-next-line vue/no-unused-components
|
||||||
components: { PiHistoryTimelineItem, PiTaskForm, PiTriggerForm, PiTaskView, PiStatusShow },
|
components: { PiHistoryTimelineItem, PiTaskForm, PiTriggerForm, PiTaskView, PiStatusShow, PiNotificationForm },
|
||||||
props: {
|
props: {
|
||||||
pipelineId: {
|
pipelineId: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
|
@ -269,7 +312,7 @@ export default defineComponent({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const detail: PipelineDetail = await props.options.getPipelineDetail({ pipelineId: value });
|
const detail: PipelineDetail = await props.options.getPipelineDetail({ pipelineId: value });
|
||||||
currentPipeline.value = _.merge({ title: "新管道流程", stages: [], triggers: [] }, detail.pipeline);
|
currentPipeline.value = _.merge({ title: "新管道流程", stages: [], triggers: [], notifications: [] }, detail.pipeline);
|
||||||
pipeline.value = currentPipeline.value;
|
pipeline.value = currentPipeline.value;
|
||||||
await loadHistoryList(true);
|
await loadHistoryList(true);
|
||||||
},
|
},
|
||||||
|
@ -369,8 +412,13 @@ export default defineComponent({
|
||||||
pipeline.value.stages.splice(stageIndex, 0, stage);
|
pipeline.value.stages.splice(stageIndex, 0, stage);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function isLastStage(index: number) {
|
||||||
|
return !props.editMode && index === pipeline.value.stages.length - 1 && pipeline.value.notifications?.length < 1;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
stageAdd
|
stageAdd,
|
||||||
|
isLastStage
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -406,6 +454,41 @@ export default defineComponent({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useNotification() {
|
||||||
|
const notificationFormRef = ref();
|
||||||
|
const notificationAdd = () => {
|
||||||
|
notificationFormRef.value.notificationAdd((type: string, value: any) => {
|
||||||
|
if (type === "save") {
|
||||||
|
if (pipeline.value.notifications == null) {
|
||||||
|
pipeline.value.notifications = [];
|
||||||
|
}
|
||||||
|
pipeline.value.notifications.push(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const notificationEdit = (notification: any, index: any) => {
|
||||||
|
if (notificationFormRef.value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (props.editMode) {
|
||||||
|
notificationFormRef.value.notificationEdit(notification, (type: string, value: any) => {
|
||||||
|
if (type === "delete") {
|
||||||
|
pipeline.value.notifications.splice(index, 1);
|
||||||
|
} else if (type === "save") {
|
||||||
|
pipeline.value.notifications[index] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notificationFormRef.value.notificationView(notification, (type: string, value: any) => {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
notificationAdd,
|
||||||
|
notificationEdit,
|
||||||
|
notificationFormRef
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function useActions() {
|
function useActions() {
|
||||||
const saveLoading = ref();
|
const saveLoading = ref();
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
|
@ -484,6 +567,7 @@ export default defineComponent({
|
||||||
historyCancel
|
historyCancel
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const useTaskRet = useTask();
|
const useTaskRet = useTask();
|
||||||
const useStageRet = useStage(useTaskRet);
|
const useStageRet = useStage(useTaskRet);
|
||||||
|
|
||||||
|
@ -495,7 +579,8 @@ export default defineComponent({
|
||||||
...useStageRet,
|
...useStageRet,
|
||||||
...useTrigger(),
|
...useTrigger(),
|
||||||
...useActions(),
|
...useActions(),
|
||||||
...useHistory()
|
...useHistory(),
|
||||||
|
...useNotification()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,9 +8,9 @@ export class PluginManager {
|
||||||
* 初始化plugins
|
* 初始化plugins
|
||||||
* @param plugins
|
* @param plugins
|
||||||
*/
|
*/
|
||||||
init(plugins) {
|
init(plugins: any) {
|
||||||
const list = plugins;
|
const list = plugins;
|
||||||
const map = {};
|
const map: any = {};
|
||||||
for (const plugin of list) {
|
for (const plugin of list) {
|
||||||
map[plugin.key] = plugin;
|
map[plugin.key] = plugin;
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ export class PluginManager {
|
||||||
return this.map[name];
|
return this.map[name];
|
||||||
}
|
}
|
||||||
|
|
||||||
getPreStepOutputOptions({ pipeline, currentStageIndex, currentStepIndex, currentTask }) {
|
getPreStepOutputOptions({ pipeline, currentStageIndex, currentStepIndex, currentTask }: any) {
|
||||||
const steps = this.collectionPreStepOutputs({
|
const steps = this.collectionPreStepOutputs({
|
||||||
pipeline,
|
pipeline,
|
||||||
currentStageIndex,
|
currentStageIndex,
|
||||||
|
@ -42,7 +42,7 @@ export class PluginManager {
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
collectionPreStepOutputs({ pipeline, currentStageIndex, currentStepIndex, currentTask }) {
|
collectionPreStepOutputs({ pipeline, currentStageIndex, currentStepIndex, currentTask }: any) {
|
||||||
const steps: any[] = [];
|
const steps: any[] = [];
|
||||||
// 开始放step
|
// 开始放step
|
||||||
for (let i = 0; i < currentStageIndex; i++) {
|
for (let i = 0; i < currentStageIndex; i++) {
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { request } from "/@/api/service";
|
||||||
|
const apiPrefix = "/basic/email";
|
||||||
|
|
||||||
|
export async function TestSend(receiver: string) {
|
||||||
|
await request({
|
||||||
|
url: apiPrefix + "/test",
|
||||||
|
method: "post",
|
||||||
|
data: {
|
||||||
|
receiver
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { request } from "/@/api/service";
|
||||||
|
const apiPrefix = "/sys/settings";
|
||||||
|
|
||||||
|
export const SettingKeys = {
|
||||||
|
Email: "email"
|
||||||
|
};
|
||||||
|
export async function SettingsGet(key: string) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/get",
|
||||||
|
method: "post",
|
||||||
|
params: {
|
||||||
|
key
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function SettingsSave(key: string, setting: any) {
|
||||||
|
await request({
|
||||||
|
url: apiPrefix + "/save",
|
||||||
|
method: "post",
|
||||||
|
data: {
|
||||||
|
key,
|
||||||
|
setting: JSON.stringify(setting)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
<template>
|
||||||
|
<fs-page class="page-setting-email">
|
||||||
|
<template #header>
|
||||||
|
<div class="title">邮件设置</div>
|
||||||
|
</template>
|
||||||
|
<div class="email-form">
|
||||||
|
<a-form :model="formState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish" @finish-failed="onFinishFailed">
|
||||||
|
<a-form-item label="STMP域名" name="host" :rules="[{ required: true, message: '请输入smtp域名或ip' }]">
|
||||||
|
<a-input v-model:value="formState.host" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="STMP端口" name="port" :rules="[{ required: true, message: '请输入smtp端口号' }]">
|
||||||
|
<a-input v-model:value="formState.port" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="用户名" :name="['auth', 'user']" :rules="[{ required: true, message: '请输入用户名' }]">
|
||||||
|
<a-input v-model:value="formState.auth.user" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="密码" :name="['auth', 'pass']" :rules="[{ required: true, message: '请输入密码' }]">
|
||||||
|
<a-input-password v-model:value="formState.auth.pass" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="发件邮箱" name="sender" :rules="[{ required: true, message: '请输入发件邮箱' }]">
|
||||||
|
<a-input v-model:value="formState.sender" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="是否ssl" name="secure">
|
||||||
|
<a-switch v-model:checked="formState.secure" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="忽略证书校验" name="tls.rejectUnauthorized">
|
||||||
|
<a-switch v-model:checked="formState.tls.rejectUnauthorized" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
|
||||||
|
<a-button type="primary" html-type="submit">保存</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
<div>
|
||||||
|
<a-form :model="testFormState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onTestSend">
|
||||||
|
<a-form-item label="测试收件邮箱" name="receiver" :rules="[{ required: true, message: '请输入测试收件邮箱' }]">
|
||||||
|
<a-input v-model:value="testFormState.receiver" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
|
||||||
|
<a-button type="primary" html-type="submit">测试</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fs-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive } from "vue";
|
||||||
|
import * as api from "./api";
|
||||||
|
import * as emailApi from "./api.email";
|
||||||
|
|
||||||
|
import { SettingKeys } from "./api";
|
||||||
|
import { notification } from "ant-design-vue";
|
||||||
|
|
||||||
|
interface FormState {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
auth: {
|
||||||
|
user: string;
|
||||||
|
pass: string;
|
||||||
|
};
|
||||||
|
secure: boolean; // use TLS
|
||||||
|
tls: {
|
||||||
|
// do not fail on invalid certs
|
||||||
|
rejectUnauthorized?: boolean;
|
||||||
|
};
|
||||||
|
sender: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formState = reactive<Partial<FormState>>({
|
||||||
|
auth: {
|
||||||
|
user: "",
|
||||||
|
pass: ""
|
||||||
|
},
|
||||||
|
tls: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
const data: any = await api.SettingsGet(SettingKeys.Email);
|
||||||
|
const setting = JSON.parse(data.setting);
|
||||||
|
Object.assign(formState, setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
load();
|
||||||
|
|
||||||
|
const onFinish = async (form: any) => {
|
||||||
|
console.log("Success:", form);
|
||||||
|
await api.SettingsSave(SettingKeys.Email, form);
|
||||||
|
notification.success({
|
||||||
|
message: "保存成功"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFinishFailed = (errorInfo: any) => {
|
||||||
|
// console.log("Failed:", errorInfo);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TestFormState {
|
||||||
|
receiver: string;
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
const testFormState = reactive<TestFormState>({
|
||||||
|
receiver: "",
|
||||||
|
loading: false
|
||||||
|
});
|
||||||
|
async function onTestSend() {
|
||||||
|
testFormState.loading = true;
|
||||||
|
try {
|
||||||
|
await emailApi.TestSend(testFormState.receiver);
|
||||||
|
notification.success({
|
||||||
|
message: "发送成功"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
testFormState.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.page-setting-email {
|
||||||
|
.email-form {
|
||||||
|
width: 500px;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,9 @@
|
||||||
|
CREATE TABLE "sys_settings" (
|
||||||
|
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"user_id" integer NOT NULL,
|
||||||
|
"key" varchar(100) NOT NULL,
|
||||||
|
"title" varchar(100) NOT NULL,
|
||||||
|
"setting" varchar(1024),
|
||||||
|
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
|
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
|
@ -9,7 +9,7 @@
|
||||||
"online:preview": "NODE_ENV=preview node ./bootstrap.js",
|
"online:preview": "NODE_ENV=preview node ./bootstrap.js",
|
||||||
"dev": "cross-env NODE_ENV=local midway-bin dev --ts --watchFile='../../core/pipeline/src,../../plugins/'",
|
"dev": "cross-env NODE_ENV=local midway-bin dev --ts --watchFile='../../core/pipeline/src,../../plugins/'",
|
||||||
"dev:preview": "cross-env NODE_ENV=preview midway-bin dev --ts",
|
"dev:preview": "cross-env NODE_ENV=preview midway-bin dev --ts",
|
||||||
"dev:syncdb": "cross-env NODE_ENV=syncdb midway-bin dev --ts --watchFile='../../core/pipeline/src'",
|
"db": "cross-env NODE_ENV=syncdb midway-bin dev --ts",
|
||||||
"test": "midway-bin test --ts",
|
"test": "midway-bin test --ts",
|
||||||
"cov": "midway-bin cov --ts",
|
"cov": "midway-bin cov --ts",
|
||||||
"lint": "mwts check",
|
"lint": "mwts check",
|
||||||
|
@ -54,6 +54,7 @@
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
"midway-flyway-js": "^3.0.0",
|
"midway-flyway-js": "^3.0.0",
|
||||||
"node-cron": "^3.0.2",
|
"node-cron": "^3.0.2",
|
||||||
|
"nodemailer": "^6.9.3",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"sqlite3": "^5.1.4",
|
"sqlite3": "^5.1.4",
|
||||||
"svg-captcha": "^1.4.0",
|
"svg-captcha": "^1.4.0",
|
||||||
|
@ -67,6 +68,7 @@
|
||||||
"@types/jest": "^26.0.24",
|
"@types/jest": "^26.0.24",
|
||||||
"@types/koa": "2.13.4",
|
"@types/koa": "2.13.4",
|
||||||
"@types/node": "^14.18.35",
|
"@types/node": "^14.18.35",
|
||||||
|
"@types/nodemailer": "^6.4.8",
|
||||||
"cross-env": "^6.0.3",
|
"cross-env": "^6.0.3",
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"mwts": "^1.3.0",
|
"mwts": "^1.3.0",
|
||||||
|
|
|
@ -10,7 +10,7 @@ export abstract class BaseController {
|
||||||
* 成功返回
|
* 成功返回
|
||||||
* @param data 返回数据
|
* @param data 返回数据
|
||||||
*/
|
*/
|
||||||
ok(data) {
|
ok(data: any) {
|
||||||
const res = {
|
const res = {
|
||||||
...Constants.res.success,
|
...Constants.res.success,
|
||||||
data: undefined,
|
data: undefined,
|
||||||
|
@ -22,12 +22,21 @@ export abstract class BaseController {
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 失败返回
|
* 失败返回
|
||||||
* @param message
|
* @param msg
|
||||||
|
* @param code
|
||||||
*/
|
*/
|
||||||
fail(msg, code) {
|
fail(msg: string, code: any) {
|
||||||
return {
|
return {
|
||||||
code: code ? code : Constants.res.error.code,
|
code: code ? code : Constants.res.error.code,
|
||||||
msg: msg ? msg : Constants.res.error.code,
|
msg: msg ? msg : Constants.res.error.code,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUserId() {
|
||||||
|
const userId = this.ctx.user?.id;
|
||||||
|
if (userId == null) {
|
||||||
|
throw new Error('Token已过期');
|
||||||
|
}
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -187,14 +187,19 @@ export abstract class BaseService<T> {
|
||||||
return await qb.getMany();
|
return await qb.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkUserId(id = 0, userId, userKey = 'userId') {
|
async checkUserId(
|
||||||
|
id: any = 0,
|
||||||
|
userId,
|
||||||
|
userKey = 'userId',
|
||||||
|
queryIdKey = 'id'
|
||||||
|
) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const res = await this.getRepository().findOne({
|
const res = await this.getRepository().findOne({
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
select: { [userKey]: true },
|
select: { [userKey]: true },
|
||||||
|
// @ts-ignore
|
||||||
where: {
|
where: {
|
||||||
// @ts-ignore
|
[queryIdKey]: id,
|
||||||
id,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
import { Provide } from '@midwayjs/decorator';
|
import { Provide } from '@midwayjs/decorator';
|
||||||
import {
|
import { IWebMiddleware, IMidwayKoaContext, NextFunction } from '@midwayjs/koa';
|
||||||
IWebMiddleware,
|
|
||||||
IMidwayKoaContext,
|
|
||||||
NextFunction,
|
|
||||||
} from '@midwayjs/koa';
|
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import { Result } from '../basic/result';
|
import { Result } from '../basic/result';
|
||||||
|
|
||||||
|
@ -20,7 +16,10 @@ export class GlobalExceptionMiddleware implements IWebMiddleware {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('请求异常:', url, Date.now() - startTime + 'ms', err);
|
logger.error('请求异常:', url, Date.now() - startTime + 'ms', err);
|
||||||
ctx.status = 200;
|
ctx.status = 200;
|
||||||
ctx.body = Result.error(err.code != null ? err.code : 1, err.message);
|
if (err.code == null || typeof err.code !== 'number') {
|
||||||
|
err.code = 1;
|
||||||
|
}
|
||||||
|
ctx.body = Result.error(err.code, err.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { Rule,RuleType } from '@midwayjs/validate';
|
import { Rule, RuleType } from '@midwayjs/validate';
|
||||||
import { ALL, Inject } from '@midwayjs/decorator';
|
import { ALL, Inject } from '@midwayjs/decorator';
|
||||||
import { Body } from '@midwayjs/decorator';
|
import { Body } from '@midwayjs/decorator';
|
||||||
import { Controller, Post, Provide } from '@midwayjs/decorator';
|
import { Controller, Post, Provide } from '@midwayjs/decorator';
|
||||||
import { BaseController } from '../../../basic/base-controller';
|
import { BaseController } from '../../../basic/base-controller';
|
||||||
import { CodeService } from '../service/code-service';
|
import { CodeService } from '../service/code-service';
|
||||||
|
import { EmailService } from '../service/email-service';
|
||||||
export class SmsCodeReq {
|
export class SmsCodeReq {
|
||||||
@Rule(RuleType.number().required())
|
@Rule(RuleType.number().required())
|
||||||
phoneCode: number;
|
phoneCode: number;
|
||||||
|
@ -18,22 +19,17 @@ export class SmsCodeReq {
|
||||||
imgCode: string;
|
imgCode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// const enumsMap = {};
|
|
||||||
// glob('src/modules/**/enums/*.ts', {}, (err, matches) => {
|
|
||||||
// console.log('matched', matches);
|
|
||||||
// for (const filePath of matches) {
|
|
||||||
// const module = require('/' + filePath);
|
|
||||||
// console.log('modules', module);
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
@Provide()
|
@Provide()
|
||||||
@Controller('/api/basic')
|
@Controller('/api/basic/code')
|
||||||
export class BasicController extends BaseController {
|
export class BasicController extends BaseController {
|
||||||
@Inject()
|
@Inject()
|
||||||
codeService: CodeService;
|
codeService: CodeService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
emailService: EmailService;
|
||||||
|
|
||||||
@Post('/sendSmsCode')
|
@Post('/sendSmsCode')
|
||||||
public sendSmsCode(
|
public sendSmsCode(
|
||||||
@Body(ALL)
|
@Body(ALL)
|
||||||
|
@ -53,4 +49,3 @@ export class BasicController extends BaseController {
|
||||||
return this.ok(captcha.data);
|
return this.ok(captcha.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Body, Controller, Inject, Post, Provide } from '@midwayjs/decorator';
|
||||||
|
import { BaseController } from '../../../basic/base-controller';
|
||||||
|
import { EmailService } from '../service/email-service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*/
|
||||||
|
@Provide()
|
||||||
|
@Controller('/api/basic/email')
|
||||||
|
export class EmailController extends BaseController {
|
||||||
|
@Inject()
|
||||||
|
emailService: EmailService;
|
||||||
|
|
||||||
|
@Post('/test')
|
||||||
|
public async test(
|
||||||
|
@Body('receiver')
|
||||||
|
receiver
|
||||||
|
) {
|
||||||
|
const userId = super.getUserId();
|
||||||
|
await this.emailService.test(userId, receiver);
|
||||||
|
return this.ok({});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/decorator';
|
||||||
|
import type { EmailSend } from '@certd/pipeline';
|
||||||
|
import { IEmailService } from '@certd/pipeline';
|
||||||
|
import nodemailer from 'nodemailer';
|
||||||
|
import { SettingsService } from '../../system/service/settings-service';
|
||||||
|
import type SMTPConnection from 'nodemailer/lib/smtp-connection';
|
||||||
|
|
||||||
|
export type EmailConfig = {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
auth: {
|
||||||
|
user: string;
|
||||||
|
pass: string;
|
||||||
|
};
|
||||||
|
secure: boolean; // use TLS
|
||||||
|
tls: {
|
||||||
|
// do not fail on invalid certs
|
||||||
|
rejectUnauthorized: boolean;
|
||||||
|
};
|
||||||
|
sender: string;
|
||||||
|
} & SMTPConnection.Options;
|
||||||
|
@Provide()
|
||||||
|
@Scope(ScopeEnum.Singleton)
|
||||||
|
export class EmailService implements IEmailService {
|
||||||
|
@Inject()
|
||||||
|
settingsService: SettingsService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*/
|
||||||
|
async send(email: EmailSend) {
|
||||||
|
console.log('sendEmail', email);
|
||||||
|
|
||||||
|
const emailConfigEntity = await this.settingsService.getByKey(
|
||||||
|
'email',
|
||||||
|
email.userId
|
||||||
|
);
|
||||||
|
if (emailConfigEntity == null || !emailConfigEntity.setting) {
|
||||||
|
throw new Error('email settings 未设置');
|
||||||
|
}
|
||||||
|
const emailConfig = JSON.parse(emailConfigEntity.setting) as EmailConfig;
|
||||||
|
const transporter = nodemailer.createTransport(emailConfig);
|
||||||
|
const mailOptions = {
|
||||||
|
from: emailConfig.sender,
|
||||||
|
to: email.receivers.join(', '), // list of receivers
|
||||||
|
subject: email.subject,
|
||||||
|
text: email.content,
|
||||||
|
};
|
||||||
|
await transporter.sendMail(mailOptions);
|
||||||
|
console.log('sendEmail success', email);
|
||||||
|
}
|
||||||
|
|
||||||
|
async test(userId: number, receiver: string) {
|
||||||
|
await this.send({
|
||||||
|
userId,
|
||||||
|
receivers: [receiver],
|
||||||
|
subject: '测试邮件,from certd',
|
||||||
|
content: '测试邮件,from certd',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/decorator";
|
import { Autoload, Init, Inject, Scope, ScopeEnum } from '@midwayjs/decorator';
|
||||||
import { PipelineService } from '../service/pipeline-service';
|
import { PipelineService } from '../service/pipeline-service';
|
||||||
import { logger } from '../../../utils/logger';
|
import { logger } from '../../../utils/logger';
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { HistoryEntity } from '../entity/history';
|
||||||
import { HistoryLogEntity } from '../entity/history-log';
|
import { HistoryLogEntity } from '../entity/history-log';
|
||||||
import { HistoryLogService } from './history-log-service';
|
import { HistoryLogService } from './history-log-service';
|
||||||
import { logger } from '../../../utils/logger';
|
import { logger } from '../../../utils/logger';
|
||||||
|
import { EmailService } from '../../basic/service/email-service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 证书申请
|
* 证书申请
|
||||||
|
@ -23,7 +24,8 @@ import { logger } from '../../../utils/logger';
|
||||||
export class PipelineService extends BaseService<PipelineEntity> {
|
export class PipelineService extends BaseService<PipelineEntity> {
|
||||||
@InjectEntityModel(PipelineEntity)
|
@InjectEntityModel(PipelineEntity)
|
||||||
repository: Repository<PipelineEntity>;
|
repository: Repository<PipelineEntity>;
|
||||||
|
@Inject()
|
||||||
|
emailService: EmailService;
|
||||||
@Inject()
|
@Inject()
|
||||||
accessService: AccessService;
|
accessService: AccessService;
|
||||||
@Inject()
|
@Inject()
|
||||||
|
@ -191,6 +193,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||||
onChanged,
|
onChanged,
|
||||||
accessService: this.accessService,
|
accessService: this.accessService,
|
||||||
storage: new DbStorage(userId, this.storageService),
|
storage: new DbStorage(userId, this.storageService),
|
||||||
|
emailService: this.emailService,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await executor.init();
|
await executor.init();
|
||||||
|
|
|
@ -1,6 +1,15 @@
|
||||||
import { ALL, Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/decorator";
|
import {
|
||||||
import { CrudController } from "../../../basic/crud-controller";
|
ALL,
|
||||||
import { SettingsService } from "../service/settings-service";
|
Body,
|
||||||
|
Controller,
|
||||||
|
Inject,
|
||||||
|
Post,
|
||||||
|
Provide,
|
||||||
|
Query,
|
||||||
|
} from '@midwayjs/decorator';
|
||||||
|
import { CrudController } from '../../../basic/crud-controller';
|
||||||
|
import { SettingsService } from '../service/settings-service';
|
||||||
|
import { SettingsEntity } from '../entity/settings';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
|
@ -50,4 +59,18 @@ export class SettingsController extends CrudController<SettingsService> {
|
||||||
return super.delete(id);
|
return super.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('/save')
|
||||||
|
async save(@Body(ALL) bean: SettingsEntity) {
|
||||||
|
await this.service.checkUserId(bean.key, this.ctx.user.id, 'userId', 'key');
|
||||||
|
bean.userId = this.ctx.user.id;
|
||||||
|
await this.service.save(bean);
|
||||||
|
return this.ok({});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/get')
|
||||||
|
async get(@Query('key') key: string) {
|
||||||
|
await this.service.checkUserId(key, this.ctx.user.id, 'userId', 'key');
|
||||||
|
const entity = await this.service.getByKey(key, this.ctx.user.id);
|
||||||
|
return this.ok(entity);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,10 @@ export class SettingsEntity {
|
||||||
id: number;
|
id: number;
|
||||||
@Column({ name: 'user_id', comment: '用户id' })
|
@Column({ name: 'user_id', comment: '用户id' })
|
||||||
userId: number;
|
userId: number;
|
||||||
|
@Column({ comment: 'key', length: 100 })
|
||||||
|
key: string;
|
||||||
@Column({ comment: '名称', length: 100 })
|
@Column({ comment: '名称', length: 100 })
|
||||||
name: string;
|
title: string;
|
||||||
@Column({ name: 'setting', comment: '设置', length: 1024, nullable: true })
|
@Column({ name: 'setting', comment: '设置', length: 1024, nullable: true })
|
||||||
setting: string;
|
setting: string;
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,15 @@
|
||||||
import { Provide, Scope, ScopeEnum } from "@midwayjs/decorator";
|
import { Provide, Scope, ScopeEnum } from '@midwayjs/decorator';
|
||||||
import { InjectEntityModel } from "@midwayjs/typeorm";
|
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||||
import { Repository } from "typeorm";
|
import { Repository } from 'typeorm';
|
||||||
import { BaseService } from "../../../basic/base-service";
|
import { BaseService } from '../../../basic/base-service';
|
||||||
import { SettingsEntity } from "../entity/settings";
|
import { SettingsEntity } from '../entity/settings';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 授权
|
* 授权
|
||||||
*/
|
*/
|
||||||
@Provide()
|
@Provide()
|
||||||
@Scope(ScopeEnum.Singleton)
|
@Scope(ScopeEnum.Singleton)
|
||||||
export class SettingsService
|
export class SettingsService extends BaseService<SettingsEntity> {
|
||||||
extends BaseService<SettingsEntity>
|
|
||||||
{
|
|
||||||
@InjectEntityModel(SettingsEntity)
|
@InjectEntityModel(SettingsEntity)
|
||||||
repository: Repository<SettingsEntity>;
|
repository: Repository<SettingsEntity>;
|
||||||
|
|
||||||
|
@ -19,8 +17,11 @@ export class SettingsService
|
||||||
return this.repository;
|
return this.repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getById(id: any): Promise<any> {
|
async getById(id: any): Promise<SettingsEntity | null> {
|
||||||
const entity = await this.info(id);
|
const entity = await this.info(id);
|
||||||
|
if (!entity) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
// const access = accessRegistry.get(entity.type);
|
// const access = accessRegistry.get(entity.type);
|
||||||
const setting = JSON.parse(entity.setting);
|
const setting = JSON.parse(entity.setting);
|
||||||
return {
|
return {
|
||||||
|
@ -29,5 +30,38 @@ export class SettingsService
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getByKey(key: string, userId: number): Promise<SettingsEntity | null> {
|
||||||
|
if (!key || !userId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await this.repository.findOne({
|
||||||
|
where: {
|
||||||
|
key,
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSettingByKey(key: string, userId: number): Promise<any | null> {
|
||||||
|
const entity = await this.getByKey(key, userId);
|
||||||
|
if (!entity) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return JSON.parse(entity.setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(bean: SettingsEntity) {
|
||||||
|
const entity = await this.repository.findOne({
|
||||||
|
where: {
|
||||||
|
key: bean.key,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (entity) {
|
||||||
|
entity.setting = bean.setting;
|
||||||
|
await this.repository.save(entity);
|
||||||
|
} else {
|
||||||
|
bean.title = bean.key;
|
||||||
|
await this.repository.save(bean);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue