Merge branch 'v2-dev' into v2-dev-buy

This commit is contained in:
xiaojunnuo
2025-11-04 23:04:11 +08:00
225 changed files with 5865 additions and 1654 deletions

View File

@@ -10,6 +10,7 @@ RUN cp /workspace/certd-client/dist/* /workspace/certd-server/public/ -rf
RUN cd /workspace/certd-server && pnpm install && npm run build-on-docker
FROM node:22-alpine
EXPOSE 7001
EXPOSE 7002

View File

@@ -0,0 +1,2 @@
#登录与权限开启
VITE_APP_PM_ENABLED=false

View File

@@ -3,6 +3,58 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.37.4](https://github.com/certd/certd/compare/v1.37.3...v1.37.4) (2025-10-28)
### Bug Fixes
* 修复站点证书监控复制按钮无效的bug ([efa26a0](https://github.com/certd/certd/commit/efa26a067f06402f30befc016d9934cadcd5a563))
## [1.37.3](https://github.com/certd/certd/compare/v1.37.2...v1.37.3) (2025-10-24)
### Performance Improvements
* 注册页面增加手机注册tab页签 ([6b2f1fc](https://github.com/certd/certd/commit/6b2f1fcd3e058061b814c3331cda8ce1b2d80d73))
* 流水线创建时支持添加到证书监控 ([59ba408](https://github.com/certd/certd/commit/59ba4080706548828ef1c0a9cd893c1c9a7d591f))
* 流水线支持有效期设置 ([911e69e](https://github.com/certd/certd/commit/911e69e3bc0cdd48b62953b5d0981d640fc1f8ac))
* 站点证书监控增加导出和分组功能 ([2ed12c4](https://github.com/certd/certd/commit/2ed12c429eb58274a4f9dd0ed3b66e160d283ded))
* 证书监控增加批量删除 ([e578c52](https://github.com/certd/certd/commit/e578c52fdf2f838038062aa4209b655fbae461fb))
## [1.37.2](https://github.com/certd/certd/compare/v1.37.1...v1.37.2) (2025-10-14)
### Performance Improvements
* 证书监控支持设置证书即将过期天数 ([cd35568](https://github.com/certd/certd/commit/cd35568e042e6ab928685efad51cdbed823d2d4f))
* 支持网络测试 ([2bef608](https://github.com/certd/certd/commit/2bef608e07ceb56d52007f290667e0afef401b22))
## [1.37.1](https://github.com/certd/certd/compare/v1.37.0...v1.37.1) (2025-09-29)
### Bug Fixes
* 修复版本比较bug ([109696e](https://github.com/certd/certd/commit/109696e965d68c50c8627ffd40203edd1d2daea5))
# [1.37.0](https://github.com/certd/certd/compare/v1.36.25...v1.37.0) (2025-09-28)
**Note:** Version bump only for package @certd/ui-client
## [1.36.25](https://github.com/certd/certd/compare/v1.36.24...v1.36.25) (2025-09-27)
**Note:** Version bump only for package @certd/ui-client
## [1.36.24](https://github.com/certd/certd/compare/v1.36.23...v1.36.24) (2025-09-27)
**Note:** Version bump only for package @certd/ui-client
## [1.36.23](https://github.com/certd/certd/compare/v1.36.22...v1.36.23) (2025-09-26)
### Bug Fixes
* 授权页面id列位置不在第一列的bug ([3f1722d](https://github.com/certd/certd/commit/3f1722d54debcb4849dc14521a2da0d9b304b69f))
### Performance Improvements
* 动态加载验证码script ([dcc396a](https://github.com/certd/certd/commit/dcc396afb7a23aeb8af57c01014b09af5f033e61))
* 验证码支持测试,登录验证码需要测试通过后才能开启 ([83e6476](https://github.com/certd/certd/commit/83e6476408090b741fabb1b542fb458d9a8b4134))
## [1.36.22](https://github.com/certd/certd/compare/v1.36.21...v1.36.22) (2025-09-23)
### Bug Fixes

View File

@@ -23,6 +23,7 @@
</div>
<script type="module" src="/src/main.ts"></script>
<script src="https://static.geetest.com/v4/gt4.js"></script>
<!--<script src="https://static.geetest.com/v4/gt4.js"></script>-->
<!--<script src="https://turing.captcha.qcloud.com/TJCaptcha.js"></script>-->
</body>
</html>

View File

@@ -1,11 +1,12 @@
{
"name": "@certd/ui-client",
"version": "1.36.22",
"version": "1.37.4",
"private": true,
"scripts": {
"dev": "vite --open",
"dev:pm": "vite --mode pm",
"dev:force": "vite --force",
"remote": "vite --mode remote --open",
"debug": "vite --mode debug --open",
"debug:pm": "vite --mode debugpm",
"debug:force": "vite --force --mode debug",
@@ -32,11 +33,11 @@
"@aws-sdk/s3-request-presigner": "^3.535.0",
"@certd/vue-js-cron-light": "^4.0.14",
"@ctrl/tinycolor": "^4.1.0",
"@fast-crud/editor-code": "^1.26.6",
"@fast-crud/fast-crud": "^1.26.6",
"@fast-crud/fast-extends": "^1.26.6",
"@fast-crud/ui-antdv4": "^1.26.6",
"@fast-crud/ui-interface": "^1.26.6",
"@fast-crud/editor-code": "^1.27.4",
"@fast-crud/fast-crud": "^1.27.4",
"@fast-crud/fast-extends": "^1.27.4",
"@fast-crud/ui-antdv4": "^1.27.4",
"@fast-crud/ui-interface": "^1.27.4",
"@iconify/tailwind": "^1.2.0",
"@iconify/vue": "^4.1.1",
"@manypkg/get-packages": "^2.2.2",
@@ -97,6 +98,7 @@
"vue-cropperjs": "^5.0.0",
"vue-echarts": "^7.0.3",
"vue-i18n": "^9.10.2",
"vue-plugin-load-script": "2.1.1",
"vue-router": "^4.3.0",
"vuedraggable": "^4.1.0",
"watermark-js-plus": "^1.5.8",
@@ -104,8 +106,8 @@
"zod-defaults": "^0.1.3"
},
"devDependencies": {
"@certd/lib-iframe": "^1.36.22",
"@certd/pipeline": "^1.36.22",
"@certd/lib-iframe": "^1.37.4",
"@certd/pipeline": "^1.37.4",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",

View File

@@ -12,6 +12,14 @@ const props = defineProps({
type: Object,
default: () => ({}),
},
type: {
type: String,
default: "image",
},
addonId: {
type: Number,
default: 0,
},
});
const captchaRef = ref(null);
const settingStore = useSettingStore();
@@ -23,7 +31,7 @@ const captchaAddonId = computed(() => {
return settingStore.sysPublic.captchaAddonId ?? 0;
});
const captchaComponent = computed(() => {
let type: any = "image";
let type: any = props.type ?? "image";
if (settingStore.sysPublic.captchaAddonId && settingStore.sysPublic.captchaType) {
type = settingStore.sysPublic.captchaType;
}
@@ -48,10 +56,10 @@ function onChange(data: any) {
}
async function getCaptchaForm() {
return await captchaRef.value.getCaptchaForm();
return await captchaRef.value?.getCaptchaForm();
}
async function reset() {
await captchaRef.value.reset();
await captchaRef.value?.reset();
}
defineExpose({
getCaptchaForm,

View File

@@ -7,6 +7,14 @@ import { useSettingStore } from "/@/store/settings";
import { request } from "/src/api/service";
import { notification } from "ant-design-vue";
import { loadScript } from "vue-plugin-load-script";
const loaded = ref(false);
async function loadCaptchaScript() {
// 加载验证码js
await loadScript("https://static.geetest.com/v4/gt4.js");
loaded.value = true;
}
defineOptions({
name: "GeetestCaptcha",
});
@@ -16,15 +24,10 @@ const props = defineProps<{
captchaGet: () => Promise<any>;
}>();
const captchaRef = ref(null);
// const addonApi = createAddonApi();
const settingStore = useSettingStore();
const captchaInstanceRef: Ref = ref({});
async function init() {
// if (!initGeetest4) {
// await import("https://static.geetest.com/v4/gt4.js");
// }
await loadCaptchaScript();
const { captchaId } = await props.captchaGet();
// @ts-ignore
initGeetest4(

View File

@@ -0,0 +1,233 @@
<template>
<div ref="captchaRef" class="tencent_captcha_wrapper" :class="{ tencent_captcha_ok: modelValue }" @click="triggerCaptcha">
<div class="validation-box" :class="{ validated: modelValue != null }">
<div class="sweep-animation"></div>
<div class="box-content">
<div class="box-icon"></div>
<span v-if="modelValue == null" class="status-text">点击进行验证</span>
<span v-else class="status-text">验证成功</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, defineProps, defineEmits, ref, onUnmounted, Ref, watch } from "vue";
import { notification } from "ant-design-vue";
import { loadScript } from "vue-plugin-load-script";
const loaded = ref(false);
async function loadCaptchaScript() {
// 加载验证码js
// var appid = "您的CaptchaAppId";
// loadScript("https://turing.captcha.qq.com/TJCaptcha.js?appid=" + appid);
await loadScript("https://turing.captcha.qcloud.com/TJCaptcha.js");
loaded.value = true;
}
loadCaptchaScript();
defineOptions({
name: "TencentCaptcha",
});
const emit = defineEmits(["update:modelValue", "change"]);
const props = defineProps<{
modelValue: any;
captchaGet: () => Promise<any>;
}>();
const captchaRef = ref(null);
const captchaInstanceRef: Ref = ref({});
// 定义回调函数
function callback(res: { ret: number; ticket: string; randstr: string; errorCode?: number; errorMessage?: string }) {
// 第一个参数传入回调结果,结果如下:
// ret Int 验证结果0验证成功。2用户主动关闭验证码。
// ticket String 验证成功的票据,当且仅当 ret = 0 时 ticket 有值。
// CaptchaAppId String 验证码应用ID。
// bizState Any 自定义透传参数。
// randstr String 本次验证的随机串,后续票据校验时需传递该参数。
// verifyDuration Int 验证码校验接口耗时ms
// actionDuration Int 操作校验成功耗时(用户动作+校验完成)(ms)。
// sid String 链路sid。
console.log("callback:", res);
// res用户主动关闭验证码= {ret: 2, ticket: null}
// res验证成功 = {ret: 0, ticket: "String", randstr: "String"}
// res请求验证码发生错误验证码自动返回trerror_前缀的容灾票据 = {ret: 0, ticket: "String", randstr: "String", errorCode: Number, errorMessage: "String"}
// 此处代码仅为验证结果的展示示例真实业务接入建议基于ticket和errorCode情况做不同的业务处理
if (res.errorCode && res.errorCode > 0) {
notification.error({
message: `验证码验证失败:${res.errorMessage || res.errorCode}`,
});
}
if (res.ret === 0) {
emitChange({
ticket: res.ticket,
randstr: res.randstr,
});
} else if (res.ret === 2) {
console.log("用户主动关闭验证码");
}
}
// 定义验证码js加载错误处理函数
function loadErrorCallback(error: any) {
// var appid = "您的CaptchaAppId";
// // 生成容灾票据或自行做其它处理
// var ticket = "trerror_1001_" + appid + "_" + Math.floor(new Date().getTime() / 1000);
// callback({
// ret: 0,
// randstr: "@" + Math.random().toString(36).substr(2),
// ticket: ticket,
// errorCode: 1001,
// errorMessage: "jsload_error",
// });
notification.error({
message: `验证码加载失败:${error?.message || error}`,
});
}
async function triggerCaptcha() {
if (!loaded.value) {
notification.error({
message: "验证码还未加载完成,请稍后再试",
});
return;
}
const { captchaAppId } = await props.captchaGet();
try {
// 生成一个验证码对象
// CaptchaAppId登录验证码控制台从【验证管理】页面进行查看。如果未创建过验证请先新建验证。注意不可使用客户端类型为小程序的CaptchaAppId会导致数据统计错误。
//callback定义的回调函数
// @ts-ignore
var captcha = new TencentCaptcha(captchaAppId + "", callback, {
userLanguage: "zh-cn",
// showFn: (ret: any) => {
// const {
// duration, // 验证码渲染完成的耗时(ms)
// sid, // 链路sid
// } = ret;
// },
});
// 调用方法,显示验证码
captcha.show();
} catch (error) {
// 加载异常调用验证码js加载错误处理函数
loadErrorCallback(error);
}
}
function emitChange(value: any) {
emit("update:modelValue", value);
emit("change", value);
}
function reset() {
captchaInstanceRef.value?.instance?.reset();
}
watch(
() => {
return props.modelValue;
},
value => {
if (value == null) {
reset();
}
}
);
defineExpose({
reset,
});
</script>
<style lang="less">
.tencent_captcha_wrapper {
.validation-box {
width: 100%;
height: 40px;
margin: 0 auto 30px;
border: 1px solid #ddd;
border-radius: 8px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
background-color: #f9f9f9;
}
.validation-box:hover {
border-color: #aaa;
background-color: #f0f0f0;
}
.validation-box.validated {
border-color: #4caf50;
background-color: #f1f8e9;
}
.box-content {
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
position: relative;
}
.box-icon {
font-size: 18px;
color: #bbb;
margin-right: 15px;
transition: all 0.3s ease;
}
.validation-box.validated .box-icon {
color: #4caf50;
}
.status-text {
font-size: 14px;
font-weight: 500;
color: #888;
transition: all 0.3s ease;
}
.validation-box.validated .status-text {
color: #4caf50;
font-weight: 600;
}
/* 划过动画效果 */
.sweep-animation {
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(76, 175, 80, 0.2), transparent);
z-index: 1;
opacity: 0;
transition: opacity 0.3s;
}
.validation-box.validated .sweep-animation {
animation: sweep 0.8s ease forwards;
opacity: 1;
}
@keyframes sweep {
0% {
left: -100%;
}
50% {
left: 0;
}
100% {
left: 100%;
}
}
}
</style>

View File

@@ -45,6 +45,16 @@ export async function DoVerify(id: number) {
});
}
export async function ResetStatus(id: number) {
return await request({
url: apiPrefix + "/resetStatus",
method: "post",
data: {
id,
},
});
}
export async function ParseDomain(fullDomain: string) {
return await request({
url: subDomainApiPrefix + "/parseDomain",

View File

@@ -16,6 +16,9 @@
<a-tooltip v-if="cnameRecord.error" :title="cnameRecord.error">
<fs-icon class="ml-5 color-red" icon="ion:warning-outline"></fs-icon>
</a-tooltip>
<a-tooltip v-if="cnameRecord.status === 'valid'" title="重置校验状态,重新校验">
<fs-icon class="ml-2 color-yellow text-md pointer" icon="solar:undo-left-square-bold" @click="resetStatus"></fs-icon>
</a-tooltip>
</td>
<td class="center">
<template v-if="cnameRecord.status !== 'valid'">
@@ -35,6 +38,7 @@ import { ref, watch } from "vue";
import { dict } from "@fast-crud/fast-crud";
import * as api from "./api.js";
import CnameTip from "./cname-tip.vue";
import { Modal } from "ant-design-vue";
const statusDict = dict({
data: [
{ label: "待设置CNAME", value: "cname", color: "warning" },
@@ -71,12 +75,15 @@ function onRecordChange() {
});
}
async function loadRecord() {
cnameRecord.value = await GetByDomain(props.domain);
}
let refreshIntervalId: any = null;
async function doRefresh() {
if (!props.domain) {
return;
}
cnameRecord.value = await GetByDomain(props.domain);
await loadRecord();
onRecordChange();
if (cnameRecord.value.status === "validating") {
@@ -114,6 +121,17 @@ async function doVerify() {
}
await doRefresh();
}
async function resetStatus() {
Modal.confirm({
title: "重置状态",
content: "确定要重置校验状态吗?",
onOk: async () => {
await api.ResetStatus(cnameRecord.value.id);
await loadRecord();
},
});
}
</script>
<style lang="less">

View File

@@ -57,6 +57,7 @@ export default {
suiteBuy: "Suite Purchase",
myTrade: "My Orders",
paymentReturn: "Payment Return",
hasExpired: "Expired",
user: {
greeting: "Hello",
profile: "Account Info",
@@ -136,10 +137,16 @@ export default {
triggerType: "Trigger Type",
pipelineId: "Pipeline Id",
},
pi: {
validTime: "Piepline Valid Time",
validTimeHelper: "Not filled in means permanent validity",
},
types: {
certApply: "Certificate Application",
certUpload: "Certificate Upload",
certApply: "Cert Apply",
certUpload: "Cert Upload",
custom: "Custom",
template: "Template",
},
myPipelines: "My Pipelines",
selectedCount: "Selected {count} items",
@@ -175,6 +182,7 @@ export default {
suiteSetting: "Suite Settings",
orderManager: "Order Management",
userSuites: "User Suites",
netTest: "Network Test",
},
certificateRepo: {
title: "Certificate Repository",
@@ -223,9 +231,13 @@ export default {
notificationWhen: "Notification Timing",
notificationHelper: "Get real-time alerts when the task fails",
groupIdTitle: "Pipeline Group",
addToMonitorEnabled: "Add to Cert Monitor",
addToMonitorDomains: "Add to Monitor Domains",
},
notificationDefault: "Use Default Notification",
monitor: {
remark: "Remark",
title: "Site Certificate Monitoring",
description: "Check website certificates' expiration at 0:00 daily; reminders sent 10 days before expiration (using default notification channel);",
settingLink: "Site Monitoring Settings",
@@ -247,6 +259,7 @@ export default {
certDomains: "Certificate Domains",
certProvider: "Issuer",
certStatus: "Certificate Status",
error: "Error",
status: {
ok: "Valid",
expired: "Expired",
@@ -280,6 +293,8 @@ export default {
cronTrigger: "Scheduled trigger for monitoring",
dnsServer: "DNS Server",
dnsServerHelper: "Use a custom domain name resolution server, such as: 1.1.1.1 , support multiple",
certValidDays: "Certificate Valid Days",
certValidDaysHelper: "Number of days before expiration to send a notification",
},
},
checkStatus: {
@@ -289,7 +304,7 @@ export default {
},
domainList: {
title: "Domain List",
helper: "Format: domain:port:name, one per line. Port and name are optional.\nExamples:\nwww.baidu.com:443:Baidu\nwww.taobao.com::Taobao\nwww.google.com",
helper: "Format: domain:port:name:remark, one per line. Port and name are optional.\nExamples:\nwww.baidu.com:443:Baidu:remarkText\nwww.taobao.com::Taobao\nwww.google.com",
required: "Please enter domains to import",
placeholder: "www.baidu.com:443:Baidu\nwww.taobao.com::Taobao\nwww.google.com\n",
},
@@ -447,6 +462,7 @@ export default {
description: "Description",
createTime: "Creation Time",
updateTime: "Update Time",
mainDomain: "Main Domain",
edit: "Edit",
groupName: "Group Name",
enterGroupName: "Please enter group name",
@@ -715,19 +731,32 @@ export default {
addonName: "Name",
addonNameHelper: "Fill freely, helps to distinguish when multiple same type exist",
addonTypeSelect: "Select type",
dates: {
years: "{count} years",
months: "{count} months",
},
sys: {
setting: {
baseSetting: "Base Settings",
registerSetting: "Register Settings",
safeSetting: "Safe Settings",
paymentSetting: "Payment Settings",
captchaSetting: "Captcha Setting",
pipelineSetting: "Pipeline Settings",
showRunStrategy: "Show RunStrategy",
showRunStrategyHelper: "Allow modify the run strategy of the task",
captchaEnabled: "Enable Login Captcha",
captchaHelper: "Whether to enable captcha verification for login",
captchaType: "Captcha Setting",
captchaTest: "Captcha Test",
// 保存后再点击测试,请务必测试通过了,再开启登录验证码
captchaTestHelper: "Save and click test, please make sure the test is passed before enabling login captcha",
baseSetting: "Base Settings",
registerSetting: "Register Settings",
safeSetting: "Safe Settings",
paymentSetting: "Payment Settings",
pipelineValidTimeEnabled: "Enable Pipeline Valid Time",
pipelineValidTimeEnabledHelper: "Whether to enable the valid time of the pipeline",
certDomainAddToMonitorEnabled: "Add Domain to Certificate Monitor",
certDomainAddToMonitorEnabledHelper: "Whether to add the domain to the certificate monitor",
},
},
modal: {

View File

@@ -62,6 +62,7 @@ export default {
suiteBuy: "套餐购买",
myTrade: "我的订单",
paymentReturn: "支付返回",
hasExpired: "已过期",
user: {
greeting: "您好",
@@ -142,10 +143,15 @@ export default {
triggerType: "触发类型",
pipelineId: "流水线Id",
},
pi: {
validTime: "流水线有效期",
validTimeHelper: "不填则为永久有效",
},
types: {
certApply: "证书申请",
certUpload: "证书上传",
custom: "自定义",
template: "模版",
},
myPipelines: "我的流水线",
selectedCount: "已选择 {count} 项",
@@ -181,6 +187,7 @@ export default {
suiteSetting: "套餐设置",
orderManager: "订单管理",
userSuites: "用户套餐",
netTest: "网络测试",
},
certificateRepo: {
title: "证书仓库",
@@ -228,9 +235,12 @@ export default {
notificationWhen: "通知时机",
notificationHelper: "任务执行失败实时提醒",
groupIdTitle: "流水线分组",
addToMonitorEnabled: "添加到证书监控",
addToMonitorDomains: "添加到监控域名",
},
notificationDefault: "使用默认通知",
monitor: {
remark: "备注",
title: "站点证书监控",
description: "每天0点检查网站证书的过期时间到期前10天时将发出提醒使用默认通知渠道;",
settingLink: "站点监控设置",
@@ -252,6 +262,7 @@ export default {
certDomains: "证书域名",
certProvider: "颁发机构",
certStatus: "证书状态",
error: "错误信息",
status: {
ok: "正常",
expired: "过期",
@@ -285,6 +296,8 @@ export default {
cronTrigger: "定时触发监控",
dnsServer: "DNS服务器",
dnsServerHelper: "使用自定义的域名解析服务器1.1.1.1 , 支持多个",
certValidDays: "证书到期前天数",
certValidDaysHelper: "证书到期前多少天发送通知",
},
},
checkStatus: {
@@ -294,9 +307,9 @@ export default {
},
domainList: {
title: "域名列表",
helper: "格式【域名:端口:名称】,一行一个,其中端口、名称可以省略\n比如\nwww.baidu.com:443:百度\nwww.taobao.com::淘宝\nwww.google.com",
helper: "格式【域名:端口:名称:备注】,一行一个,其中端口、名称、备注可以省略\n比如\nwww.baidu.com:443:百度:备注文本\nwww.taobao.com::淘宝\nwww.google.com",
required: "请输入要导入的域名",
placeholder: "www.baidu.com:443:百度\nwww.taobao.com::淘宝\nwww.google.com\n",
placeholder: "www.baidu.com:443:百度:备注文本\nwww.taobao.com::淘宝\nwww.google.com\n",
},
accountInfo: "账号信息",
securitySettings: "认证安全设置",
@@ -453,6 +466,7 @@ export default {
description: "说明",
createTime: "创建时间",
updateTime: "更新时间",
mainDomain: "主域名",
edit: "编辑",
groupName: "分组名称",
enterGroupName: "请输入分组名称",
@@ -461,7 +475,7 @@ export default {
batchDeleteConfirm: "确定要批量删除这{count}条记录吗",
selectRecordFirst: "请先勾选记录",
subdomainHosted: "托管的子域名",
subdomainHelpText: "如果您不理解什么是子域托管,请不要随意设置可能导致证书无法申请,可以参考文档",
subdomainHelpText: "如果您不理解什么是子域托管,请不要随意设置可能导致证书无法申请,以前设置过的cname记录也需要重新配置可以参考文档",
subdomainManagement: "子域管理",
isDisabled: "是否禁用",
enabled: "启用",
@@ -717,19 +731,32 @@ export default {
copyPipelineConfig: "复制该流水线配置作为模板来源",
pipeline: "流水线",
},
dates: {
years: "{count}年",
months: "{count}月",
},
sys: {
setting: {
baseSetting: "基本设置",
registerSetting: "注册设置",
safeSetting: "安全设置",
paymentSetting: "支付设置",
captchaSetting: "验证码设置",
pipelineSetting: "流水线设置",
showRunStrategy: "显示运行策略选择",
showRunStrategyHelper: "任务设置中是否允许选择运行策略",
captchaEnabled: "启用登录验证码",
captchaHelper: "登录时是否启用验证码",
captchaType: "验证码配置",
captchaTest: "测试验证码",
captchaTestHelper: "保存后再点击测试,请务必测试通过了,再开启登录验证码",
baseSetting: "基本设置",
registerSetting: "注册设置",
safeSetting: "安全设置",
paymentSetting: "支付设置",
pipelineValidTimeEnabled: "启用流水线有效期",
pipelineValidTimeEnabledHelper: "是否启用流水线有效期",
certDomainAddToMonitorEnabled: "证书域名添加到证书监控",
certDomainAddToMonitorEnabledHelper: "创建证书流水线时是否可以选择将域名添加到证书监控",
},
},
modal: {

View File

@@ -1,6 +1,6 @@
import { request } from "/src/api/service";
// import "/src/mock";
import { ColumnCompositionProps, CrudOptions, FastCrud, PageQuery, PageRes, setLogger, TransformResProps, useColumns, UseCrudProps, UserPageQuery, useTypes, utils } from "@fast-crud/fast-crud";
import { ColumnCompositionProps, CrudOptions, FastCrud, PageQuery, PageRes, setLogger, TransformResProps, useColumns, UseCrudProps, UserPageQuery, useTypes, utils, forEachTableColumns } from "@fast-crud/fast-crud";
import "@fast-crud/fast-crud/dist/style.css";
import { FsExtendsCopyable, FsExtendsEditor, FsExtendsJson, FsExtendsTime, FsExtendsUploader, FsExtendsInput } from "@fast-crud/fast-extends";
import "@fast-crud/fast-extends/dist/style.css";
@@ -14,25 +14,27 @@ import { usePreferences } from "/@/vben/preferences";
import { LocalStorage } from "/@/utils/util.storage";
import { FsEditorCode } from "@fast-crud/editor-code";
import "@fast-crud/editor-code/dist/style.css"
import "@fast-crud/editor-code/dist/style.css";
class ColumnSizeSaver {
save: (key: string, size: number) => void;
constructor() {
this.save = debounce((key: string, size: number) => {
type: string;
save: (key: string, value: any) => void;
constructor(type: string = "columnSize") {
this.type = type;
this.save = debounce((key: string, value: any) => {
const saveKey = this.getKey();
let data = LocalStorage.get(saveKey);
if (!data) {
data = {};
}
data[key] = size;
data[key] = value;
LocalStorage.set(saveKey, data);
});
}
getKey() {
const loc = window.location;
const currentUrl = `${loc.pathname}${loc.search}${loc.hash}`;
return `columnSize-${currentUrl}`;
return `${this.type}-${currentUrl}`;
}
get(key: string) {
const saveKey = this.getKey();
@@ -45,6 +47,7 @@ class ColumnSizeSaver {
}
}
const columnSizeSaver = new ColumnSizeSaver();
const tableSortSaver = new ColumnSizeSaver("tableSorter");
function install(app: App, options: any = {}) {
app.use(UiAntdv);
@@ -63,6 +66,8 @@ function install(app: App, options: any = {}) {
commonOptions(props: UseCrudProps): CrudOptions {
utils.logger.debug("commonOptions:", props);
const crudBinding = props.crudExpose?.crudBinding;
const crudExpose = props.crudExpose;
const { isMobile } = usePreferences();
const opts: CrudOptions = {
settings: {
@@ -74,6 +79,20 @@ function install(app: App, options: any = {}) {
},
},
},
onUseCrud(bindings: any) {
const oldSorter = tableSortSaver.get("sorter");
if (oldSorter) {
const { prop, order } = oldSorter;
forEachTableColumns(bindings.table.columns, (column: any) => {
if (column.key === prop) {
column.sortOrder = order;
} else {
column.sortOrder = false;
}
});
bindings.table.sort = oldSorter;
}
},
},
table: {
scroll: {
@@ -104,6 +123,30 @@ function install(app: App, options: any = {}) {
return "-";
},
},
onSortChange: (sortChange: any) => {
const { isServerSort, prop, asc, order } = sortChange;
const oldSort = crudBinding.value.table.sort;
const newSorter = isServerSort ? { prop, order, asc } : null;
forEachTableColumns(crudBinding.value.table.columns, (column: any) => {
if (column.key === prop) {
column.sortOrder = order;
} else {
column.sortOrder = false;
}
});
crudBinding.value.table.sort = newSorter;
if (newSorter) {
tableSortSaver.save("sorter", newSorter);
} else {
tableSortSaver.clear();
}
if (isServerSort || oldSort != null) {
crudExpose.doRefresh();
}
},
},
toolbar: {
export: {
@@ -189,6 +232,10 @@ function install(app: App, options: any = {}) {
},
wrapperCol: {
span: null,
buttons: {
copy: { show: false },
paste: { show: false },
},
},
wrapper: {
saveRemind: true,

View File

@@ -249,6 +249,17 @@ export const sysResources = [
},
],
},
{
title: "certd.sysResources.netTest",
name: "NetTest",
path: "/sys/nettest",
component: "/sys/nettest/index.vue",
meta: {
icon: "ion:build-outline",
auth: true,
keepAlive: true,
},
},
],
},
];

View File

@@ -50,6 +50,12 @@ export type SysPublicSetting = {
captchaEnabled?: boolean;
captchaType?: number;
captchaAddonId?: number;
//流水线是否启用有效期
pipelineValidTimeEnabled?: boolean;
//证书域名添加到监控
certDomainAddToMonitorEnabled?: boolean;
};
export type SuiteSetting = {
enabled?: boolean;
@@ -63,6 +69,9 @@ export type SysPrivateSetting = {
type?: string;
config?: any;
};
//http请求超时时间
httpRequestTimeout?: number;
};
export type SysInstallInfo = {
siteId: string;

View File

@@ -19,6 +19,10 @@ div#app {
height: 100%;
}
pre.pre{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
h1,
h2,
h3,

View File

@@ -1,23 +1,23 @@
::-webkit-scrollbar {
width: 8px;
height: 8px;
width: 12px !important;
height: 12px !important;
}
::-webkit-scrollbar-track {
width: 8px;
width: 12px !important;
background: rgba(#101f1c, 0.1);
-webkit-border-radius: 2em;
-moz-border-radius: 2em;
border-radius: 2em;
-webkit-border-radius: 4em;
-moz-border-radius: 4em;
border-radius: 4em;
}
::-webkit-scrollbar-thumb {
// background-color: rgba(#101F1C, 0.5);
background-clip: padding-box;
min-height: 28px;
-webkit-border-radius: 2em;
-moz-border-radius: 2em;
border-radius: 2em;
-webkit-border-radius: 4em;
-moz-border-radius: 4em;
border-radius: 4em;
background-color: #b3b3b3;
box-shadow: 0px 1px 1px #eee inset;
}

View File

@@ -54,8 +54,8 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
);
},
{
inheritAttrs: false,
name: "VbenParentModal",
inheritAttrs: false,
}
);
return [Modal, extendedApi as ExtendedModalApi] as const;
@@ -104,8 +104,8 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
);
},
{
inheritAttrs: false,
name: "VbenModal",
inheritAttrs: false,
}
);
injectData.extendApi?.(extendedApi);

View File

@@ -91,6 +91,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
type: "number",
column: {
width: 50,
order: -999,
},
form: {
show: false,

View File

@@ -66,6 +66,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
type: "number",
column: {
width: 100,
order: -999,
},
form: {
show: false,

View File

@@ -75,6 +75,7 @@ export function getCommonColumnDefine(crudExpose: any, typeRef: any, api: any, a
type: "number",
column: {
width: 100,
order: -999,
},
form: {
show: false,

View File

@@ -0,0 +1,64 @@
import { dict } from "@fast-crud/fast-crud";
import { request } from "/src/api/service";
export function createApi() {
const apiPrefix = "/basic/group";
return {
async GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query,
});
},
async AddObj(obj: any) {
return await request({
url: apiPrefix + "/add",
method: "post",
data: obj,
});
},
async UpdateObj(obj: any) {
return await request({
url: apiPrefix + "/update",
method: "post",
data: obj,
});
},
async DelObj(id: number) {
return await request({
url: apiPrefix + "/delete",
method: "post",
params: { id },
});
},
async GetObj(id: number) {
return await request({
url: apiPrefix + "/info",
method: "post",
params: { id },
});
},
async ListAll(type: string) {
return await request({
url: apiPrefix + "/all",
method: "post",
params: { type },
});
},
};
}
export const pipelineGroupApi = createApi();
export function createGroupDictRef(type: string) {
return dict({
url: "/basic/group/all?type=" + type,
value: "id",
label: "name",
});
}

View File

@@ -0,0 +1,142 @@
import { useI18n } from "/src/locales";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { pipelineGroupApi } from "./api";
import { ref } from "vue";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
const api = pipelineGroupApi;
const typeRef = ref(context.type);
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async (req: EditReq) => {
const { form, row } = req;
form.id = row.id;
const res = await api.UpdateObj(form);
return res;
};
const delRequest = async (req: DelReq) => {
const { row } = req;
return await api.DelObj(row.id);
};
const addRequest = async (req: AddReq) => {
const { form } = req;
form.type = typeRef.value;
const res = await api.AddObj(form);
return res;
};
return {
crudOptions: {
settings: {
plugins: {
mobile: {
props: {
rowHandle: {
width: 160,
},
},
},
},
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
search: {
initialForm: {
type: typeRef.value,
},
},
form: {
labelCol: {
//固定label宽度
span: null,
style: {
width: "100px",
},
},
col: {
span: 22,
},
wrapper: {
width: 600,
},
},
rowHandle: {
width: 200,
group: {
editable: {
edit: {
text: t("certd.edit"),
order: -1,
type: "primary",
click({ row, index }) {
crudExpose.openEdit({
index,
row,
});
},
},
},
},
},
table: {
editable: {
enabled: true,
mode: "cell",
exclusive: true,
//排他式激活效果,将其他行的编辑状态触发保存
exclusiveEffect: "save", //自动保存其他行编辑状态cancel = 自动关闭其他行编辑状态
async updateCell(opts) {
const { row, key, value } = opts;
//如果是添加,需要返回{[rowKey]:xxx},比如:{id:2}
return await api.UpdateObj({ id: row.id, [key]: value });
},
},
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
search: {
show: true,
},
column: {
width: 100,
editable: {
disabled: true,
},
},
form: {
show: false,
},
},
name: {
title: t("certd.groupName"),
search: {
show: true,
},
type: "text",
form: {
rules: [
{
required: true,
message: t("certd.enterGroupName"),
},
],
},
column: {
width: 400,
},
},
},
},
};
}

View File

@@ -0,0 +1,58 @@
<template>
<div class="pi-group-selector flex full-w">
<div class="flex-1">
<fs-dict-select :value="modelValue" :dict="groupDictRef" :allow-clear="true" @update:value="doUpdate"></fs-dict-select>
</div>
<fs-table-select
class="flex-0"
:create-crud-options="createCrudOptions"
:crud-options-override="{
search: { show: false, initialForm: { type: props.type } },
table: {
scroll: {
x: 540,
},
},
}"
:model-value="modelValue"
:dict="groupDictRef"
:show-current="false"
:show-select="false"
:dialog="{ width: 960 }"
:destroy-on-close="false"
height="400px"
@update:model-value="doUpdate"
@dialog-closed="doRefresh"
>
<template #default="scope">
<fs-button class="ml-5" type="primary" icon="ant-design:edit-outlined" @click="scope.open({ context: { type: props.type } })"></fs-button>
</template>
</fs-table-select>
</div>
</template>
<script setup lang="ts">
import { createGroupDictRef } from "./api";
import createCrudOptions from "./crud";
import { dict, FsDictSelect } from "@fast-crud/fast-crud";
const props = defineProps<{
modelValue?: number;
type: string;
}>();
defineOptions({
name: "GroupSelector",
});
const groupDictRef = createGroupDictRef(props.type);
const emit = defineEmits(["refresh", "update:modelValue", "change"]);
function doRefresh() {
emit("refresh");
groupDictRef.reloadDict();
}
function doUpdate(value: any) {
emit("update:modelValue", value);
}
</script>

View File

@@ -0,0 +1,37 @@
<template>
<fs-page>
<template #header>
<div class="title">
分组管理
<span class="sub">流水线分组</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</fs-page>
</template>
<script lang="ts">
import { defineComponent, onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
export default defineComponent({
name: "BasicGroupManager",
setup() {
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: {} });
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
return {
crudBinding,
crudRef,
};
},
});
</script>

View File

@@ -57,4 +57,3 @@ export async function DeleteBatch(ids: any[]) {
data: { ids },
});
}

View File

@@ -95,6 +95,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
type: "number",
column: {
width: 80,
order: -999,
},
form: {
show: false,

View File

@@ -67,3 +67,13 @@ export async function DoVerify(id: number) {
},
});
}
export async function ResetStatus(id: number) {
return await request({
url: apiPrefix + "/resetStatus",
method: "post",
data: {
id,
},
});
}

View File

@@ -5,7 +5,7 @@ import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { useUserStore } from "/@/store/user";
import { useSettingStore } from "/@/store/settings";
import { message } from "ant-design-vue";
import { message, Modal } from "ant-design-vue";
import CnameTip from "/@/components/plugins/cert/domains-verify-plan-editor/cname-tip.vue";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
@@ -79,6 +79,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
type: "number",
column: {
width: 80,
order: -999,
},
form: {
show: false,
@@ -188,16 +189,32 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
column: {
width: 120,
align: "center",
align: "left",
cellRender({ value, row }) {
async function resetStatus() {
Modal.confirm({
title: "重置状态",
content: "确定要重置校验状态吗?",
onOk: async () => {
await api.ResetStatus(row.id);
await crudExpose.doRefresh();
},
});
}
return (
<div class={"flex flex-center"}>
<div class={"flex flex-left"}>
<fs-values-format modelValue={value} dict={dictRef}></fs-values-format>
{row.error && (
<a-tooltip title={row.error}>
<fs-icon class={"ml-5 color-red"} icon="ion:warning-outline"></fs-icon>
</a-tooltip>
)}
{row.status === "valid" && (
<a-tooltip title={"重置校验状态,重新校验"}>
<fs-icon class={"ml-5 pointer "} icon="solar:undo-left-square-bold" onClick={resetStatus}></fs-icon>
</a-tooltip>
)}
</div>
);
},
@@ -251,6 +268,13 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
},
mainDomain: {
title: t("certd.mainDomain"),
type: "text",
form: {
show: false,
},
},
createTime: {
title: t("certd.createTime"),
type: "datetime",

View File

@@ -93,6 +93,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
type: "number",
column: {
width: 100,
order: -999,
},
form: {
show: false,

View File

@@ -217,14 +217,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
show: false,
},
column: {
sorter: true,
sorter: false,
conditionalRender: false,
cellRender({ row }) {
const {
applyTime,
effectiveTime,
expiresTime,
} = row || {};
const { applyTime, effectiveTime, expiresTime } = row || {};
if (!expiresTime) {
return "-";
}

View File

@@ -35,6 +35,14 @@ export const siteInfoApi = {
});
},
async BatchDelObj(ids: number[]) {
return await request({
url: apiPrefix + "/batchDelete",
method: "post",
data: { ids },
});
},
async GetObj(id: number) {
return await request({
url: apiPrefix + "/info",

View File

@@ -1,15 +1,18 @@
// @ts-ignore
import { useI18n } from "/src/locales";
import { AddReq, ColumnCompositionProps, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { AddReq, ColumnCompositionProps, ColumnProps, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DataFormatterContext, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { siteInfoApi } from "./api";
import * as settingApi from "./setting/api";
import dayjs from "dayjs";
import { Modal, notification } from "ant-design-vue";
import { message, Modal, notification } from "ant-design-vue";
import { useSettingStore } from "/@/store/settings";
import { mySuiteApi } from "/@/views/certd/suite/mine/api";
import { mitter } from "/@/utils/util.mitt";
import { useSiteIpMonitor } from "./ip/use";
import { useSiteImport } from "/@/views/certd/monitor/site/use";
import { ref } from "vue";
import GroupSelector from "../../basic/group/group-selector.vue";
import { createGroupDictRef } from "../../basic/group/api";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
const api = siteInfoApi;
@@ -30,6 +33,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const addRequest = async (req: AddReq) => {
const { form } = req;
delete form.id;
const res = await api.AddObj(form);
return res;
};
@@ -47,6 +51,35 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const { openSiteIpMonitorDialog } = useSiteIpMonitor();
const { openSiteImportDialog } = useSiteImport();
const certValidDaysRef = ref(10);
async function loadSetting() {
const setting = await settingApi.SiteMonitorSettingsGet();
certValidDaysRef.value = setting?.certValidDays || 10;
}
loadSetting();
const selectedRowKeys = ref([]);
const handleBatchDelete = () => {
if (selectedRowKeys.value?.length > 0) {
Modal.confirm({
title: "确认",
content: `确定要批量删除这${selectedRowKeys.value.length}条记录吗`,
async onOk() {
await api.BatchDelObj(selectedRowKeys.value);
message.info("删除成功");
crudExpose.doRefresh();
selectedRowKeys.value = [];
},
});
} else {
message.error("请先勾选记录");
}
};
context.handleBatchDelete = handleBatchDelete;
function checkAll() {
Modal.confirm({
title: t("certd.monitor.confirmTitle"), // "确认"
@@ -60,6 +93,16 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
});
}
const GroupTypeSite = "site";
const groupDictRef = createGroupDictRef(GroupTypeSite);
function getDefaultGroupId() {
const searchFrom = crudExpose.getSearchValidatedFormData();
if (searchFrom.groupId) {
return searchFrom.groupId;
}
}
return {
id: "siteMonitorCrud",
crudOptions: {
@@ -69,6 +112,68 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
editRequest,
delRequest,
},
tabs: {
name: "groupId",
show: true,
},
toolbar: {
buttons: {
export: {
show: true,
},
},
export: {
dataFrom: "search",
columnFilter: (col: ColumnProps) => {
//列过滤器返回true则导出该列
//例如: 只导出show=true的列
return col.show === true;
},
dataFormatter: (opts: DataFormatterContext) => {
//例如 格式化日期
const { row, originalRow, col, exportCol } = opts;
const key = col.key;
const element = originalRow[key];
if (key.includes("Time") && element) {
row[key] = dayjs(element).format("YYYY-MM-DD HH:mm:ss");
}
if (col.width) {
exportCol.width = col.width / 10;
}
if (col.key === "certInfo" && originalRow?.certProvider) {
row[key] = originalRow?.certProvider + " " + originalRow?.certDomains;
}
//参数说明
// DataFormatterContext = {row: any,originalRow: any, key: string, col: ColumnProps, exportCol:ExportColumn}
// row = 当前行数据
// originalRow = 当前行原始数据
// key = 当前列的key
// col = 当前列的配置
// exportCol = 当前列的导出配置
},
},
},
pagination: {
pageSizeOptions: ["10", "20", "50", "100", "200"],
},
settings: {
plugins: {
//这里使用行选择插件生成行选择crudOptions配置最终会与crudOptions合并
rowSelection: {
enabled: true,
props: {
multiple: true,
crossPage: false,
selectedRowKeys: () => {
return selectedRowKeys;
},
},
},
},
},
form: {
labelCol: {
//固定label宽度
@@ -112,7 +217,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}
}
await crudExpose.openAdd({});
const defaultGroupId = getDefaultGroupId();
await crudExpose.openAdd({
row: { groupId: defaultGroupId },
});
},
},
//导入按钮
@@ -121,7 +229,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
text: t("certd.monitor.bulkImport"),
type: "primary",
async click() {
const defaultGroupId = getDefaultGroupId();
openSiteImportDialog({
defaultGroupId,
afterSubmit() {
crudExpose.doRefresh();
},
@@ -173,10 +283,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
},
tabs: {
name: "disabled",
show: true,
},
// tabs: {
// name: "disabled",
// show: true,
// },
columns: {
id: {
title: "ID",
@@ -357,6 +467,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
column: {
sorter: true,
width: 155,
show: false,
},
},
certExpiresTime: {
@@ -385,10 +496,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
column: {
conditionalRender: false,
cellRender({ row }) {
const {
certEffectiveTime: effectiveTime,
certExpiresTime: expiresTime,
} = row || {};
const certValidDays = certValidDaysRef.value;
const { certEffectiveTime: effectiveTime, certExpiresTime: expiresTime } = row || {};
if (!expiresTime) {
return "-";
}
@@ -400,13 +509,53 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const effectiveDays = Math.max(90, dayjs(expiresTime).diff(applyDate, "day"));
// 距离失效时间剩余天数
const leftDays = dayjs(expiresTime).diff(dayjs(), "day");
const color = leftDays < 20 ? "red" : "#389e0d";
const color = leftDays < certValidDays ? "red" : "#389e0d";
const percent = (leftDays / effectiveDays) * 100;
// console.log('cellRender', 'effectiveDays', effectiveDays, 'expiresTime', expiresTime, 'applyTime', applyTime, 'percent', percent, row)
return <a-progress title={expireDate + t("certd.monitor.expired")} percent={percent} strokeColor={color} format={(percent: number) => `${leftDays}${t("certd.monitor.days")}`} />;
},
},
},
groupId: {
title: t("certd.fields.group"),
type: "dict-select",
search: {
show: true,
},
dict: groupDictRef,
form: {
component: {
name: GroupSelector,
vModel: "modelValue",
type: GroupTypeSite,
onRefresh() {
groupDictRef.reloadDict();
},
},
},
column: {
width: 130,
align: "center",
component: {
color: "auto",
},
sorter: true,
},
},
remark: {
title: t("certd.monitor.remark"),
search: {
show: false,
},
type: "text",
column: {
width: 200,
sorter: true,
cellRender({ value }) {
return <a-tooltip title={value}>{value}</a-tooltip>;
},
},
},
lastCheckTime: {
title: t("certd.monitor.lastCheckTime"),
search: {
@@ -574,6 +723,21 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
show: false,
},
},
error: {
title: t("certd.monitor.error"),
search: {
show: false,
},
type: "text",
form: { show: false },
column: {
width: 200,
sorter: true,
cellRender({ value }) {
return <a-tooltip title={value}>{value}</a-tooltip>;
},
},
},
},
},
};

View File

@@ -15,23 +15,28 @@
</div>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-left>
<a-tooltip title="批量删除">
<fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>
</a-tooltip>
</template>
</fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import { onActivated, onMounted } from "vue";
import createCrudOptions from "./crud";
import { siteInfoApi } from "./api";
import { Modal, notification } from "ant-design-vue";
import { useI18n } from "/src/locales";
const { t } = useI18n();
defineOptions({
name: "SiteCertMonitor",
});
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: {} });
const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions });
const handleBatchDelete = context.handleBatchDelete;
// 页面打开后获取列表数据
onMounted(() => {

View File

@@ -6,6 +6,7 @@ export type UserSiteMonitorSetting = {
retryTimes?: number;
cron?: string;
dnsServer?: string[];
certValidDays?: number;
};
export async function SiteMonitorSettingsGet() {

View File

@@ -17,6 +17,12 @@
</div>
<div class="helper">{{ t("certd.monitor.setting.monitorRetryTimes") }}</div>
</a-form-item>
<a-form-item :label="t('certd.monitor.setting.certValidDays')" :name="['certValidDays']">
<div class="flex">
<a-input-number v-model:value="formState.certValidDays" />
</div>
<div class="helper">{{ t("certd.monitor.setting.certValidDaysHelper") }}</div>
</a-form-item>
<a-form-item :label="t('certd.monitor.setting.dnsServer')" :name="['dnsServer']">
<div class="flex">
<a-select v-model:value="formState.dnsServer" mode="tags" :open="false" />

View File

@@ -1,13 +1,13 @@
import { useFormWrapper } from "@fast-crud/fast-crud";
import { siteInfoApi } from "./api";
import { useI18n } from "/src/locales";
import GroupSelector from "../../basic/group/group-selector.vue";
export function useSiteImport() {
const { t } = useI18n();
const { openCrudFormDialog } = useFormWrapper();
async function openSiteImportDialog(opts: { afterSubmit: any }) {
const { afterSubmit } = opts;
async function openSiteImportDialog(opts: { afterSubmit: any; defaultGroupId?: number }) {
const { afterSubmit, defaultGroupId } = opts;
await openCrudFormDialog<any>({
crudOptions: {
columns: {
@@ -26,6 +26,21 @@ export function useSiteImport() {
},
},
},
groupId: {
type: "select",
title: t("certd.fields.group"),
form: {
value: defaultGroupId,
component: {
name: GroupSelector,
vModel: "modelValue",
type: "site",
},
col: {
span: 24,
},
},
},
},
form: {

View File

@@ -72,6 +72,7 @@ export function getCommonColumnDefine(crudExpose: any, typeRef: any, api: any) {
type: "number",
column: {
width: 100,
order: -999,
},
form: {
show: false,

View File

@@ -6,12 +6,13 @@ import { useRouter } from "vue-router";
import { compute, CreateCrudOptionsRet, dict, useFormWrapper } from "@fast-crud/fast-crud";
import NotificationSelector from "/@/views/certd/notification/notification-selector/index.vue";
import { useReference } from "/@/use/use-refrence";
import { ref } from "vue";
import { computed, ref } from "vue";
import * as api from "../api";
import { PluginGroup, usePluginStore } from "/@/store/plugin";
import { createNotificationApi } from "/@/views/certd/notification/api";
import GroupSelector from "../group/group-selector.vue";
import { useI18n } from "/src/locales";
import { useSettingStore } from "/@/store/settings";
export function fillPipelineByDefaultForm(pipeline: any, form: any) {
const triggers = [];
@@ -78,6 +79,7 @@ export function useCertPipelineCreator() {
const { openCrudFormDialog } = useFormWrapper();
const pluginStore = usePluginStore();
const settingStore = useSettingStore();
const router = useRouter();
function createCrudOptions(certPlugins: any[], getFormData: any, doSubmit: any): CreateCrudOptionsRet {
@@ -251,7 +253,48 @@ export function useCertPipelineCreator() {
name: GroupSelector,
vModel: "modelValue",
},
order: 9999,
order: 888,
},
},
addToMonitorEnabled: {
title: t("certd.pipelineForm.addToMonitorEnabled"),
type: "switch",
form: {
show: computed(() => {
return settingStore.isPlus && settingStore.sysPublic?.certDomainAddToMonitorEnabled;
}),
value: false,
component: {
name: "a-switch",
vModel: "checked",
},
col: {
span: 24,
},
order: 999,
valueChange({ value, form }) {
if (value) {
form.addToMonitorDomains = form.domains.join("\n").replaceAll("*", "www");
}
},
},
},
addToMonitorDomains: {
title: t("certd.pipelineForm.addToMonitorDomains"),
type: "text",
form: {
show: compute(({ form }) => {
return form.addToMonitorEnabled;
}),
component: {
name: "a-textarea",
vModel: "value",
},
col: {
span: 24,
},
helper: t("certd.domainList.helper"),
order: 999,
},
},
},
@@ -330,6 +373,8 @@ export function useCertPipelineCreator() {
keepHistoryCount: 30,
type: "cert",
groupId,
addToMonitorEnabled: form.addToMonitorEnabled,
addToMonitorDomains: form.addToMonitorDomains,
});
if (form.email) {
try {

View File

@@ -366,10 +366,7 @@ export default function ({ crudExpose, context: { groupDictRef, selectedRowKeys
},
column: {
cellRender({ row }) {
const {
certEffectiveTime: effectiveTime,
certExpiresTime: expiresTime,
} = row?.lastVars || {};
const { certEffectiveTime: effectiveTime, certExpiresTime: expiresTime } = row?.lastVars || {};
if (!expiresTime) {
return "-";
}
@@ -469,7 +466,7 @@ export default function ({ crudExpose, context: { groupDictRef, selectedRowKeys
},
column: {
sorter: true,
width: 80,
width: 100,
align: "center",
component: {
name: "fs-dict-switch",
@@ -516,7 +513,7 @@ export default function ({ crudExpose, context: { groupDictRef, selectedRowKeys
{ value: "cert", label: t("certd.types.certApply") },
{ value: "cert_upload", label: t("certd.types.certUpload") },
{ value: "custom", label: t("certd.types.custom") },
{ value: "template", label: "模版" },
{ value: "template", label: t("certd.types.template") },
],
}),
form: {
@@ -525,7 +522,7 @@ export default function ({ crudExpose, context: { groupDictRef, selectedRowKeys
},
column: {
sorter: true,
width: 90,
width: 110,
align: "center",
show: true,
component: {
@@ -558,16 +555,53 @@ export default function ({ crudExpose, context: { groupDictRef, selectedRowKeys
sorter: true,
},
},
createTime: {
title: t("certd.fields.createTime"),
type: "datetime",
validTime: {
title: t("certd.pi.validTime"),
type: "date",
form: {
show: false,
show: computed(() => {
return settingStore.isPlus && settingStore.sysPublic.pipelineValidTimeEnabled && userStore.isAdmin;
}),
helper: t("certd.pi.validTimeHelper"),
valueResolve({ form, key, value }) {
if (value) {
form[key] = value.valueOf();
}
},
valueBuilder({ form, key, value }) {
if (value) {
form[key] = dayjs(value);
}
},
component: {
presets: [
{ label: t("certd.dates.months", { count: 3 }), value: dayjs().add(3, "month") },
{ label: t("certd.dates.months", { count: 6 }), value: dayjs().add(6, "month") },
{ label: t("certd.dates.years", { count: 1 }), value: dayjs().add(1, "year") },
{ label: t("certd.dates.years", { count: 2 }), value: dayjs().add(2, "year") },
{ label: t("certd.dates.years", { count: 3 }), value: dayjs().add(3, "year") },
{ label: t("certd.dates.years", { count: 4 }), value: dayjs().add(4, "year") },
{ label: t("certd.dates.years", { count: 5 }), value: dayjs().add(5, "year") },
{ label: t("certd.dates.years", { count: 6 }), value: dayjs().add(6, "year") },
],
},
},
column: {
show: computed(() => {
return settingStore.isPlus && settingStore.sysPublic.pipelineValidTimeEnabled;
}),
sorter: true,
width: 155,
align: "center",
cellRender({ value }) {
if (!value || value <= 0) {
return "-";
}
if (value < Date.now()) {
return t("certd.hasExpired");
}
return dayjs(value).format("YYYY-MM-DD");
},
},
},
updateTime: {

View File

@@ -37,6 +37,7 @@ const pipelineOptions: PipelineOptions = {
type: detail.pipeline.type,
from: detail.pipeline.from,
},
validTime: detail.pipeline.validTime,
} as PipelineDetail;
},

View File

@@ -26,7 +26,6 @@
import { onActivated, onMounted, ref } from "vue";
import { dict, useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import PiCertdForm from "./certd-form/index.vue";
import ChangeGroup from "./components/change-group.vue";
import ChangeTrigger from "./components/change-trigger.vue";
import { Modal, notification } from "ant-design-vue";

View File

@@ -28,6 +28,13 @@
未设置触发源不会自动执行
</span>
</a-tag>
<a-tag v-if="pipelineEntity.validTime > 0 && settingStore.sysPublic.pipelineValidTimeEnabled && settingStore.isPlus" :color="pipelineEntity.validTime > Date.now() ? 'green' : 'red'">
<span class="flex">
<fs-icon icon="ion:time-outline"></fs-icon>
<span v-if="pipelineEntity.validTime > Date.now()"> 有效期:<FsTimeHumanize :model-value="pipelineEntity.validTime" :options="{ units: ['d'] }" format="YYYY-MM-DD"></FsTimeHumanize> </span>
<span v-else> 已过期 </span>
</span>
</a-tag>
</div>
<div class="basis-40 flex justify-end mr-10">
<template v-if="editMode">
@@ -343,7 +350,7 @@ export default defineComponent({
const { t } = useI18n();
const currentPipeline: Ref<any> = ref({});
const pipeline: Ref<any> = ref({});
const pipelineEntity: Ref<any> = ref({});
const histories: Ref<RunHistory[]> = ref([]);
const currentHistory: Ref<any> = ref({});
@@ -490,6 +497,7 @@ export default defineComponent({
return;
}
const detail: PipelineDetail = await props.options.getPipelineDetail({ pipelineId: value });
pipelineEntity.value = detail;
currentPipeline.value = merge(
{
title: "新管道流程",
@@ -808,7 +816,7 @@ export default defineComponent({
return nodes;
},
});
throw new Error(errorMessage);
throw new Error(errorMessages?.join(","));
}
}
@@ -822,10 +830,6 @@ export default defineComponent({
saveLoading.value = true;
try {
if (props.options.doSave) {
if (pipeline.value.version == null) {
pipeline.value.version = 0;
}
pipeline.value.version++;
currentPipeline.value = pipeline.value;
//移除空阶段
@@ -970,6 +974,7 @@ export default defineComponent({
nextTriggerTimes,
viewCert,
downloadCert,
pipelineEntity,
};
},
});

View File

@@ -3,6 +3,7 @@ import { PluginGroups } from "/@/store/plugin";
export type PipelineDetail = {
pipeline: Pipeline;
validTime?: number;
};
export type RunHistory = {

View File

@@ -177,6 +177,8 @@ function isNewVersion(version: string, latestVersion: string) {
for (let i = 0; i < current.length; i++) {
if (parseInt(latest[i]) > parseInt(current[i])) {
return true;
} else if (parseInt(latest[i]) < parseInt(current[i])) {
return false;
}
}
return false;
@@ -191,7 +193,6 @@ async function loadLatestVersion() {
const minVersion = settingsStore.productInfo?.app?.minVersion;
if (minVersion) {
//
if (isNewVersion(version.value, minVersion)) {
notification.error({
message: settingsStore.productInfo?.app?.minVersionTip ?? "版本过低,为了您的数据安全,请尽快升级",

View File

@@ -95,11 +95,14 @@ import SmsCode from "/@/views/framework/login/sms-code.vue";
import { useI18n } from "/@/locales";
import { LanguageToggle } from "/@/vben/layouts";
import CaptchaInput from "/@/components/captcha/captcha-input.vue";
import { useRoute } from "vue-router";
export default defineComponent({
name: "LoginPage",
components: { LanguageToggle, SmsCode, CaptchaInput },
setup() {
const { t } = useI18n();
const route = useRoute();
const urlLoginType = route.query.loginType as string | undefined;
const verifyCodeInputRef = ref();
const loading = ref(false);
const userStore = useUserStore();
@@ -110,7 +113,7 @@ export default defineComponent({
phoneCode: "86",
mobile: "",
password: "",
loginType: "password", //password
loginType: urlLoginType || "password", //password
smsCode: "",
captcha: null,
smsCaptcha: null,

View File

@@ -1,7 +1,7 @@
<template>
<div class="main">
<a-form ref="formRef" class="user-layout-register" name="custom-validation" :model="formState" :rules="rules" v-bind="layout" :label-col="{ span: 6 }" @finish="handleFinish" @finish-failed="handleFinishFailed">
<a-tabs v-model:active-key="registerType">
<a-tabs v-model:active-key="registerType" @change="handleTabChange">
<a-tab-pane key="username" tab="用户名注册" :disabled="!settingsStore.sysPublic.usernameRegisterEnabled">
<template v-if="registerType === 'username'">
<a-form-item required has-feedback name="username" label="用户名" :rules="rules.username">
@@ -61,7 +61,7 @@
</a-input-password>
</a-form-item>
<a-form-item has-feedback name="imgCode" label="验证码" :rules="rules.imgCode">
<a-form-item has-feedback name="captchaForEmail" label="验证码" :rules="rules.captchaForEmail">
<CaptchaInput v-model:model-value="formState.captchaForEmail"></CaptchaInput>
</a-form-item>
@@ -70,6 +70,8 @@
</a-form-item>
</template>
</a-tab-pane>
<a-tab-pane v-if="settingsStore.sysPublic.smsLoginEnabled" key="mobile" tab="手机号注册"> </a-tab-pane>
</a-tabs>
<a-form-item>
@@ -90,6 +92,7 @@ import EmailCode from "./email-code.vue";
import { useSettingStore } from "/@/store/settings";
import { notification } from "ant-design-vue";
import CaptchaInput from "/@/components/captcha/captcha-input.vue";
import { useRouter } from "vue-router";
export default defineComponent({
name: "RegisterPage",
components: { CaptchaInput, EmailCode },
@@ -115,6 +118,7 @@ export default defineComponent({
password: "",
confirmPassword: "",
captcha: null,
captchaForEmail: null,
});
const rules = {
@@ -171,6 +175,18 @@ export default defineComponent({
message: "请输入邮件验证码",
},
],
captcha: [
{
required: true,
message: "请通过验证码",
},
],
captchaForEmail: [
{
required: true,
message: "请通过验证码",
},
],
};
const layout = {
labelCol: {
@@ -189,7 +205,7 @@ export default defineComponent({
password: formState.password,
username: formState.username,
email: formState.email,
captcha: formState.captcha,
captcha: registerType.value === "email" ? formState.captchaForEmail : formState.captcha,
validateCode: formState.validateCode,
}) as any
);
@@ -206,6 +222,13 @@ export default defineComponent({
formRef.value.resetFields();
};
const router = useRouter();
const handleTabChange = (key: string) => {
if (key === "mobile") {
router.push({ path: "/login", query: { loginType: "sms" } });
}
};
return {
formState,
formRef,
@@ -216,6 +239,7 @@ export default defineComponent({
resetForm,
registerType,
settingsStore,
handleTabChange,
};
},
});

View File

@@ -0,0 +1,288 @@
<template>
<div class="domain-test-card">
<div class="card-header flex flex-wrap justify-start">
<div v-if="title">{{ title }}</div>
<a-form v-if="editing" layout="inline" :model="formData">
<a-form-item label="域名">
<a-input v-model:value="formData.domain" placeholder="请输入要测试的域名或IP" style="width: 240px" />
</a-form-item>
<a-form-item label="端口">
<a-input-number v-model:value="formData.port" placeholder="请输入端口" :min="1" :max="65535" style="width: 120px" />
</a-form-item>
</a-form>
<div v-else class="domain-info">
<span>域名: {{ formData.domain }}</span>
<span>端口: {{ formData.port }}</span>
</div>
<a-button :disabled="!formData.domain" size="small" type="primary" :loading="loading" @click="runAllTests"> 开始测试 </a-button>
</div>
<div class="card-content">
<div class="test-results">
<!-- 域名解析结果 -->
<test-case ref="domainResolveRef" title="域名解析" :test-method="() => createDomainResolveMethod()" :disabled="!getCurrentDomain()" />
<!-- Ping测试结果 -->
<test-case ref="pingTestRef" title="Ping测试" :test-method="() => createPingTestMethod()" :disabled="!getCurrentDomain()" />
<!-- Telnet测试结果 -->
<test-case ref="telnetTestRef" title="Telnet测试" :port="getCurrentPort()" :test-method="() => createTelnetTestMethod()" :disabled="!getCurrentDomain() || !getCurrentPort()" />
</div>
<div class="summary">
<a-alert :message="testSummary.title" :type="testSummary.status === 'success' ? 'success' : testSummary.status === 'failed' ? 'error' : 'warning'" show-icon :closable="false">
<template v-if="testSummary.text" #description>
<pre class="summary-text pre">{{ testSummary.text }}</pre>
</template>
</a-alert>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, onMounted, watch } from "vue";
import { message } from "ant-design-vue";
import { DomainResolve, PingTest, TelnetTest } from "./api";
import TestCase from "./TestCase.vue";
// 组件属性
const props = defineProps<{
title?: string;
domain?: string;
port?: number;
autoStart?: boolean;
}>();
const editing = ref(!props.domain);
// 测试组件的引用
const domainResolveRef = ref();
const pingTestRef = ref();
const telnetTestRef = ref();
// 表单数据
const formData = reactive({
domain: props.domain || "",
port: props.port || 443,
});
// 加载状态
const loading = ref(false);
// 创建域名解析测试方法
const createDomainResolveMethod = async () => {
const domain = getCurrentDomain();
return DomainResolve(domain);
};
// 创建Ping测试方法
const createPingTestMethod = async () => {
const domain = getCurrentDomain();
return PingTest(domain);
};
// 创建Telnet测试方法
const createTelnetTestMethod = async () => {
const domain = getCurrentDomain();
const port = getCurrentPort();
return TelnetTest(domain, port);
};
// 获取当前使用的域名
const getCurrentDomain = () => {
return formData.domain;
};
// 获取当前使用的端口
const getCurrentPort = () => {
return formData.port;
};
// 获取各测试用例的状态
const getTestStatus = (testRef: any) => {
const result = testRef?.getResult();
if (!result) {
return null;
}
const isNetTestResult = typeof result === "object" && result !== null && "success" in result && "message" in result;
return {
success: isNetTestResult ? result.success : false,
message: isNetTestResult ? result.message : "测试失败",
};
};
// 生成测试总结
const testSummary = computed(() => {
if (loading.value) {
return { status: "waiting", title: "测试中请稍后..." };
}
// 通过computed获取各测试结果
const domainResolveResult = getTestStatus(domainResolveRef.value);
const pingTestResult = getTestStatus(pingTestRef.value);
const telnetTestResult = getTestStatus(telnetTestRef.value);
// 检查是否有测试结果
const testDone = domainResolveResult != null && pingTestResult != null && telnetTestResult != null;
if (!testDone) {
return { status: "waiting", title: '请点击"开始测试"按钮进行网络测试' };
}
// 详细分析不同的测试结果组合
// 1. 三个测试都失败
if (domainResolveResult?.success === false && pingTestResult?.success === false && telnetTestResult?.success === false) {
return {
status: "failed",
title: "所有测试均未通过",
text: `这表明应用容器内的网络可能完全不通。建议:\n1. 检查宿主机的网络连接状态\n2. 确认容器网络配置是否正确\n3. 检查防火墙设置是否阻止了网络访问`,
};
}
// 2. 域名解析成功但Ping不通
if (domainResolveResult?.success === true && pingTestResult?.success === false) {
return {
status: "partial",
title: "域名解析成功但Ping不通",
text: `可能原因:\n1. DNS被劫持解析到了错误的IP地址\n2. 目标服务器禁止了Ping请求\n3. 目标服务器IP被墙\n4. 目标服务器网络不通或已下线`,
};
}
// 3. 域名解析和Ping都成功但Telnet连接失败
if (domainResolveResult?.success === true && pingTestResult?.success === true && telnetTestResult?.success === false) {
return {
status: "partial",
title: "域名解析和Ping测试均通过但Telnet连接失败",
text: `可能原因:\n1. 端口号输入错误,请确认目标服务使用的正确端口\n2. 目标服务器上该端口未开放或服务未启动\n3. 防火墙或安全组限制了该端口的访问\n4. 目标网站被墙`,
};
}
// 4. 域名解析失败,但其他测试可能成功或未执行
if (domainResolveResult?.success === false) {
return {
status: "partial",
title: "域名解析失败",
text: `可能原因:\n1. 域名输入错误或不存在\n2. DNS服务器配置问题\n3. 本地网络DNS解析故障\n4. 域名已过期或被注销`,
};
}
// 5. 所有测试都成功
if (domainResolveResult?.success === true && pingTestResult?.success === true && telnetTestResult?.success === true) {
return {
status: "success",
title: "所有测试均通过",
text: `域名${formData.domain}解析正常能够正常Ping通且端口${formData.port}可访问。`,
};
}
// 6. 其他部分成功的情况
return {
status: "partial",
title: "部分测试未通过",
text: `请结合具体测试结果进行分析:\n- 域名解析:${domainResolveResult ? (domainResolveResult.success ? "成功" : "失败") : "未执行"}\n- Ping测试${pingTestResult ? (pingTestResult.success ? "成功" : "失败") : "未执行"}\n- Telnet测试${telnetTestResult ? (telnetTestResult.success ? "成功" : "失败") : "未执行"}`,
};
});
// 运行全部测试
async function runAllTests() {
const domain = getCurrentDomain();
// 检查是否有域名
if (!domain) {
message.error("请输入域名");
return;
}
loading.value = true;
// 通过组件引用调用测试方法
try {
await Promise.allSettled([domainResolveRef.value?.test(), pingTestRef.value?.test(), telnetTestRef.value?.test()]);
} catch (error) {
message.error("部分测试执行失败请查看详细结果");
} finally {
loading.value = false;
}
}
onMounted(() => {
if (props.autoStart) {
runAllTests();
}
});
</script>
<style lang="less">
.domain-test-card {
border: 1px solid #e8e8e8;
border-radius: 4px;
overflow: hidden;
background-color: #fff;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: #fafafa;
border-bottom: 1px solid #e8e8e8;
}
.card-header h3 {
margin: 0;
font-size: 16px;
font-weight: 500;
}
.card-content {
padding: 16px;
}
.input-form {
margin-bottom: 12px;
padding: 12px;
background-color: #fafafa;
border-radius: 4px;
}
.domain-info {
padding: 5.5px 12px;
background-color: #f0f0f0;
border-radius: 4px;
display: flex;
gap: 16px;
font-size: 14px;
color: #666;
}
.test-buttons {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.test-results {
margin-top: 0px;
}
.summary {
margin-top: 16px;
padding: 12px;
background-color: #f8f9fa;
border-radius: 4px;
.summary-text {
}
}
/* 调整按钮大小 */
.ant-btn {
font-size: 12px;
padding: 2px 8px;
height: 24px;
}
}
</style>

View File

@@ -0,0 +1,140 @@
<template>
<a-card title="服务端信息" class="server-info-card">
<template #extra>
<a-button size="small" :loading="loading" @click="refreshServerInfo">
<template #icon>
<a-icon type="sync" :spin="loading" />
</template>
刷新
</a-button>
</template>
<div v-if="loading" class="loading">
<a-spin size="small" />
<span style="margin-left: 8px">加载中...</span>
</div>
<div v-else-if="error" class="error">
<a-alert message="获取服务器信息失败" :description="error" type="error" show-icon />
</div>
<div v-else class="server-info-grid">
<!-- 本地IP -->
<div class="info-item">
<div class="info-label">本地IP:</div>
<div v-if="serverInfo.localIP && serverInfo.localIP.length > 0" class="info-value">
<a-tag v-for="ip in serverInfo.localIP" :key="ip" type="info" color="blue">{{ ip }}</a-tag>
</div>
<div v-else class="info-empty">暂无信息</div>
</div>
<!-- 外网IP -->
<div class="info-item">
<div class="info-label">外网IP:</div>
<div v-if="serverInfo.publicIP && serverInfo.publicIP.length > 0" class="info-value">
<a-tag v-for="ip in serverInfo.publicIP" :key="ip" type="info" color="green">{{ ip }}</a-tag>
</div>
<div v-else class="info-empty">暂无信息</div>
</div>
<!-- DNS服务器 -->
<div class="info-item">
<div class="info-label">DNS服务器:</div>
<div v-if="serverInfo.dnsServers && serverInfo.dnsServers.length > 0" class="info-value">
<a-tag v-for="dns in serverInfo.dnsServers" :key="dns" type="info" color="cyan">{{ dns }}</a-tag>
</div>
<div v-else class="info-empty">暂无信息</div>
</div>
</div>
</a-card>
</template>
<script lang="ts" setup>
import { ref, onMounted } from "vue";
import { message } from "ant-design-vue";
import { GetServerInfo } from "./api";
// 服务器信息类型
interface ServerInfo {
localIP?: string[];
publicIP?: string[];
dnsServers?: string[];
}
const loading = ref(false);
const error = ref<string | null>(null);
const serverInfo = ref<ServerInfo>({});
// 加载服务器信息
const loadServerInfo = async () => {
loading.value = true;
error.value = null;
try {
serverInfo.value = await GetServerInfo();
} catch (e) {
error.value = e instanceof Error ? e.message : String(e);
message.error("获取服务器信息失败");
} finally {
loading.value = false;
}
};
// 刷新服务器信息
const refreshServerInfo = () => {
loadServerInfo();
};
// 组件挂载时加载数据
onMounted(() => {
loadServerInfo();
});
</script>
<style lang="less">
.server-info-card {
margin-bottom: 16px;
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
color: #666;
}
.error {
margin: 0;
}
.server-info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
}
.info-item {
background-color: #fafafa;
border-radius: 4px;
padding: 12px;
.info-label {
font-size: 14px;
font-weight: 500;
color: #666;
margin-bottom: 8px;
}
.info-value {
font-size: 14px;
color: #333;
.ant-list-item {
padding: 4px 0;
}
}
.info-empty {
font-size: 14px;
color: #999;
font-style: italic;
}
}
}
</style>

View File

@@ -0,0 +1,186 @@
<template>
<div class="test-case" :class="{ loading }">
<div class="case-header">
<span class="flex items-center">
<fs-button size="small" type="text" icon="ion:play-circle" :loading="loading" :disabled="disabled" class="test-button" @click="runTest" />
<a-tag color="blue" class="case-title">
{{ title }}
</a-tag>
<span v-if="port" class="port-info">{{ port }}</span>
</span>
<span v-if="result && isNetTestResult" class="result-status flex-1" :style="{ color: isSuccess ? 'green' : 'red' }">
<span>
{{ isSuccess ? "✓" : "✗" }}
</span>
<span class="ml-2">
{{ result.message }}
</span>
</span>
</div>
<div v-if="result" class="result-content">
<div v-if="error" class="error-message">
<span style="color: red">{{ error }}</span>
</div>
<div v-else-if="isNetTestResult">
<div v-if="resultTestLog" class="test-log">
<pre>{{ resultTestLog }}</pre>
</div>
</div>
<div v-else-if="typeof result === 'object'" class="object-result">
<pre>{{ JSON.stringify(result, null, 2) }}</pre>
</div>
<div v-else class="text-result">
<pre>{{ result }}</pre>
</div>
</div>
<div v-else class="no-result">
<p>暂无结果</p>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from "vue";
import { message } from "ant-design-vue";
// 组件属性
const props = defineProps<{
title: string;
port?: number | string;
testMethod: () => Promise<any>;
disabled?: boolean;
}>();
// 内部状态
const loading = ref(false);
const result = ref<any>(null);
const error = ref<string | null>(null);
// 运行测试
const runTest = async () => {
loading.value = true;
error.value = null;
result.value = null;
try {
const testResult = await props.testMethod();
// 如果结果有 data 属性,则使用 data否则使用整个结果
result.value = testResult.data || testResult;
} catch (err: any) {
result.value = null;
error.value = err.message || "测试失败";
message.error(`${props.title} 测试失败: ${error.value}`);
} finally {
loading.value = false;
}
};
// 暴露方法给父组件
defineExpose({
test: runTest,
getResult: () => result.value,
});
// 辅助计算属性,用于模板中显示结果
const isNetTestResult = computed(() => {
return typeof result.value === "object" && result.value !== null && "success" in result.value && "message" in result.value && "testLog" in result.value;
});
const isSuccess = computed(() => {
return isNetTestResult.value && result.value.success;
});
const resultMessage = computed(() => {
return isNetTestResult.value ? result.value.message : "";
});
const resultTestLog = computed(() => {
return isNetTestResult.value ? result.value.testLog : "";
});
const resultError = computed(() => {
return isNetTestResult.value ? result.value.error : "";
});
</script>
<style lang="less" scoped>
.test-case {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
position: relative;
&:last-child {
border-bottom: none;
}
&.loading {
opacity: 0.7;
}
}
.case-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.result-status {
font-size: 14px;
color: #999;
margin-right: 10px;
}
}
.case-title {
font-weight: 500;
font-size: 14px;
}
.port-info {
font-size: 12px;
color: #999;
background-color: #f0f0f0;
padding: 2px 6px;
border-radius: 3px;
margin-right: 8px;
}
.test-button {
color: #1890ff;
font-size: 12px;
margin-right: 5px;
}
.result-content {
.error-message,
.object-result,
.text-result {
background-color: #f8f8f8;
padding: 8px 10px;
border-radius: 3px;
overflow-x: auto;
}
pre {
margin: 0;
font-size: 12px;
line-height: 1.4;
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
white-space: pre-wrap;
word-wrap: break-word;
}
.test-log {
background-color: #f8f8f8;
padding: 8px 10px;
border-radius: 3px;
overflow-x: auto;
}
}
.no-result {
padding: 12px 0;
text-align: center;
color: #999;
font-size: 12px;
}
</style>

View File

@@ -0,0 +1,33 @@
import { request } from "/@/api/service";
export async function DomainResolve(domain: string) {
return await request({
url: "/sys/nettest/domainResolve",
method: "post",
data: { domain },
});
}
export async function PingTest(domain: string) {
return await request({
url: "/sys/nettest/ping",
method: "post",
data: { domain },
});
}
export async function TelnetTest(domain: string, port: number) {
return await request({
url: "/sys/nettest/telnet",
method: "post",
data: { domain, port },
});
}
// 获取服务器信息包括本地IP、外网IP和DNS服务器
export async function GetServerInfo() {
return await request({
url: "/sys/nettest/serverInfo",
method: "post",
});
}

View File

@@ -0,0 +1,46 @@
<template>
<fs-page class="page-sys-nettest">
<template #header>
<div class="title">
网络测试
<span class="sub">测试您的服务器容器网络连接是否正常</span>
</div>
</template>
<div class="nettest-container">
<!-- 服务端信息 -->
<server-info-card />
<!-- 测试区域 -->
<div class="test-areas flex-wrap md:flex-nowrap">
<!-- 百度域名测试 (用于对比) -->
<domain-test-card class="test-card" :domain="'baidu.com'" :port="443" :auto-start="true" />
<!-- 用户输入域名测试 -->
<domain-test-card class="test-card" :title="'自定义域名测试'" />
</div>
</div>
</fs-page>
</template>
<script lang="ts" setup>
import DomainTestCard from "./DomainTestCard.vue";
import ServerInfoCard from "./ServerInfoCard.vue";
</script>
<style lang="less">
.page-sys-nettest {
.nettest-container {
padding: 16px;
background-color: #fff;
}
.test-areas {
display: flex;
gap: 16px;
margin-top: 16px;
}
.test-card {
min-width: 50%;
}
}
</style>

View File

@@ -79,6 +79,14 @@ export async function SysSettingsSave(data: SysSettings) {
});
}
export async function TestCaptcha(form: any) {
return await request({
url: apiPrefix + "/captchaTest",
method: "post",
data: form,
});
}
export async function TestProxy() {
return await request({
url: apiPrefix + "/testProxy",

View File

@@ -17,6 +17,12 @@
<a-tab-pane key="safe" :tab="t('certd.sys.setting.safeSetting')">
<SettingSafe v-if="activeKey === 'safe'" />
</a-tab-pane>
<a-tab-pane key="captcha" :tab="t('certd.sys.setting.captchaSetting')">
<SettingCaptcha v-if="activeKey === 'captcha'" />
</a-tab-pane>
<a-tab-pane key="pipeline" :tab="t('certd.sys.setting.pipelineSetting')">
<SettingPipeline v-if="activeKey === 'pipeline'" />
</a-tab-pane>
</a-tabs>
</div>
</fs-page>
@@ -27,6 +33,8 @@ import SettingBase from "/@/views/sys/settings/tabs/base.vue";
import SettingRegister from "/@/views/sys/settings/tabs/register.vue";
import SettingPayment from "/@/views/sys/settings/tabs/payment.vue";
import SettingSafe from "/@/views/sys/settings/tabs/safe.vue";
import SettingCaptcha from "/@/views/sys/settings/tabs/captcha.vue";
import SettingPipeline from "/@/views/sys/settings/tabs/pipeline.vue";
import { useRoute, useRouter } from "vue-router";
import { ref } from "vue";
import { useSettingStore } from "/@/store/settings";
@@ -58,7 +66,7 @@ function onChange(value: string) {
<style lang="less">
.page-sys-settings {
.sys-settings-form {
width: 600px;
width: 800px;
max-width: 100%;
padding: 20px;
}

View File

@@ -1,6 +1,6 @@
<template>
<div class="sys-settings-form sys-settings-base">
<a-form :model="formState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish" @finish-failed="onFinishFailed">
<a-form :model="formState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish">
<a-form-item :label="t('certd.icpRegistrationNumber')" :name="['public', 'icpNo']">
<a-input v-model:value="formState.public.icpNo" :placeholder="t('certd.icpPlaceholder')" />
</a-form-item>
@@ -47,18 +47,6 @@
<div class="helper" v-html="t('certd.commonCnameHelper')"></div>
</a-form-item>
<a-form-item :label="t('certd.sys.setting.captchaEnabled')" :name="['public', 'captchaEnabled']">
<a-switch v-model:checked="formState.public.captchaEnabled" />
<div class="helper" v-html="t('certd.sys.setting.captchaHelper')"></div>
</a-form-item>
<a-form-item :label="t('certd.sys.setting.captchaType')" :name="['public', 'captchaAddonId']">
<addon-selector v-model:model-value="formState.public.captchaAddonId" addon-type="captcha" from="sys" @selected-change="onAddonChanged" />
</a-form-item>
<a-form-item :name="['public', 'captchaType']" class="hidden">
<a-input v-model:model-value="formState.public.captchaType"></a-input>
</a-form-item>
<a-form-item label=" " :colon="false" :wrapper-col="{ span: 8 }">
<a-button :loading="saveLoading" type="primary" html-type="submit">{{ t("certd.saveButton") }}</a-button>
</a-form-item>
@@ -76,6 +64,7 @@ import { notification } from "ant-design-vue";
import { util } from "/@/utils";
import { useI18n } from "/src/locales";
import AddonSelector from "../../../certd/addon/addon-selector/index.vue";
import CaptchaInput from "/@/components/captcha/captcha-input.vue";
const { t } = useI18n();
defineOptions({
@@ -106,6 +95,7 @@ const settingsStore = useSettingStore();
const onFinish = async (form: any) => {
try {
saveLoading.value = true;
await api.SysSettingsSave(form);
await settingsStore.loadSysSettings();
notification.success({
@@ -116,21 +106,6 @@ const onFinish = async (form: any) => {
}
};
const onFinishFailed = (errorInfo: any) => {
// console.log("Failed:", errorInfo);
};
async function stopOtherUserTimer() {
await api.stopOtherUserTimer();
notification.success({
message: t("certd.stopSuccess"),
});
}
function onAddonChanged(target: any) {
formState.public.captchaType = target.type;
}
const testProxyLoading = ref(false);
async function testProxy() {
testProxyLoading.value = true;

View File

@@ -0,0 +1,125 @@
<template>
<div class="sys-settings-form sys-settings-base">
<a-form :model="formState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish">
<a-form-item :label="t('certd.sys.setting.captchaEnabled')" :name="['public', 'captchaEnabled']">
<a-switch v-model:checked="formState.public.captchaEnabled" />
<div class="helper" v-html="t('certd.sys.setting.captchaHelper')"></div>
</a-form-item>
<a-form-item :label="t('certd.sys.setting.captchaType')" :name="['public', 'captchaAddonId']">
<addon-selector v-model:model-value="formState.public.captchaAddonId" addon-type="captcha" from="sys" @selected-change="onAddonChanged" />
</a-form-item>
<a-form-item v-if="formState.public.captchaType === settingsStore.sysPublic.captchaType" :label="t('certd.sys.setting.captchaTest')">
<div class="flex">
<CaptchaInput v-model:model-value="captchaTestForm.captcha" class="w-50%"></CaptchaInput>
<a-button class="ml-2" type="primary" @click="doCaptchaValidate">后端验证</a-button>
</div>
</a-form-item>
<a-form-item :name="['public', 'captchaType']" class="hidden">
<a-input v-model:model-value="formState.public.captchaType"></a-input>
</a-form-item>
<a-form-item label=" " :colon="false" :wrapper-col="{ span: 8 }">
<a-button :loading="saveLoading" type="primary" html-type="submit">{{ t("certd.saveButton") }}</a-button>
</a-form-item>
</a-form>
</div>
</template>
<script setup lang="tsx">
import { reactive, ref } from "vue";
import { SysSettings } from "/@/views/sys/settings/api";
import * as api from "/@/views/sys/settings/api";
import { merge } from "lodash-es";
import { useSettingStore } from "/@/store/settings";
import { notification } from "ant-design-vue";
import { util } from "/@/utils";
import { useI18n } from "/src/locales";
import AddonSelector from "../../../certd/addon/addon-selector/index.vue";
import CaptchaInput from "/@/components/captcha/captcha-input.vue";
const { t } = useI18n();
defineOptions({
name: "SettingCaptcha",
});
const captchaTestForm = reactive({
captcha: null,
pass: false,
});
async function doCaptchaValidate() {
if (!captchaTestForm.captcha) {
notification.error({
message: "请进行验证码验证",
});
return;
}
await api.TestCaptcha(captchaTestForm.captcha);
notification.success({
message: "校验通过",
});
captchaTestForm.pass = true;
}
const formState = reactive<Partial<SysSettings>>({
public: {
icpNo: "",
mpsNo: "",
},
private: {},
});
async function loadSysSettings() {
const data: any = await api.SysSettingsGet();
merge(formState, data);
}
const saveLoading = ref(false);
loadSysSettings();
const settingsStore = useSettingStore();
const onFinish = async (form: any) => {
try {
saveLoading.value = true;
if (form.public.captchaEnabled && !captchaTestForm.pass) {
if (form.public.captchaType === settingsStore.sysPublic.captchaType) {
notification.error({
message: "您正在开启登录验证码,请先通过验证码测试,后端校验成功后才能保存",
});
} else {
notification.error({
message: "您正在开启登录验证码,请先关闭登录验证码开关,保存,然后会显示验证码,进行验证码测试,后端校验成功,之后再开启登录验证码,并保存",
});
}
return;
}
await api.SysSettingsSave(form);
await settingsStore.loadSysSettings();
notification.success({
message: t("certd.saveSuccess"),
});
} catch (e) {
console.error(e);
clearValidState();
} finally {
saveLoading.value = false;
}
};
function clearValidState() {
captchaTestForm.pass = false;
captchaTestForm.captcha = null;
}
function onAddonChanged(target: any) {
formState.public.captchaType = target.type;
clearValidState();
}
</script>
<style lang="less">
.sys-settings-base {
}
</style>

View File

@@ -0,0 +1,75 @@
<template>
<div class="sys-settings-form sys-settings-pipeline">
<a-form :model="formState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish">
<a-form-item :label="t('certd.manageOtherUserPipeline')" :name="['public', 'managerOtherUserPipeline']">
<a-switch v-model:checked="formState.public.managerOtherUserPipeline" />
</a-form-item>
<a-form-item :label="t('certd.limitUserPipelineCount')" :name="['public', 'limitUserPipelineCount']">
<a-input-number v-model:value="formState.public.limitUserPipelineCount" />
<div class="helper">{{ t("certd.limitUserPipelineCountHelper") }}</div>
</a-form-item>
<a-form-item :label="t('certd.sys.setting.pipelineValidTimeEnabled')" :name="['public', 'pipelineValidTimeEnabled']">
<div class="flex items-center">
<a-switch v-model:checked="formState.public.pipelineValidTimeEnabled" :disabled="!settingsStore.isPlus" />
<vip-button class="ml-5" mode="button"></vip-button>
</div>
<div class="helper">{{ t("certd.sys.setting.pipelineValidTimeEnabledHelper") }}</div>
</a-form-item>
<a-form-item :label="t('certd.sys.setting.certDomainAddToMonitorEnabled')" :name="['public', 'certDomainAddToMonitorEnabled']">
<div class="flex items-center">
<a-switch v-model:checked="formState.public.certDomainAddToMonitorEnabled" :disabled="!settingsStore.isPlus" />
<vip-button class="ml-5" mode="button"></vip-button>
</div>
<div class="helper">{{ t("certd.sys.setting.certDomainAddToMonitorEnabledHelper") }}</div>
</a-form-item>
<a-form-item label=" " :colon="false" :wrapper-col="{ span: 8 }">
<a-button :loading="saveLoading" type="primary" html-type="submit">{{ t("certd.saveButton") }}</a-button>
</a-form-item>
</a-form>
</div>
</template>
<script setup lang="tsx">
import { reactive, ref } from "vue";
import { SysSettings } from "/@/views/sys/settings/api";
import * as api from "/@/views/sys/settings/api";
import { merge } from "lodash-es";
import { useSettingStore } from "/@/store/settings";
import { notification } from "ant-design-vue";
import { useI18n } from "/src/locales";
const { t } = useI18n();
defineOptions({
name: "SettingPipeline",
});
const formState = reactive<Partial<SysSettings>>({
public: {},
private: {},
});
async function loadSysSettings() {
const data: any = await api.SysSettingsGet();
merge(formState, data);
}
const saveLoading = ref(false);
loadSysSettings();
const settingsStore = useSettingStore();
const onFinish = async (form: any) => {
try {
saveLoading.value = true;
await api.SysSettingsSave(form);
await settingsStore.loadSysSettings();
notification.success({
message: t("certd.saveSuccess"),
});
} finally {
saveLoading.value = false;
}
};
</script>
<style lang="less"></style>

View File

@@ -1,13 +1,6 @@
<template>
<div class="sys-settings-form sys-settings-register">
<a-form :model="formState" name="register" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish">
<a-form-item :label="t('certd.manageOtherUserPipeline')" :name="['public', 'managerOtherUserPipeline']">
<a-switch v-model:checked="formState.public.managerOtherUserPipeline" />
</a-form-item>
<a-form-item :label="t('certd.limitUserPipelineCount')" :name="['public', 'limitUserPipelineCount']">
<a-input-number v-model:value="formState.public.limitUserPipelineCount" />
<div class="helper">{{ t("certd.limitUserPipelineCountHelper") }}</div>
</a-form-item>
<a-form-item :label="t('certd.enableSelfRegistration')" :name="['public', 'registerEnabled']">
<a-switch v-model:checked="formState.public.registerEnabled" />
</a-form-item>

View File

@@ -55,15 +55,15 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
groups: {
base: {
header: t("certd.basicInfo"),
columns: ["title", "type", "disabled", "order", "supportBuy", "intro"]
columns: ["title", "type", "disabled", "order", "supportBuy", "intro"],
},
content: {
header: t("certd.packageContent"),
columns: ["content.maxDomainCount", "content.maxPipelineCount", "content.maxDeployCount", "content.maxMonitorCount"]
columns: ["content.maxDomainCount", "content.maxPipelineCount", "content.maxDeployCount", "content.maxMonitorCount"],
},
price: {
header: t("certd.price"),
columns: ["durationPrices"]
columns: ["durationPrices"],
},
},
},

View File

@@ -41,7 +41,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
//这里使用行选择插件生成行选择crudOptions配置最终会与crudOptions合并
rowSelection: {
enabled: true,
order: -2,
order: -99,
before: true,
// handle: (pluginProps,useCrudProps)=>CrudOptions,
props: {

View File

@@ -84,6 +84,7 @@ export default ({ command, mode }) => {
host: "0.0.0.0",
port: 3008,
fs: devServerFs,
allowedHosts: ["localhost", "127.0.0.1", "yfy.docmirror.cn"],
proxy: {
// with options
"/api": {

View File

@@ -23,7 +23,6 @@ typeorm:
database: './data/db-plus-dev.sqlite'
# plus server: 'http://127.0.0.1:11007'
account:
server:
baseUrl: 'http://localhost:1017/subject'

View File

@@ -3,6 +3,84 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.37.4](https://github.com/certd/certd/compare/v1.37.3...v1.37.4) (2025-10-28)
### Performance Improvements
* 优化数据备份效率,流式写入文件 ([c38dbbb](https://github.com/certd/certd/commit/c38dbbb1d72bd00a92fe275b76aea82a791e7199))
## [1.37.3](https://github.com/certd/certd/compare/v1.37.2...v1.37.3) (2025-10-24)
### Bug Fixes
* 修复网络测试telnet的bug ([c03a70f](https://github.com/certd/certd/commit/c03a70fde23c8e840bd0fdb4fcbca8990f6c65eb))
* 修复站点证书监控证书已经更新到最新日期了仍然发出警告通知的bug ([1f42f93](https://github.com/certd/certd/commit/1f42f933f07860b27aa3d016e40916ff2b063eac))
### Performance Improvements
* 注册页面增加手机注册tab页签 ([6b2f1fc](https://github.com/certd/certd/commit/6b2f1fcd3e058061b814c3331cda8ce1b2d80d73))
* 流水线创建时支持添加到证书监控 ([59ba408](https://github.com/certd/certd/commit/59ba4080706548828ef1c0a9cd893c1c9a7d591f))
* 流水线支持有效期设置 ([911e69e](https://github.com/certd/certd/commit/911e69e3bc0cdd48b62953b5d0981d640fc1f8ac))
* 通知支持meow ([c77645e](https://github.com/certd/certd/commit/c77645e1733670214aaca5544cf8759d7e4adda4))
* 站点证书监控增加导出和分组功能 ([2ed12c4](https://github.com/certd/certd/commit/2ed12c429eb58274a4f9dd0ed3b66e160d283ded))
* 证书监控增加批量删除 ([e578c52](https://github.com/certd/certd/commit/e578c52fdf2f838038062aa4209b655fbae461fb))
* esa 自动删除过期证书提示 ([8bf1f82](https://github.com/certd/certd/commit/8bf1f828b9eaa9208f32e8ee7460b86420fed0c7))
* ssh 增加禁止-i参数提示 ([3a8931f](https://github.com/certd/certd/commit/3a8931feeffd7157163ff7d46b693e5e1a434b9c))
## [1.37.2](https://github.com/certd/certd/compare/v1.37.1...v1.37.2) (2025-10-14)
### Bug Fixes
* 修复飞牛证书部署后无法生效的bug ([bf156a1](https://github.com/certd/certd/commit/bf156a13bd443cdadb73c9dff79bbef7231b4401))
* aliyunoss 选择证书接入点选择新加坡无法上传的bug ([e00733a](https://github.com/certd/certd/commit/e00733a34644c23ffe926486b15dc96bf2fa4b57))
### Performance Improvements
* 增加飞牛证书id选择的提示 ([5a4d812](https://github.com/certd/certd/commit/5a4d8121462b1afe921d028465687be8c9679814))
* 证书监控支持设置证书即将过期天数 ([cd35568](https://github.com/certd/certd/commit/cd35568e042e6ab928685efad51cdbed823d2d4f))
* 支持网络测试 ([2bef608](https://github.com/certd/certd/commit/2bef608e07ceb56d52007f290667e0afef401b22))
* 支持新网代理方式 ([f612509](https://github.com/certd/certd/commit/f612509cac87b859e81a7a52fe94b2eaccad22f9))
* dns支持新网互联 ([f415190](https://github.com/certd/certd/commit/f41519048326d971acd9e0a30462231f77a299a6))
## [1.37.1](https://github.com/certd/certd/compare/v1.37.0...v1.37.1) (2025-09-29)
### Bug Fixes
* 修复某些情况下cname申请证书报错主域名不一致的bug ([2671781](https://github.com/certd/certd/commit/2671781e1bb0838981728d85eacf0e1a25a0fa48))
### Performance Improvements
* dns解析支持阿里esa ([9291fa6](https://github.com/certd/certd/commit/9291fa68aa7a88a05c2f888bf3048df36a8fbde3))
# [1.37.0](https://github.com/certd/certd/compare/v1.36.25...v1.37.0) (2025-09-28)
### Features
* dist打包前检查 ([8f6e5bd](https://github.com/certd/certd/commit/8f6e5bd24b3b65fbfcba36c08f532a3abad2d606))
## [1.36.25](https://github.com/certd/certd/compare/v1.36.24...v1.36.25) (2025-09-27)
### Bug Fixes
* 固定midwayjs版本修复ui-server import 错误的bug ([eb4d125](https://github.com/certd/certd/commit/eb4d125eaf4a41e88c752d0c68993829589f8f27))
## [1.36.24](https://github.com/certd/certd/compare/v1.36.23...v1.36.24) (2025-09-27)
### Bug Fixes
* 修复 ui-server 加载失败问题 ([c2ccdbe](https://github.com/certd/certd/commit/c2ccdbec9dd08bca4688eeb2f34d0105eec43ba1))
### Performance Improvements
* 重置管理员密码同时会关闭验证码,防止验证码失效之后无法登录 ([03899d4](https://github.com/certd/certd/commit/03899d4d9c76fc2077dacc53ab88e2c9ca41af7c))
## [1.36.23](https://github.com/certd/certd/compare/v1.36.22...v1.36.23) (2025-09-26)
### Performance Improvements
* 开启子域名托管之后cname记录支持重置 ([54c8d62](https://github.com/certd/certd/commit/54c8d622437761d350db0f17e07f7517f1911211))
* 验证码支持测试,登录验证码需要测试通过后才能开启 ([83e6476](https://github.com/certd/certd/commit/83e6476408090b741fabb1b542fb458d9a8b4134))
## [1.36.22](https://github.com/certd/certd/compare/v1.36.21...v1.36.22) (2025-09-23)
### Performance Improvements

View File

@@ -5,9 +5,8 @@ const { Bootstrap } = require('@midwayjs/bootstrap');
const DirectoryFileDetector = require('@midwayjs/core').DirectoryFileDetector;
const baseDir = process.cwd();
const pipelineDir = baseDir + './node_modules/@certd/pipeline/dist';
const customFileDetector = new DirectoryFileDetector({
loadDir: [baseDir, pipelineDir],
loadDir: [baseDir],
});
module.exports = async () => {

View File

@@ -0,0 +1 @@
ALTER TABLE cd_cname_record ADD COLUMN `main_domain` varchar(100);

View File

@@ -0,0 +1,17 @@
ALTER TABLE cd_site_info ADD COLUMN `remark` varchar(512);
CREATE TABLE `cd_group`
(
`id` bigint PRIMARY KEY AUTO_INCREMENT NOT NULL,
`user_id` bigint NOT NULL,
`name` varchar(100) NOT NULL,
`icon` varchar(100),
`favorite` boolean NOT NULL DEFAULT false,
`type` varchar(512),
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
);
ALTER TABLE cd_site_info ADD COLUMN `group_id` bigint;
ALTER TABLE pi_pipeline ADD COLUMN `valid_time` bigint;

View File

@@ -0,0 +1 @@
ALTER TABLE cd_cname_record ADD COLUMN "main_domain" varchar(100);

View File

@@ -0,0 +1,17 @@
ALTER TABLE cd_site_info ADD COLUMN "remark" varchar(512);
CREATE TABLE "cd_group"
(
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY NOT NULL,
"user_id" bigint NOT NULL,
"name" varchar(100) NOT NULL,
"icon" varchar(100),
"favorite" boolean NOT NULL DEFAULT (false),
"type" varchar(512),
"create_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP),
"update_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP)
);
ALTER TABLE cd_site_info ADD COLUMN "group_id" bigint;
ALTER TABLE pi_pipeline ADD COLUMN "valid_time" bigint;

View File

@@ -0,0 +1 @@
ALTER TABLE cd_cname_record ADD COLUMN "main_domain" varchar(100);

View File

@@ -0,0 +1,17 @@
ALTER TABLE cd_site_info ADD COLUMN "remark" varchar(512);
CREATE TABLE "cd_group"
(
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"user_id" integer NOT NULL,
"name" varchar(100) NOT NULL,
"icon" varchar(100),
"favorite" boolean NOT NULL DEFAULT (false),
"type" varchar(512),
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
);
ALTER TABLE cd_site_info ADD COLUMN "group_id" integer;
ALTER TABLE pi_pipeline ADD COLUMN "valid_time" integer;

View File

@@ -1,12 +1,14 @@
{
"name": "@certd/ui-server",
"version": "1.36.22",
"version": "1.37.4",
"description": "fast-server base midway",
"private": true,
"type": "module",
"scripts": {
"start": "cross-env NODE_ENV=production node ./bootstrap.js",
"dev": "cross-env NODE_ENV=local mwtsc --watch --run @midwayjs/mock/app",
"dev-start": "mwtsc --watch --run @midwayjs/mock/app",
"dc": "cd ../../../ && pnpm run dev",
"dev": "cross-env NODE_ENV=local & pnpm run dev-start",
"dev-commlocal": "cross-env NODE_ENV=dev-commlocal mwtsc --watch --run @midwayjs/mock/app",
"dev-commpro": "cross-env NODE_ENV=dev-commpro mwtsc --watch --run @midwayjs/mock/app",
"dev-pg": "cross-env NODE_ENV=dev-pg mwtsc --watch --run @midwayjs/mock/app",
@@ -43,34 +45,34 @@
"@aws-sdk/client-cloudfront": "^3.699.0",
"@aws-sdk/client-iam": "^3.699.0",
"@aws-sdk/client-s3": "^3.705.0",
"@certd/acme-client": "^1.36.22",
"@certd/basic": "^1.36.22",
"@certd/commercial-core": "^1.36.22",
"@certd/acme-client": "^1.37.4",
"@certd/basic": "^1.37.4",
"@certd/commercial-core": "^1.37.4",
"@certd/cv4pve-api-javascript": "^8.4.2",
"@certd/jdcloud": "^1.36.22",
"@certd/lib-huawei": "^1.36.22",
"@certd/lib-k8s": "^1.36.22",
"@certd/lib-server": "^1.36.22",
"@certd/midway-flyway-js": "^1.36.22",
"@certd/pipeline": "^1.36.22",
"@certd/plugin-cert": "^1.36.22",
"@certd/plugin-lib": "^1.36.22",
"@certd/plugin-plus": "^1.36.22",
"@certd/plus-core": "^1.36.22",
"@certd/jdcloud": "^1.37.4",
"@certd/lib-huawei": "^1.37.4",
"@certd/lib-k8s": "^1.37.4",
"@certd/lib-server": "^1.37.4",
"@certd/midway-flyway-js": "^1.37.4",
"@certd/pipeline": "^1.37.4",
"@certd/plugin-cert": "^1.37.4",
"@certd/plugin-lib": "^1.37.4",
"@certd/plugin-plus": "^1.37.4",
"@certd/plus-core": "^1.37.4",
"@huaweicloud/huaweicloud-sdk-cdn": "^3.1.120",
"@huaweicloud/huaweicloud-sdk-core": "^3.1.120",
"@koa/cors": "^5.0.0",
"@midwayjs/bootstrap": "~3.20.3",
"@midwayjs/cache": "~3.14.0",
"@midwayjs/core": "~3.20.3",
"@midwayjs/i18n": "~3.20.3",
"@midwayjs/info": "~3.20.3",
"@midwayjs/koa": "~3.20.3",
"@midwayjs/logger": "~3.4.2",
"@midwayjs/static-file": "~3.20.3",
"@midwayjs/typeorm": "~3.20.3",
"@midwayjs/upload": "~3.20.3",
"@midwayjs/validate": "~3.20.3",
"@midwayjs/bootstrap": "3.20.11",
"@midwayjs/cache": "3.14.0",
"@midwayjs/core": "3.20.11",
"@midwayjs/i18n": "3.20.13",
"@midwayjs/info": "3.20.13",
"@midwayjs/koa": "3.20.13",
"@midwayjs/logger": "3.4.2",
"@midwayjs/static-file": "3.20.13",
"@midwayjs/typeorm": "3.20.11",
"@midwayjs/upload": "3.20.13",
"@midwayjs/validate": "3.20.13",
"@volcengine/openapi": "^1.28.1",
"ali-oss": "^6.21.0",
"axios": "^1.7.2",
@@ -93,7 +95,6 @@
"jsonwebtoken": "^9.0.0",
"jszip": "^3.10.1",
"koa-send": "^5.0.1",
"kubernetes-client": "^9.0.0",
"lodash-es": "^4.17.21",
"log4js": "^6.7.1",
"lru-cache": "^11.0.1",
@@ -120,10 +121,11 @@
"svg-captcha": "^1.4.0",
"tencentcloud-sdk-nodejs": "^4.1.112",
"typeorm": "^0.3.20",
"uuid": "^10.0.0"
"uuid": "^10.0.0",
"xml2js": "^0.6.2"
},
"devDependencies": {
"@midwayjs/mock": "~3.20.3",
"@midwayjs/mock": "3.20.11",
"@types/ali-oss": "^6.16.11",
"@types/cache-manager": "^4.0.6",
"@types/jest": "^29.5.13",
@@ -142,7 +144,7 @@
"why-is-node-running": "^3.2.2"
},
"engines": {
"node": ">=18.0.0"
"node": ">=20.0.0"
},
"repository": {
"type": "git",

View File

@@ -27,7 +27,7 @@ const development = {
},
keys: 'certd',
koa: {
hostname:"::",
hostname: "::",
port: 7001,
},
https: {

View File

@@ -20,9 +20,13 @@ import * as commercial from '@certd/commercial-core';
import * as upload from '@midwayjs/upload';
import { setLogger } from '@certd/acme-client';
import {HiddenMiddleware} from "./middleware/hidden.js";
process.on('uncaughtException', error => {
console.error('未捕获的异常:', error);
// 在这里可以添加日志记录、发送错误通知等操作
if(error?.message?.includes('address family not supported')){
logger.error("您的服务器不支持监听IPV6格式的地址::),请配置环境变量: certd_koa_hostname=0.0.0.0");
}
});
@Configuration({
@@ -107,5 +111,6 @@ export class MainConfiguration {
});
logger.info('当前环境:', this.app.getEnv()); // prod
// throw new Error("address family not supported")
}
}

View File

@@ -1,7 +1,6 @@
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
import { AccessService, Constants } from '@certd/lib-server';
import { AccessController } from '../../user/pipeline/access-controller.js';
import { checkComm } from '@certd/plus-core';
import { ALL, Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/core";
import { AccessService, Constants } from "@certd/lib-server";
import { AccessController } from "../../user/pipeline/access-controller.js";
/**
* 授权
@@ -17,7 +16,7 @@ export class SysAccessController extends AccessController {
}
getUserId() {
checkComm();
// checkComm();
return 0;
}

View File

@@ -0,0 +1,47 @@
import { BaseController } from '@certd/lib-server';
import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
import { NetTestService } from '../../../modules/sys/nettest/nettest-service.js';
@Provide()
@Controller('/api/sys/nettest/')
export class SysNetTestController extends BaseController {
@Inject()
netTestService: NetTestService;
@Post('/domainResolve', { summary: 'sys:settings:view' })
public async domainResolve(@Body(ALL) body: { domain: string }) {
const { domain } = body;
const result = await this.netTestService.domainResolve(domain);
return this.ok(result);
}
// ping
@Post('/ping', { summary: 'sys:settings:view' })
public async ping(@Body(ALL) body: { domain: string }) {
const { domain } = body;
const result = await this.netTestService.ping(domain);
return this.ok(result);
}
// telnet
@Post('/telnet', { summary: 'sys:settings:view' })
public async telnet(@Body(ALL) body: { domain: string, port: number }) {
const { domain, port } = body;
const result = await this.netTestService.telnet(domain, port);
return this.ok(result);
}
// telnet
@Post('/serverInfo', { summary: 'sys:settings:view' })
public async serverInfo() {
const result = await this.netTestService.serverInfo();
return this.ok(result);
}
}

View File

@@ -192,4 +192,11 @@ export class SysSettingsController extends CrudController<SysSettingsService> {
await this.service.saveSetting(blankSetting);
return this.ok({});
}
@Post("/captchaTest", { summary: "sys:settings:edit" })
async captchaTest(@Body(ALL) body: any) {
await this.codeService.checkCaptcha(body)
return this.ok({});
}
}

View File

@@ -11,56 +11,59 @@ import {
import { AuthService } from "../../../modules/sys/authority/service/auth-service.js";
import { checkPlus } from "@certd/plus-core";
import { http, logger, utils } from "@certd/basic";
import { TaskServiceBuilder } from "../../../modules/pipeline/service/getter/task-service-getter.js";
/**
* Addon
*/
@Provide()
@Controller('/api/addon')
@Controller("/api/addon")
export class AddonController extends CrudController<AddonService> {
@Inject()
service: AddonService;
@Inject()
authService: AuthService;
@Inject()
taskServiceBuilder:TaskServiceBuilder
getService(): AddonService {
return this.service;
}
@Post('/page', { summary: Constants.per.authOnly })
@Post("/page", { summary: Constants.per.authOnly })
async page(@Body(ALL) body) {
body.query = body.query ?? {};
delete body.query.userId;
const buildQuery = qb => {
qb.andWhere('user_id = :userId', { userId: this.getUserId() });
qb.andWhere("user_id = :userId", { userId: this.getUserId() });
};
const res = await this.service.page({
query: body.query,
page: body.page,
sort: body.sort,
buildQuery,
buildQuery
});
return this.ok(res);
}
@Post('/list', { summary: Constants.per.authOnly })
@Post("/list", { summary: Constants.per.authOnly })
async list(@Body(ALL) body) {
body.query = body.query ?? {};
body.query.userId = this.getUserId();
return super.list(body);
}
@Post('/add', { summary: Constants.per.authOnly })
@Post("/add", { summary: Constants.per.authOnly })
async add(@Body(ALL) bean) {
bean.userId = this.getUserId();
const type = bean.type;
const addonType = bean.addonType;
if (! type || !addonType){
throw new ValidateException('请选择Addon类型');
if (!type || !addonType) {
throw new ValidateException("请选择Addon类型");
}
const define: AddonDefine = this.service.getDefineByType(type,addonType);
const define: AddonDefine = this.service.getDefineByType(type, addonType);
if (!define) {
throw new ValidateException('Addon类型不存在');
throw new ValidateException("Addon类型不存在");
}
if (define.needPlus) {
checkPlus();
@@ -68,19 +71,19 @@ export class AddonController extends CrudController<AddonService> {
return super.add(bean);
}
@Post('/update', { summary: Constants.per.authOnly })
@Post("/update", { summary: Constants.per.authOnly })
async update(@Body(ALL) bean) {
await this.service.checkUserId(bean.id, this.getUserId());
const old = await this.service.info(bean.id);
if (!old) {
throw new ValidateException('Addon配置不存在');
throw new ValidateException("Addon配置不存在");
}
if (old.type !== bean.type ) {
if (old.type !== bean.type) {
const addonType = old.type;
const type = bean.type;
const define: AddonDefine = this.service.getDefineByType(type,addonType);
const define: AddonDefine = this.service.getDefineByType(type, addonType);
if (!define) {
throw new ValidateException('Addon类型不存在');
throw new ValidateException("Addon类型不存在");
}
if (define.needPlus) {
checkPlus();
@@ -89,26 +92,27 @@ export class AddonController extends CrudController<AddonService> {
delete bean.userId;
return super.update(bean);
}
@Post('/info', { summary: Constants.per.authOnly })
async info(@Query('id') id: number) {
@Post("/info", { summary: Constants.per.authOnly })
async info(@Query("id") id: number) {
await this.service.checkUserId(id, this.getUserId());
return super.info(id);
}
@Post('/delete', { summary: Constants.per.authOnly })
async delete(@Query('id') id: number) {
@Post("/delete", { summary: Constants.per.authOnly })
async delete(@Query("id") id: number) {
await this.service.checkUserId(id, this.getUserId());
return super.delete(id);
}
@Post('/define', { summary: Constants.per.authOnly })
async define(@Query('type') type: string,@Query('addonType') addonType: string) {
const notification = this.service.getDefineByType(type,addonType);
@Post("/define", { summary: Constants.per.authOnly })
async define(@Query("type") type: string, @Query("addonType") addonType: string) {
const notification = this.service.getDefineByType(type, addonType);
return this.ok(notification);
}
@Post('/getTypeDict', { summary: Constants.per.authOnly })
async getTypeDict(@Query('addonType') addonType: string) {
@Post("/getTypeDict", { summary: Constants.per.authOnly })
async getTypeDict(@Query("addonType") addonType: string) {
const list: any = this.service.getDefineList(addonType);
let dict = [];
for (const item of list) {
@@ -116,7 +120,7 @@ export class AddonController extends CrudController<AddonService> {
value: item.name,
label: item.title,
needPlus: item.needPlus ?? false,
icon: item.icon,
icon: item.icon
});
}
dict = dict.sort(a => {
@@ -125,13 +129,13 @@ export class AddonController extends CrudController<AddonService> {
return this.ok(dict);
}
@Post('/simpleInfo', { summary: Constants.per.authOnly })
async simpleInfo(@Query('addonType') addonType: string,@Query('id') id: number) {
@Post("/simpleInfo", { summary: Constants.per.authOnly })
async simpleInfo(@Query("addonType") addonType: string, @Query("id") id: number) {
if (id === 0) {
//获取默认
const res = await this.service.getDefault(this.getUserId(),addonType);
const res = await this.service.getDefault(this.getUserId(), addonType);
if (!res) {
throw new ValidateException('默认Addon配置不存在');
throw new ValidateException("默认Addon配置不存在");
}
const simple = await this.service.getSimpleInfo(res.id);
return this.ok(simple);
@@ -141,27 +145,27 @@ export class AddonController extends CrudController<AddonService> {
return this.ok(res);
}
@Post('/getDefaultId', { summary: Constants.per.authOnly })
async getDefaultId(@Query('addonType') addonType: string) {
const res = await this.service.getDefault(this.getUserId(),addonType);
@Post("/getDefaultId", { summary: Constants.per.authOnly })
async getDefaultId(@Query("addonType") addonType: string) {
const res = await this.service.getDefault(this.getUserId(), addonType);
return this.ok(res?.id);
}
@Post('/setDefault', { summary: Constants.per.authOnly })
async setDefault(@Query('addonType') addonType: string,@Query('id') id: number) {
@Post("/setDefault", { summary: Constants.per.authOnly })
async setDefault(@Query("addonType") addonType: string, @Query("id") id: number) {
await this.service.checkUserId(id, this.getUserId());
const res = await this.service.setDefault(id, this.getUserId(),addonType);
const res = await this.service.setDefault(id, this.getUserId(), addonType);
return this.ok(res);
}
@Post('/options', { summary: Constants.per.authOnly })
async options(@Query('addonType') addonType: string) {
@Post("/options", { summary: Constants.per.authOnly })
async options(@Query("addonType") addonType: string) {
const res = await this.service.list({
query: {
userId: this.getUserId(),
addonType
},
}
});
for (const item of res) {
delete item.setting;
@@ -170,7 +174,7 @@ export class AddonController extends CrudController<AddonService> {
}
@Post('/handle', { summary: Constants.per.authOnly })
@Post("/handle", { summary: Constants.per.authOnly })
async handle(@Body(ALL) body: AddonRequestHandleReq) {
const userId = this.getUserId();
let inputAddon = body.input.addon;
@@ -178,21 +182,24 @@ export class AddonController extends CrudController<AddonService> {
const oldEntity = await this.service.info(body.input.id);
if (oldEntity) {
if (oldEntity.userId !== userId) {
throw new Error('addon not found');
throw new Error("addon not found");
}
// const param: any = {
// type: body.typeName,
// setting: JSON.stringify(body.input.access),
// };
inputAddon = JSON.parse( oldEntity.setting)
inputAddon = JSON.parse(oldEntity.setting);
}
}
const serviceGetter = this.taskServiceBuilder.create({ userId });
const ctx = {
http: http,
logger:logger,
utils:utils,
}
const addon = await newAddon(body.addonType,body.typeName, inputAddon,ctx);
logger: logger,
utils: utils,
serviceGetter
};
const addon = await newAddon(body.addonType, body.typeName, inputAddon, ctx);
const res = await addon.onRequest(body);
return this.ok(res);
}

View File

@@ -0,0 +1,78 @@
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
import { Constants, CrudController } from '@certd/lib-server';
import { AuthService } from '../../../modules/sys/authority/service/auth-service.js';
import { GroupService } from '../../../modules/basic/service/group-service.js';
/**
* 通知
*/
@Provide()
@Controller('/api/basic/group')
export class GroupController extends CrudController<GroupService> {
@Inject()
service: GroupService;
@Inject()
authService: AuthService;
getService(): GroupService {
return this.service;
}
@Post('/page', { summary: Constants.per.authOnly })
async page(@Body(ALL) body: any) {
body.query = body.query ?? {};
delete body.query.userId;
const buildQuery = qb => {
qb.andWhere('user_id = :userId', { userId: this.getUserId() });
};
const res = await this.service.page({
query: body.query,
page: body.page,
sort: body.sort,
buildQuery,
});
return this.ok(res);
}
@Post('/list', { summary: Constants.per.authOnly })
async list(@Body(ALL) body: any) {
body.query = body.query ?? {};
body.query.userId = this.getUserId();
return await super.list(body);
}
@Post('/add', { summary: Constants.per.authOnly })
async add(@Body(ALL) bean: any) {
bean.userId = this.getUserId();
return await super.add(bean);
}
@Post('/update', { summary: Constants.per.authOnly })
async update(@Body(ALL) bean) {
await this.service.checkUserId(bean.id, this.getUserId());
delete bean.userId;
return await super.update(bean);
}
@Post('/info', { summary: Constants.per.authOnly })
async info(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
return await super.info(id);
}
@Post('/delete', { summary: Constants.per.authOnly })
async delete(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
return await super.delete(id);
}
@Post('/all', { summary: Constants.per.authOnly })
async all(@Query('type') type: string) {
const list: any = await this.service.find({
where: {
userId: this.getUserId(),
type,
},
});
return this.ok(list);
}
}

View File

@@ -85,10 +85,18 @@ export class CnameRecordController extends CrudController<CnameRecordService> {
}
@Post('/verify', { summary: Constants.per.authOnly })
async verify(@Body(ALL) body: { id: string }) {
async verify(@Body(ALL) body: { id: number }) {
const userId = this.getUserId();
await this.service.checkUserId(body.id, userId);
const res = await this.service.verify(body.id);
return this.ok(res);
}
@Post('/resetStatus', { summary: Constants.per.authOnly })
async resetStatus(@Body(ALL) body: { id: number }) {
const userId = this.getUserId();
await this.service.checkUserId(body.id, userId);
const res = await this.service.resetStatus(body.id);
return this.ok(res);
}
}

View File

@@ -92,6 +92,14 @@ export class SiteInfoController extends CrudController<SiteInfoService> {
return await super.delete(id);
}
@Post('/batchDelete', { summary: Constants.per.authOnly })
async batchDelete(@Body(ALL) body: any) {
const userId = this.getUserId();
await this.service.batchDelete(body.ids,userId);
return this.ok();
}
@Post('/check', { summary: Constants.per.authOnly })
async check(@Body('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
@@ -111,6 +119,7 @@ export class SiteInfoController extends CrudController<SiteInfoService> {
const userId = this.getUserId();
await this.service.doImport({
text:body.text,
groupId:body.groupId,
userId
})
return this.ok();

View File

@@ -107,6 +107,9 @@ export class NotificationController extends CrudController<NotificationService>
icon: item.icon,
});
}
dict = dict.sort(a => {
return a.order ? 0 : -1;
});
dict = dict.sort(a => {
return a.needPlus ? 0 : -1;
});

View File

@@ -4,6 +4,8 @@ import { PipelineService } from '../../../modules/pipeline/service/pipeline-serv
import { PipelineEntity } from '../../../modules/pipeline/entity/pipeline.js';
import { HistoryService } from '../../../modules/pipeline/service/history-service.js';
import { AuthService } from '../../../modules/sys/authority/service/auth-service.js';
import { SiteInfoService } from '../../../modules/monitor/index.js';
import { isPlus } from '@certd/plus-core';
/**
* 证书
@@ -20,6 +22,9 @@ export class PipelineController extends CrudController<PipelineService> {
@Inject()
sysSettingsService: SysSettingsService;
@Inject()
siteInfoService: SiteInfoService;
getService() {
return this.service;
}
@@ -74,13 +79,30 @@ export class PipelineController extends CrudController<PipelineService> {
}
@Post('/save', { summary: Constants.per.authOnly })
async save(@Body(ALL) bean: PipelineEntity) {
async save(@Body(ALL) bean: {addToMonitorEnabled: boolean, addToMonitorDomains: string} & PipelineEntity) {
if (bean.id > 0) {
await this.authService.checkEntityUserId(this.ctx, this.getService(), bean.id);
} else {
bean.userId = this.getUserId();
}
if(!this.isAdmin()){
// 非管理员用户 不允许设置流水线有效期
delete bean.validTime
}
await this.service.save(bean);
//是否增加证书监控
if (bean.addToMonitorEnabled && bean.addToMonitorDomains) {
const sysPublicSettings = await this.sysSettingsService.getPublicSettings();
if (isPlus() && sysPublicSettings.certDomainAddToMonitorEnabled) {
//增加证书监控
await this.siteInfoService.doImport({
text: bean.addToMonitorDomains,
userId: this.getUserId(),
});
}
}
return this.ok(bean.id);
}
@@ -101,7 +123,7 @@ export class PipelineController extends CrudController<PipelineService> {
@Post('/trigger', { summary: Constants.per.authOnly })
async trigger(@Query('id') id: number, @Query('stepId') stepId?: string) {
await this.authService.checkEntityUserId(this.ctx, this.getService(), id);
await this.service.trigger(id, stepId);
await this.service.trigger(id, stepId,true);
return this.ok({});
}

View File

@@ -1,7 +1,7 @@
import {ALL, Body, Controller, Inject, Post, Provide, Query} from '@midwayjs/core';
import {Constants, CrudController} from '@certd/lib-server';
import {SubDomainService} from "../../../modules/pipeline/service/sub-domain-service.js";
import {DomainParser} from '@certd/plugin-cert/dist/dns-provider/domain-parser.js';
import {DomainParser} from '@certd/plugin-cert';
import { SubDomainsGetter } from '../../../modules/pipeline/service/getter/sub-domain-getter.js';
/**

View File

@@ -1,6 +1,6 @@
import { Autoload, Config, Init, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { IMidwayKoaContext, IWebMiddleware, NextFunction } from '@midwayjs/koa';
import { CommonException } from '@certd/lib-server';
import { CommonException, SysSettingsService } from "@certd/lib-server";
import { UserService } from '../../modules/sys/authority/service/user-service.js';
import { logger } from '@certd/basic';
import {UserSettingsService} from "../../modules/mine/service/user-settings-service.js";
@@ -17,6 +17,8 @@ export class ResetPasswdMiddleware implements IWebMiddleware {
@Inject()
userSettingsService: UserSettingsService;
@Inject()
sysSettingsService: SysSettingsService;
@Config('system.resetAdminPasswd')
private resetAdminPasswd: boolean;
@@ -40,8 +42,12 @@ export class ResetPasswdMiddleware implements IWebMiddleware {
userId: 1,
key:"user.two.factor"
})
const publicSettings = await this.sysSettingsService.getPublicSettings()
publicSettings.captchaEnabled = false
await this.sysSettingsService.savePublicSettings(publicSettings);
const user = await this.userService.info(1);
logger.info(`重置1号管理员用户的密码完成2FA设置已删除用户名${user.username},新密码:${newPasswd},请在登录进去之后尽快修改密码`);
logger.info(`重置1号管理员用户的密码完成2FA设置已删除验证码登录已禁用,用户名:${user.username},新密码:${newPasswd},请在登录进去之后尽快修改密码`);
}
}
}

View File

@@ -2,7 +2,7 @@ import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/core";
import { CertInfoService } from "../monitor/index.js";
import { pipelineEmitter } from "@certd/pipeline";
import { CertInfo, EVENT_CERT_APPLY_SUCCESS } from "@certd/plugin-cert";
import { PipelineEvent } from "@certd/pipeline/dist/service/emit.js";
import { PipelineEvent } from "@certd/pipeline";
@Autoload()
@Scope(ScopeEnum.Request, { allowDowngrade: true })

View File

@@ -19,6 +19,8 @@ export class AutoZPrint {
@Config('https')
httpsConfig: HttpsServerOptions;
@Config('koa')
koaConfig: any;
@Init()
async init() {
@@ -58,6 +60,7 @@ export class AutoZPrint {
httpsServer.start({
...this.httpsConfig,
app: this.app,
hostname: this.httpsConfig.hostname || this.koaConfig.hostname,
});
}
}

View File

@@ -7,6 +7,7 @@ import {logger, safePromise} from '@certd/basic';
export type HttpsServerOptions = {
enabled: boolean;
app?: Application;
hostname?: string;
port: number;
key: string;
cert: string;
@@ -58,7 +59,7 @@ export class HttpsServer {
opts.app.callback()
);
this.server = httpServer;
const hostname = '::';
let hostname = opts.hostname || '::';
// A function that runs in the context of the http server
// and reports what type of server listens on which port
function listeningReporter() {
@@ -70,7 +71,19 @@ export class HttpsServer {
httpServer.listen(opts.port, hostname, listeningReporter);
return httpServer;
} catch (e) {
logger.error('启动https服务失败', e);
if ( e.message?.includes("address family not supported")) {
hostname = "0.0.0.0"
logger.error(`${e.message},尝试监听${hostname}`, e);
try{
httpServer.listen(opts.port, hostname, listeningReporter);
return httpServer;
}catch (e) {
logger.error('启动https服务失败', e);
}
}else{
logger.error('启动https服务失败', e);
}
}
}
}

View File

@@ -0,0 +1,38 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
export const GROUP_TYPE_SITE = 'site';
@Entity('cd_group')
export class GroupEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'user_id', comment: '用户id' })
userId: number;
@Column({ name: 'name', comment: '分组名称' })
name: string;
@Column({ name: 'icon', comment: '图标' })
icon: string;
@Column({ name: 'favorite', comment: '收藏' })
favorite: boolean;
@Column({ name: 'type', comment: '类型', length: 512 })
type: string;
@Column({
name: 'create_time',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
})
createTime: Date;
@Column({
name: 'update_time',
comment: '修改时间',
default: () => 'CURRENT_TIMESTAMP',
})
updateTime: Date;
}

View File

@@ -1,7 +1,8 @@
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { AddonService, SysSettingsService } from "@certd/lib-server";
import { SysSettingsService } from "@certd/lib-server";
import { logger } from "@certd/basic";
import { ICaptchaAddon } from "../../../plugins/plugin-captcha/api.js";
import { AddonGetterService } from "../../pipeline/service/addon-getter-service.js";
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@@ -9,45 +10,48 @@ export class CaptchaService {
@Inject()
sysSettingsService: SysSettingsService;
@Inject()
addonService: AddonService;
addonGetterService: AddonGetterService;
async getCaptcha(captchaAddonId?:number){
async getCaptcha(captchaAddonId?: number) {
if (!captchaAddonId) {
const settings = await this.sysSettingsService.getPublicSettings()
captchaAddonId = settings.captchaAddonId ?? 0
const settings = await this.sysSettingsService.getPublicSettings();
captchaAddonId = settings.captchaAddonId ?? 0;
}
const addon:ICaptchaAddon = await this.addonService.getAddonById(captchaAddonId,true,0)
const addon: ICaptchaAddon = await this.addonGetterService.getAddonById(captchaAddonId, true, 0, {
type: "captcha",
name: "image"
});
if (!addon) {
throw new Error('验证码插件还未配置')
throw new Error("验证码插件还未配置");
}
return await addon.getCaptcha()
return await addon.getCaptcha();
}
async doValidate(opts:{form:any,must?:boolean,captchaAddonId?:number}){
async doValidate(opts: { form: any, must?: boolean, captchaAddonId?: number }) {
if (!opts.captchaAddonId) {
const settings = await this.sysSettingsService.getPublicSettings()
opts.captchaAddonId = settings.captchaAddonId ?? 0
const settings = await this.sysSettingsService.getPublicSettings();
opts.captchaAddonId = settings.captchaAddonId ?? 0;
}
const addon = await this.addonService.getById(opts.captchaAddonId,0)
const addon = await this.addonGetterService.getById(opts.captchaAddonId, 0);
if (!addon) {
if (opts.must) {
throw new Error('请先配置验证码插件');
throw new Error("请先配置验证码插件");
}
logger.warn('验证码插件还未配置,忽略验证码校验')
return true
logger.warn("验证码插件还未配置,忽略验证码校验");
return true;
}
if (!opts.form) {
throw new Error('请输入验证码');
throw new Error("请输入验证码");
}
const res = await addon.onValidate(opts.form)
const res = await addon.onValidate(opts.form);
if (!res) {
throw new Error('验证码错误');
throw new Error("验证码错误");
}
return true
return true;
}

View File

@@ -3,7 +3,7 @@ import { cache, isDev, randomNumber } from '@certd/basic';
import { SysSettingsService, SysSiteInfo } from '@certd/lib-server';
import { SmsServiceFactory } from '../sms/factory.js';
import { ISmsService } from '../sms/api.js';
import { CodeErrorException } from '@certd/lib-server/dist/basic/exception/code-error-exception.js';
import { CodeErrorException } from '@certd/lib-server';
import { EmailService } from './email-service.js';
import { AccessService } from '@certd/lib-server';
import { AccessSysGetter } from '@certd/lib-server';

View File

@@ -0,0 +1,31 @@
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { BaseService } from '@certd/lib-server';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { merge } from 'lodash-es';
import { GroupEntity } from '../entity/group.js';
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class GroupService extends BaseService<GroupEntity> {
@InjectEntityModel(GroupEntity)
repository: Repository<GroupEntity>;
//@ts-ignore
getRepository() {
return this.repository;
}
async add(bean: any) {
if (!bean.type) {
throw new Error('type is required');
}
bean = merge(
{
favorite: false,
},
bean
);
return await this.repository.save(bean);
}
}

View File

@@ -4,7 +4,7 @@ import {In, Not, Repository} from 'typeorm';
import {AccessService, BaseService} from '@certd/lib-server';
import {DomainEntity} from '../entity/domain.js';
import {SubDomainService} from "../../pipeline/service/sub-domain-service.js";
import {DomainParser} from "@certd/plugin-cert/dist/dns-provider/domain-parser.js";
import {DomainParser} from "@certd/plugin-cert";
import {DomainVerifiers} from "@certd/plugin-cert";
import { SubDomainsGetter } from '../../pipeline/service/getter/sub-domain-getter.js';
import { CnameRecordService } from '../../cname/service/cname-record-service.js';

View File

@@ -13,6 +13,8 @@ export class CnameRecordEntity {
@Column({ comment: '证书申请域名', length: 100 })
domain: string;
@Column({ comment: '主域名', name: 'main_domain', length: 100 })
mainDomain:string;
@Column({ comment: '主机记录', name: 'host_record', length: 100 })
hostRecord: string;

View File

@@ -17,7 +17,7 @@ import { getAuthoritativeDnsResolver, walkTxtRecord } from "@certd/acme-client";
import { CnameProviderService } from "./cname-provider-service.js";
import { CnameProviderEntity } from "../entity/cname-provider.js";
import { CommonDnsProvider } from "./common-provider.js";
import { DomainParser } from "@certd/plugin-cert/dist/dns-provider/domain-parser.js";
import { DomainParser } from "@certd/plugin-cert";
import punycode from "punycode.js";
import { SubDomainService } from "../../pipeline/service/sub-domain-service.js";
import { SubDomainsGetter } from "../../pipeline/service/getter/sub-domain-getter.js";
@@ -37,7 +37,7 @@ type CnameCheckCacheValue = {
* 授权
*/
@Provide()
@Scope(ScopeEnum.Request, {allowDowngrade: true})
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class CnameRecordService extends BaseService<CnameRecordEntity> {
@InjectEntityModel(CnameRecordEntity)
repository: Repository<CnameRecordEntity>;
@@ -71,16 +71,16 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
*/
async add(param: any): Promise<CnameRecordEntity> {
if (!param.domain) {
throw new ValidateException('域名不能为空');
throw new ValidateException("域名不能为空");
}
if (!param.userId) {
throw new ValidateException('userId不能为空');
throw new ValidateException("userId不能为空");
}
if (param.domain.startsWith('*.')) {
if (param.domain.startsWith("*.")) {
param.domain = param.domain.substring(2);
}
param.domain = param.domain.trim()
const info = await this.getRepository().findOne({where: {domain: param.domain, userId: param.userId}});
param.domain = param.domain.trim();
const info = await this.getRepository().findOne({ where: { domain: param.domain, userId: param.userId } });
if (info) {
return info;
}
@@ -90,63 +90,64 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
//获取默认的cnameProviderId
cnameProvider = await this.cnameProviderService.getByPriority();
if (cnameProvider == null) {
throw new ValidateException('找不到CNAME服务请先前往“系统管理->CNAME服务设置”添加CNAME服务');
throw new ValidateException("找不到CNAME服务请先前往“系统管理->CNAME服务设置”添加CNAME服务");
}
} else {
cnameProvider = await this.cnameProviderService.info(param.cnameProviderId);
}
await this.cnameProviderChanged(param.userId, param, cnameProvider);
param.status = 'cname';
const {id} = await super.add(param);
param.status = "cname";
const { id } = await super.add(param);
return await this.info(id);
}
private async cnameProviderChanged(userId: number, param: any, cnameProvider: CnameProviderEntity) {
param.cnameProviderId = cnameProvider.id;
const subDomainGetter = new SubDomainsGetter(userId, this.subDomainService)
const subDomainGetter = new SubDomainsGetter(userId, this.subDomainService);
const domainParser = new DomainParser(subDomainGetter);
const realDomain = await domainParser.parse(param.domain);
const prefix = param.domain.replace(realDomain, '');
const prefix = param.domain.replace(realDomain, "");
let hostRecord = `_acme-challenge.${prefix}`;
if (hostRecord.endsWith('.')) {
if (hostRecord.endsWith(".")) {
hostRecord = hostRecord.substring(0, hostRecord.length - 1);
}
param.hostRecord = hostRecord;
param.mainDomain = realDomain;
const randomKey = utils.id.simpleNanoId(6).toLowerCase();
const userIdHex = utils.hash.toHex(userId)
let userKeyHash = ""
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo)
userKeyHash = `${installInfo.siteId}_${userIdHex}_${randomKey}`
userKeyHash = utils.hash.md5(userKeyHash).substring(0, 10)
logger.info(`userKeyHash:${userKeyHash},subjectId:${installInfo.siteId},randomKey:${randomKey},userIdHex:${userIdHex}`)
const userIdHex = utils.hash.toHex(userId);
let userKeyHash = "";
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
userKeyHash = `${installInfo.siteId}_${userIdHex}_${randomKey}`;
userKeyHash = utils.hash.md5(userKeyHash).substring(0, 10);
logger.info(`userKeyHash:${userKeyHash},subjectId:${installInfo.siteId},randomKey:${randomKey},userIdHex:${userIdHex}`);
const cnameKey = `${userKeyHash}-${userIdHex}-${randomKey}`;
const safeDomain = param.domain.replaceAll('.', '-');
const safeDomain = param.domain.replaceAll(".", "-");
param.recordValue = `${safeDomain}.${cnameKey}.${cnameProvider.domain}`;
}
async update(param: any) {
if (!param.id) {
throw new ValidateException('id不能为空');
throw new ValidateException("id不能为空");
}
//hostRecord包含所有权校验信息不允许用户修改hostRecord
delete param.hostRecord
const old = await this.info(param.id);
if (!old) {
throw new ValidateException('数据不存在');
throw new ValidateException("数据不存在");
}
if (old.domain !== param.domain) {
throw new ValidateException('域名不允许修改');
if (param.domain && old.domain !== param.domain) {
throw new ValidateException("域名不允许修改");
}
if (old.cnameProviderId !== param.cnameProviderId) {
if (param.cnameProviderId && old.cnameProviderId !== param.cnameProviderId) {
const cnameProvider = await this.cnameProviderService.info(param.cnameProviderId);
await this.cnameProviderChanged(old.userId, param, cnameProvider);
param.status = 'cname';
param.status = "cname";
}
return await super.update(param);
}
@@ -171,7 +172,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
} else {
record.commonDnsProvider = new CommonDnsProvider({
config: record.cnameProvider,
plusService: this.plusService,
plusService: this.plusService
});
}
@@ -180,19 +181,22 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
async getByDomain(domain: string, userId: number, createOnNotFound = true) {
if (!domain) {
throw new ValidateException('domain不能为空');
throw new ValidateException("domain不能为空");
}
if (userId == null) {
throw new ValidateException('userId不能为空');
throw new ValidateException("userId不能为空");
}
let record = await this.getRepository().findOne({where: {domain, userId}});
let record = await this.getRepository().findOne({ where: { domain, userId } });
if (record == null) {
if (createOnNotFound) {
record = await this.add({domain, userId});
record = await this.add({ domain, userId });
} else {
throw new ValidateException(`找不到${domain}的CNAME记录`);
}
}
await this.fillMainDomain(record);
const provider = await this.cnameProviderService.info(record.cnameProviderId);
if (provider == null) {
throw new ValidateException(`找不到${domain}的CNAME服务`);
@@ -201,25 +205,53 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
return {
...record,
cnameProvider: {
...provider,
} as CnameProvider,
...provider
} as CnameProvider
} as CnameRecord;
}
async fillMainDomain(record: CnameRecordEntity, update = true) {
const notMainDomain = !record.mainDomain;
const hasErrorMainDomain = record.mainDomain && !record.mainDomain.includes(".");
if (notMainDomain || hasErrorMainDomain) {
let domainPrefix = record.hostRecord.replace("_acme-challenge", "");
if (domainPrefix.startsWith(".")) {
domainPrefix = domainPrefix.substring(1);
}
if (domainPrefix) {
const prefixStr = domainPrefix + ".";
record.mainDomain = record.domain.substring(prefixStr.length);
}else{
record.mainDomain = record.domain;
}
if (update) {
await this.update({
id: record.id,
mainDomain: record.mainDomain
});
}
}
}
/**
* 验证是否配置好cname
* @param id
*/
async verify(id: string) {
async verify(id: number) {
const bean = await this.info(id);
if (!bean) {
throw new ValidateException(`CnameRecord:${id} 不存在`);
}
if (bean.status === 'valid') {
if (bean.status === "valid") {
return true;
}
const subDomainGetter = new SubDomainsGetter(bean.userId, this.subDomainService)
await this.getByDomain(bean.domain, bean.userId);
const subDomainGetter = new SubDomainsGetter(bean.userId, this.subDomainService);
const domainParser = new DomainParser(subDomainGetter);
const cacheKey = `cname.record.verify.${bean.id}`;
@@ -229,7 +261,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
value = {
validating: false,
pass: false,
startTime: new Date().getTime(),
startTime: new Date().getTime()
};
}
let ttl = 5 * 60 * 1000;
@@ -251,16 +283,16 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
//公共CNAME
return new CommonDnsProvider({
config: cnameProvider,
plusService: this.plusService,
plusService: this.plusService
});
}
const serviceGetter = this.taskServiceBuilder.create({userId:cnameProvider.userId})
const serviceGetter = this.taskServiceBuilder.create({ userId: cnameProvider.userId });
const access = await this.accessService.getById(cnameProvider.accessId, cnameProvider.userId);
const context = {access, logger, http, utils, domainParser,serviceGetter};
const context = { access, logger, http, utils, domainParser, serviceGetter };
const dnsProvider: IDnsProvider = await createDnsProvider({
dnsProviderType: cnameProvider.dnsProviderType,
context,
context
});
return dnsProvider;
};
@@ -268,15 +300,15 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
const clearVerifyRecord = async () => {
cache.delete(cacheKey);
try {
let dnsProvider = value.dnsProvider
let dnsProvider = value.dnsProvider;
if (!dnsProvider) {
dnsProvider = await buildDnsProvider();
}
await dnsProvider.removeRecord({
recordReq: value.recordReq,
recordRes: value.recordRes,
recordRes: value.recordRes
});
logger.info('删除CNAME的校验DNS记录成功');
logger.info("删除CNAME的校验DNS记录成功");
} catch (e) {
logger.error(`删除CNAME的校验DNS记录失败 ${e.message}req:${JSON.stringify(value.recordReq)}recordRes:${JSON.stringify(value.recordRes)}`, e);
}
@@ -289,8 +321,8 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
if (value.startTime + ttl < new Date().getTime()) {
logger.warn(`cname验证超时,停止检查,${bean.domain} ${testRecordValue}`);
clearInterval(value.intervalId);
await this.updateStatus(bean.id, 'timeout');
await clearVerifyRecord()
await this.updateStatus(bean.id, "timeout");
await clearVerifyRecord();
return false;
}
@@ -301,7 +333,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
logger.info(`检查CNAME配置 ${fullDomain} ${testRecordValue}`);
//检查是否有重复的acme配置
await this.checkRepeatAcmeChallengeRecords(fullDomain,bean.recordValue)
await this.checkRepeatAcmeChallengeRecords(fullDomain, bean.recordValue);
// const txtRecords = await dns.promises.resolveTxt(fullDomain);
// if (txtRecords.length) {
@@ -318,9 +350,9 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
if (success) {
clearInterval(value.intervalId);
logger.info(`检测到CNAME配置,修改状态 ${fullDomain} ${testRecordValue}`);
await this.updateStatus(bean.id, 'valid', "");
await this.updateStatus(bean.id, "valid", "");
value.pass = true;
await clearVerifyRecord()
await clearVerifyRecord();
return success;
}
};
@@ -331,88 +363,88 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
}
cache.set(cacheKey, value, {
ttl: ttl,
ttl: ttl
});
const domain = await domainParser.parse(bean.recordValue);
const fullRecord = bean.recordValue;
const hostRecord = fullRecord.replace(`.${domain}`, '');
const hostRecord = fullRecord.replace(`.${domain}`, "");
const req = {
domain: domain,
fullRecord: fullRecord,
hostRecord: hostRecord,
type: 'TXT',
value: testRecordValue,
type: "TXT",
value: testRecordValue
};
const dnsProvider = await buildDnsProvider();
if(dnsProvider.usePunyCode()){
if (dnsProvider.usePunyCode()) {
//是否需要中文转英文
req.domain = dnsProvider.punyCodeEncode(req.domain)
req.fullRecord = dnsProvider.punyCodeEncode(req.fullRecord)
req.hostRecord = dnsProvider.punyCodeEncode(req.hostRecord)
req.value = dnsProvider.punyCodeEncode(req.value)
req.domain = dnsProvider.punyCodeEncode(req.domain);
req.fullRecord = dnsProvider.punyCodeEncode(req.fullRecord);
req.hostRecord = dnsProvider.punyCodeEncode(req.hostRecord);
req.value = dnsProvider.punyCodeEncode(req.value);
}
const recordRes = await dnsProvider.createRecord(req);
value.dnsProvider = dnsProvider;
value.validating = true;
value.recordReq = req;
value.recordRes = recordRes;
await this.updateStatus(bean.id, 'validating', "");
await this.updateStatus(bean.id, "validating", "");
value.intervalId = setInterval(async () => {
try {
await checkRecordValue();
} catch (e) {
logger.error('检查cname出错', e);
logger.error("检查cname出错", e);
await this.updateError(bean.id, e.message);
}
}, 10000);
}
async updateStatus(id: number, status: CnameRecordStatusType, error?: string) {
const updated: any = {status}
const updated: any = { status };
if (error != null) {
updated.error = error
updated.error = error;
}
await this.getRepository().update(id, updated);
}
async updateError(id: number, error: string) {
await this.getRepository().update(id, {error});
await this.getRepository().update(id, { error });
}
async checkRepeatAcmeChallengeRecords(acmeRecordDomain: string,targetCnameDomain:string) {
async checkRepeatAcmeChallengeRecords(acmeRecordDomain: string, targetCnameDomain: string) {
let dnsResolver = null
try{
dnsResolver = await getAuthoritativeDnsResolver(acmeRecordDomain)
}catch (e) {
logger.error(`获取${acmeRecordDomain}的权威DNS服务器失败${e.message}`)
return
let dnsResolver = null;
try {
dnsResolver = await getAuthoritativeDnsResolver(acmeRecordDomain);
} catch (e) {
logger.error(`获取${acmeRecordDomain}的权威DNS服务器失败${e.message}`);
return;
}
let cnameRecords = []
try{
let cnameRecords = [];
try {
cnameRecords = await dnsResolver.resolveCname(acmeRecordDomain);
}catch (e) {
logger.error(`查询CNAME记录失败${e.message}`)
return
} catch (e) {
logger.error(`查询CNAME记录失败${e.message}`);
return;
}
targetCnameDomain = targetCnameDomain.toLowerCase()
targetCnameDomain = punycode.toASCII(targetCnameDomain)
targetCnameDomain = targetCnameDomain.toLowerCase();
targetCnameDomain = punycode.toASCII(targetCnameDomain);
if (cnameRecords.length > 0) {
for (const cnameRecord of cnameRecords) {
if(cnameRecord.toLowerCase() !== targetCnameDomain){
if (cnameRecord.toLowerCase() !== targetCnameDomain) {
//确保只有一个cname记录
throw new Error(`${acmeRecordDomain}存在多个CNAME记录请删除多余的CNAME记录${cnameRecord}`)
throw new Error(`${acmeRecordDomain}存在多个CNAME记录请删除多余的CNAME记录${cnameRecord}`);
}
}
}
// 确保权威服务器里面没有纯粹的TXT记录
let txtRecords = []
try{
let txtRecords = [];
try {
const txtRecordRes = await dnsResolver.resolveTxt(acmeRecordDomain);
if (txtRecordRes && txtRecordRes.length > 0) {
@@ -420,13 +452,13 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
logger.info(`TXT records: ${JSON.stringify(txtRecords)}`);
txtRecords = txtRecords.concat(...txtRecordRes);
}
}catch (e) {
logger.error(`查询Txt记录失败${e.message}`)
} catch (e) {
logger.error(`查询Txt记录失败${e.message}`);
}
if (txtRecords.length === 0) {
//如果权威服务器中查不到txt无需继续检查
return
return;
}
if (cnameRecords.length > 0) {
// 从cname记录中获取txt记录
@@ -435,11 +467,18 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
if (res.length > 0) {
for (const txtRecord of txtRecords) {
if (!res.includes(txtRecord)) {
throw new Error(`${acmeRecordDomain}存在多个TXT记录请删除多余的TXT记录:${txtRecord}`)
throw new Error(`${acmeRecordDomain}存在多个TXT记录请删除多余的TXT记录:${txtRecord}`);
}
}
}
}
}
async resetStatus(id: number) {
if (!id) {
throw new ValidateException("id不能为空");
}
await this.getRepository().update(id, { status: "cname", mainDomain: "" });
}
}

View File

@@ -11,12 +11,12 @@ import {
import { RoleService } from "../../sys/authority/service/role-service.js";
import { UserEntity } from "../../sys/authority/entity/user.js";
import { cache, utils } from "@certd/basic";
import { LoginErrorException } from "@certd/lib-server/dist/basic/exception/login-error-exception.js";
import { LoginErrorException } from "@certd/lib-server";
import { CodeService } from "../../basic/service/code-service.js";
import { TwoFactorService } from "../../mine/service/two-factor-service.js";
import { UserSettingsService } from "../../mine/service/user-settings-service.js";
import { isPlus } from "@certd/plus-core";
import { AddonService } from "@certd/lib-server/dist/user/addon/service/addon-service.js";
import { AddonService } from "@certd/lib-server";
/**
* 系统用户

View File

@@ -28,6 +28,7 @@ export class UserSiteMonitorSetting extends BaseSettings {
cron?:string = undefined;
retryTimes?:number = 3;
dnsServer?:string[] = undefined;
certValidDays?:number = 10;
}
export class UserEmailSetting extends BaseSettings {

View File

@@ -56,6 +56,12 @@ export class SiteInfoEntity {
@Column({ name: 'disabled', comment: '禁用启用' })
disabled: boolean;
@Column({ name: 'remark', comment: '备注', length: 512 })
remark: string;
@Column({ name: 'group_id', comment: '分组id' })
groupId: number;
@Column({ name: 'create_time', comment: '创建时间', default: () => 'CURRENT_TIMESTAMP' })
createTime: Date;
@Column({ name: 'update_time', comment: '修改时间', default: () => 'CURRENT_TIMESTAMP' })

View File

@@ -169,8 +169,9 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
if (!notify) {
return;
}
try {
await this.sendExpiresNotify(site);
await this.sendExpiresNotify(site.id);
} catch (e) {
logger.error("send notify error", e);
}
@@ -186,7 +187,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
return;
}
try {
await this.sendCheckErrorNotify(site);
await this.sendCheckErrorNotify(site.id);
} catch (e) {
logger.error("send notify error", e);
}
@@ -231,8 +232,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
ipErrorCount: errorCount
});
try {
site = await this.info(site.id);
await this.sendCheckErrorNotify(site, true);
await this.sendCheckErrorNotify(site.id, true);
} catch (e) {
logger.error("send notify error", e);
}
@@ -254,7 +254,8 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
return await this.doCheck(site, notify, retryTimes);
}
async sendCheckErrorNotify(site: SiteInfoEntity, fromIpCheck = false) {
async sendCheckErrorNotify(siteId: number, fromIpCheck = false) {
const site = await this.info(siteId);
const url = await this.notificationService.getBindUrl("#/certd/monitor/site");
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(site.userId, UserSiteMonitorSetting)
// 发邮件
@@ -274,14 +275,14 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
);
}
async sendExpiresNotify(site: SiteInfoEntity) {
const tipDays = 10;
async sendExpiresNotify(siteId: number) {
const site = await this.info(siteId);
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(site.userId, UserSiteMonitorSetting)
const tipDays = setting?.certValidDays || 10;
const expires = site.certExpiresTime;
const validDays = dayjs(expires).diff(dayjs(), "day");
const url = await this.notificationService.getBindUrl("#/certd/monitor/site");
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(site.userId, UserSiteMonitorSetting)
const content = `站点名称: ${site.name} \n站点域名 ${site.domain} \n证书域名 ${site.certDomains} \n颁发机构 ${site.certProvider} \n过期时间 ${dayjs(site.certExpiresTime).format("YYYY-MM-DD")} \n`;
if (validDays >= 0 && validDays < tipDays) {
// 发通知
@@ -392,7 +393,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
}
}
async doImport(req: { text: string; userId: number }) {
async doImport(req: { text: string; userId: number,groupId?:number }) {
if (!req.text) {
throw new Error("text is required");
}
@@ -420,17 +421,22 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
} catch (e) {
throw new Error(`${item}格式错误`);
}
}
if (arr.length > 2) {
name = arr[2] || domain;
}
let remark:string = "";
if (arr.length > 3) {
remark = arr[3] || "";
}
list.push({
domain,
name,
httpsPort: port,
userId: req.userId
userId: req.userId,
remark,
groupId: req.groupId
});
}

View File

@@ -43,28 +43,19 @@ export class PipelineEntity {
@Column({ name:"is_template", comment: '是否模版', nullable: true, default: false })
isTemplate: boolean;
@Column({
name: 'last_history_time',
comment: '最后一次执行时间',
nullable: true,
})
@Column({name: 'last_history_time',comment: '最后一次执行时间',nullable: true,})
lastHistoryTime: number;
@Column({name: 'valid_time',comment: '到期时间',nullable: true,default: 0})
validTime: number;
// 变量
lastVars: any;
@Column({
name: 'order',
comment: '排序',
nullable: true,
})
@Column({name: 'order', comment: '排序', nullable: true,})
order: number;
@Column({
name: 'create_time',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
})
@Column({name: 'create_time',comment: '创建时间', default: () => 'CURRENT_TIMESTAMP',})
createTime: Date;
@Column({
name: 'update_time',

View File

@@ -0,0 +1,65 @@
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { http, logger, utils } from "@certd/basic";
import { TaskServiceBuilder } from "./getter/task-service-getter.js";
import { AddonService, newAddon, PermissionException, ValidateException } from "@certd/lib-server";
/**
* Addon
*/
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AddonGetterService {
@Inject()
taskServiceBuilder: TaskServiceBuilder;
@Inject()
addonService: AddonService;
async getAddonById(id: any, checkUserId: boolean, userId?: number, defaultAddon?:{type:string,name:string} ): Promise<any> {
const serviceGetter = this.taskServiceBuilder.create({
userId
});
const ctx = {
http,
logger,
utils,
serviceGetter
}
if (!id) {
if (!defaultAddon) {
return null;
}
return await newAddon(defaultAddon.type, defaultAddon.name, {}, ctx);
}
const entity = await this.addonService.info(id);
if (entity == null) {
if (!defaultAddon) {
return null;
}
return await newAddon(defaultAddon.type, defaultAddon.name, {}, ctx);
}
if (checkUserId) {
if (userId == null) {
throw new ValidateException("userId不能为空");
}
if (userId !== entity.userId) {
throw new PermissionException("您对该Addon无访问权限");
}
}
const setting = JSON.parse(entity.setting ?? "{}");
const input = {
id: entity.id,
...setting
};
return await newAddon(entity.addonType, entity.type, input, ctx);
}
async getById(id: any, userId: number): Promise<any> {
return await this.getAddonById(id, true, userId);
}
}

Some files were not shown because too many files have changed in this diff Show More