feat: 邮件通知

pull/21/merge
xiaojunnuo 2023-06-25 15:30:18 +08:00
parent 64afebecd4
commit 937e3fac19
32 changed files with 790 additions and 88 deletions

View File

@ -2,9 +2,9 @@
"name": "@certd/pipeline",
"private": false,
"version": "1.0.6",
"main": "./dist/bundle.js",
"module": "./dist/pipeline.mjs",
"types": "./dist/d/index.d.ts",
"main": "./src",
"module": "./src",
"types": "./src",
"publishConfig": {
"main": "./dist/bundle.js",
"module": "./dist/bundle.mjs",
@ -19,6 +19,7 @@
"dependencies": {
"axios": "^1.4.0",
"node-forge": "^1.3.1",
"nodemailer": "^6.9.3",
"qs": "^6.11.2"
},
"devDependencies": {

View File

@ -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 { RunHistory, RunnableCollection } from "./run-history";
import { AbstractTaskPlugin, PluginDefine, pluginRegistry } from "../plugin";
@ -216,13 +216,13 @@ export class Executor {
let subject = "";
let content = "";
if (when === "start") {
subject = `【CertD】${this.pipeline.title} 开始执行,buildId:${this.runtime.id}`;
content = `【CertD】${this.pipeline.title} 开始执行buildId:${this.runtime.id}`;
subject = `【CertD】开始执行,${this.pipeline.title}, buildId:${this.runtime.id}`;
content = subject;
} else if (when === "success") {
subject = `【CertD】${this.pipeline.title} 执行成功,buildId:${this.runtime.id}`;
content = `【CertD】${this.pipeline.title} 执行成功buildId:${this.runtime.id}`;
subject = `【CertD】执行成功,${this.pipeline.title}, buildId:${this.runtime.id}`;
content = subject;
} 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>`;
} else {
return;
@ -234,6 +234,7 @@ export class Executor {
}
if (notification.type === "email") {
this.options.emailService?.send({
userId: this.pipeline.userId,
subject,
content,
receivers: notification.options.receivers,

View File

@ -0,0 +1,10 @@
export type EmailSend = {
userId: number;
subject: string;
content: string;
receivers: string[];
};
export interface IEmailService {
send(email: EmailSend): Promise<void>;
}

View File

@ -1,9 +1 @@
export type EmailSend = {
subject: string;
content: string;
receivers: string[];
};
export interface IEmailService {
send(email: EmailSend): Promise<void>;
}
export * from "./email";

View File

@ -1,3 +1,4 @@
VITE_APP_API=/api
#登录与权限关闭
VITE_APP_PM_ENABLED=true
VITE_APP_TITLE=Certd

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" href="/logo.svg" />
<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" />
</head>
<body>

View File

@ -106,8 +106,8 @@ function createService() {
* @description
* @param {Object} service axios
*/
function createRequestFunction(service) {
return function (config) {
function createRequestFunction(service: any) {
return function (config: any) {
const configDefault = {
headers: {
"Content-Type": get(config, "headers.Content-Type", "application/json")

View File

@ -48,7 +48,10 @@ export function responseError(data = {}, msg = "请求失败", code = 500) {
* @description
* @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);
// 显示提示
@ -59,8 +62,6 @@ export function errorLog(error) {
* @description
* @param {String} msg
*/
export function errorCreate(msg) {
const error = new Error(msg);
errorLog(error);
throw error;
export function errorCreate(msg: string) {
throw new Error(msg);
}

View File

@ -35,6 +35,28 @@ export const certdResources = [
meta: {
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
}
}
]
}
]
}

View File

@ -1,15 +1,16 @@
// @ts-ignore
import _ from "lodash";
export function getEnvValue(key) {
export function getEnvValue(key: string) {
// @ts-ignore
return import.meta.env["VITE_APP_" + key];
}
export class EnvConfig {
API;
MODE;
STORAGE;
TITLE;
PM_ENABLED;
API: string;
MODE: string;
STORAGE: string;
TITLE: string;
PM_ENABLED: string;
constructor() {
this.init();
}
@ -19,6 +20,7 @@ export class EnvConfig {
_.forEach(import.meta.env, (value, key) => {
if (key.startsWith("VITE_APP")) {
key = key.replace("VITE_APP_", "");
//@ts-ignore
this[key] = value;
}
});
@ -26,7 +28,8 @@ export class EnvConfig {
this.MODE = import.meta.env.MODE;
}
get(key, defaultValue) {
get(key: string, defaultValue: string) {
//@ts-ignore
return this[key] ?? defaultValue;
}
isDev() {

View File

@ -2,9 +2,9 @@ import { env } from "./util.env";
export const site = {
/**
* @description
* @param {String} title
* @param titleText
*/
title: function (titleText) {
title: function (titleText: string) {
const processTitle = env.TITLE || "FsAdmin";
window.document.title = `${processTitle}${titleText ? ` | ${titleText}` : ""}`;
}

View File

@ -3,7 +3,7 @@ import { RunHistory } from "/@/views/certd/pipeline/pipeline/type";
const apiPrefix = "/pi/history";
export async function GetList(query) {
export async function GetList(query: any) {
const list = await request({
url: apiPrefix + "/list",
method: "post",
@ -18,7 +18,7 @@ export async function GetList(query) {
return list;
}
export async function GetDetail(query): Promise<RunHistory> {
export async function GetDetail(query: any): Promise<RunHistory> {
const detail = await request({
url: apiPrefix + "/detail",
method: "post",

View File

@ -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>

View File

@ -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>

View File

@ -2,7 +2,6 @@
<fs-page v-if="pipeline" class="page-pipeline-edit">
<template #header>
<div class="title">
<fs-button icon="ion:left" @click="goBack" />
<pi-editable v-model="pipeline.title" :hover-show="false" :disabled="!editMode"></pi-editable>
</div>
<div class="more">
@ -63,7 +62,7 @@
</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">
<pi-editable v-model="stage.title" :disabled="!editMode"></pi-editable>
</div>
@ -118,6 +117,48 @@
</a-button>
</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>
@ -140,6 +181,7 @@
<pi-task-form ref="taskFormRef" :edit-mode="editMode"></pi-task-form>
<pi-trigger-form ref="triggerFormRef" :edit-mode="editMode"></pi-trigger-form>
<pi-task-view ref="taskViewRef"></pi-task-view>
<PiNotificationForm ref="notificationFormRef" :edit-mode="editMode"></PiNotificationForm>
</fs-page>
</template>
@ -148,9 +190,10 @@ import { defineComponent, ref, provide, Ref, watch } from "vue";
import { useRouter } from "vue-router";
import PiTaskForm from "./component/task-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 PiStatusShow from "./component/status-show.vue";
import _ from "lodash-es";
import _ from "lodash";
import { message, Modal, notification } from "ant-design-vue";
import { pluginManager } from "/@/views/certd/pipeline/pipeline/plugin";
import { nanoid } from "nanoid";
@ -159,7 +202,7 @@ import PiHistoryTimelineItem from "/@/views/certd/pipeline/pipeline/component/hi
export default defineComponent({
name: "PipelineEdit",
// eslint-disable-next-line vue/no-unused-components
components: { PiHistoryTimelineItem, PiTaskForm, PiTriggerForm, PiTaskView, PiStatusShow },
components: { PiHistoryTimelineItem, PiTaskForm, PiTriggerForm, PiTaskView, PiStatusShow, PiNotificationForm },
props: {
pipelineId: {
type: [Number, String],
@ -269,7 +312,7 @@ export default defineComponent({
return;
}
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;
await loadHistoryList(true);
},
@ -369,8 +412,13 @@ export default defineComponent({
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 {
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() {
const saveLoading = ref();
const run = async () => {
@ -484,6 +567,7 @@ export default defineComponent({
historyCancel
};
}
const useTaskRet = useTask();
const useStageRet = useStage(useTaskRet);
@ -495,7 +579,8 @@ export default defineComponent({
...useStageRet,
...useTrigger(),
...useActions(),
...useHistory()
...useHistory(),
...useNotification()
};
}
});

View File

@ -8,9 +8,9 @@ export class PluginManager {
* plugins
* @param plugins
*/
init(plugins) {
init(plugins: any) {
const list = plugins;
const map = {};
const map: any = {};
for (const plugin of list) {
map[plugin.key] = plugin;
}
@ -21,7 +21,7 @@ export class PluginManager {
return this.map[name];
}
getPreStepOutputOptions({ pipeline, currentStageIndex, currentStepIndex, currentTask }) {
getPreStepOutputOptions({ pipeline, currentStageIndex, currentStepIndex, currentTask }: any) {
const steps = this.collectionPreStepOutputs({
pipeline,
currentStageIndex,
@ -42,7 +42,7 @@ export class PluginManager {
return options;
}
collectionPreStepOutputs({ pipeline, currentStageIndex, currentStepIndex, currentTask }) {
collectionPreStepOutputs({ pipeline, currentStageIndex, currentStepIndex, currentTask }: any) {
const steps: any[] = [];
// 开始放step
for (let i = 0; i < currentStageIndex; i++) {

View File

@ -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
}
});
}

View File

@ -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)
}
});
}

View File

@ -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>

View File

@ -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)
);

View File

@ -9,7 +9,7 @@
"online:preview": "NODE_ENV=preview node ./bootstrap.js",
"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: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",
"cov": "midway-bin cov --ts",
"lint": "mwts check",
@ -54,6 +54,7 @@
"md5": "^2.3.0",
"midway-flyway-js": "^3.0.0",
"node-cron": "^3.0.2",
"nodemailer": "^6.9.3",
"reflect-metadata": "^0.1.13",
"sqlite3": "^5.1.4",
"svg-captcha": "^1.4.0",
@ -67,6 +68,7 @@
"@types/jest": "^26.0.24",
"@types/koa": "2.13.4",
"@types/node": "^14.18.35",
"@types/nodemailer": "^6.4.8",
"cross-env": "^6.0.3",
"jest": "^26.6.3",
"mwts": "^1.3.0",

View File

@ -10,7 +10,7 @@ export abstract class BaseController {
*
* @param data
*/
ok(data) {
ok(data: any) {
const res = {
...Constants.res.success,
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 {
code: code ? code : 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;
}
}

View File

@ -187,14 +187,19 @@ export abstract class BaseService<T> {
return await qb.getMany();
}
async checkUserId(id = 0, userId, userKey = 'userId') {
async checkUserId(
id: any = 0,
userId,
userKey = 'userId',
queryIdKey = 'id'
) {
// @ts-ignore
const res = await this.getRepository().findOne({
// @ts-ignore
select: { [userKey]: true },
// @ts-ignore
where: {
// @ts-ignore
id,
[queryIdKey]: id,
},
});
// @ts-ignore

View File

@ -1,9 +1,5 @@
import { Provide } from '@midwayjs/decorator';
import {
IWebMiddleware,
IMidwayKoaContext,
NextFunction,
} from '@midwayjs/koa';
import { IWebMiddleware, IMidwayKoaContext, NextFunction } from '@midwayjs/koa';
import { logger } from '../utils/logger';
import { Result } from '../basic/result';
@ -20,7 +16,10 @@ export class GlobalExceptionMiddleware implements IWebMiddleware {
} catch (err) {
logger.error('请求异常:', url, Date.now() - startTime + 'ms', err);
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);
}
};
}

View File

@ -1,9 +1,10 @@
import { Rule,RuleType } from '@midwayjs/validate';
import { Rule, RuleType } from '@midwayjs/validate';
import { ALL, Inject } from '@midwayjs/decorator';
import { Body } from '@midwayjs/decorator';
import { Controller, Post, Provide } from '@midwayjs/decorator';
import { BaseController } from '../../../basic/base-controller';
import { CodeService } from '../service/code-service';
import { EmailService } from '../service/email-service';
export class SmsCodeReq {
@Rule(RuleType.number().required())
phoneCode: number;
@ -18,22 +19,17 @@ export class SmsCodeReq {
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()
@Controller('/api/basic')
@Controller('/api/basic/code')
export class BasicController extends BaseController {
@Inject()
codeService: CodeService;
@Inject()
emailService: EmailService;
@Post('/sendSmsCode')
public sendSmsCode(
@Body(ALL)
@ -53,4 +49,3 @@ export class BasicController extends BaseController {
return this.ok(captcha.data);
}
}

View File

@ -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({});
}
}

View File

@ -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',
});
}
}

View File

@ -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 { logger } from '../../../utils/logger';

View File

@ -14,6 +14,7 @@ import { HistoryEntity } from '../entity/history';
import { HistoryLogEntity } from '../entity/history-log';
import { HistoryLogService } from './history-log-service';
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> {
@InjectEntityModel(PipelineEntity)
repository: Repository<PipelineEntity>;
@Inject()
emailService: EmailService;
@Inject()
accessService: AccessService;
@Inject()
@ -191,6 +193,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
onChanged,
accessService: this.accessService,
storage: new DbStorage(userId, this.storageService),
emailService: this.emailService,
});
try {
await executor.init();

View File

@ -1,6 +1,15 @@
import { ALL, Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/decorator";
import { CrudController } from "../../../basic/crud-controller";
import { SettingsService } from "../service/settings-service";
import {
ALL,
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);
}
@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);
}
}

View File

@ -8,8 +8,10 @@ export class SettingsEntity {
id: number;
@Column({ name: 'user_id', comment: '用户id' })
userId: number;
@Column({ comment: 'key', length: 100 })
key: string;
@Column({ comment: '名称', length: 100 })
name: string;
title: string;
@Column({ name: 'setting', comment: '设置', length: 1024, nullable: true })
setting: string;

View File

@ -1,17 +1,15 @@
import { Provide, Scope, ScopeEnum } from "@midwayjs/decorator";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { Repository } from "typeorm";
import { BaseService } from "../../../basic/base-service";
import { SettingsEntity } from "../entity/settings";
import { Provide, Scope, ScopeEnum } from '@midwayjs/decorator';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { BaseService } from '../../../basic/base-service';
import { SettingsEntity } from '../entity/settings';
/**
*
*/
@Provide()
@Scope(ScopeEnum.Singleton)
export class SettingsService
extends BaseService<SettingsEntity>
{
export class SettingsService extends BaseService<SettingsEntity> {
@InjectEntityModel(SettingsEntity)
repository: Repository<SettingsEntity>;
@ -19,8 +17,11 @@ export class SettingsService
return this.repository;
}
async getById(id: any): Promise<any> {
async getById(id: any): Promise<SettingsEntity | null> {
const entity = await this.info(id);
if (!entity) {
return null;
}
// const access = accessRegistry.get(entity.type);
const setting = JSON.parse(entity.setting);
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);
}
}
}