mirror of
https://github.com/certd/certd.git
synced 2026-06-25 03:57:30 +08:00
perf: 新增站点证书监控从DNS解析记录批量导入功能
本次提交新增了从DNS解析记录批量导入站点监控的完整功能: 1. 扩展Registrable类型新增icon字段支持 2. 新增DNS解析记录获取接口和基础实现 3. 为阿里云、腾讯云、Cloudflare等DNS提供商添加解析记录分页获取支持 4. 新增站点监控导入任务管理功能,支持保存、启动、删除导入任务 5. 新增中文/英文多语言支持 6. 优化暗黑模式表格样式 7. 修复ACME账户访问修复逻辑中项目ID可选的问题 8. 优化HiPM DNS提供商的域名获取逻辑
This commit is contained in:
@@ -1 +1 @@
|
||||
02:33
|
||||
23:28
|
||||
|
||||
@@ -7,6 +7,7 @@ export type Registrable = {
|
||||
group?: string;
|
||||
deprecated?: string;
|
||||
order?: number;
|
||||
icon?: string;
|
||||
};
|
||||
export type TargetGetter<T> = () => Promise<T>;
|
||||
export type RegistryItem<T> = {
|
||||
|
||||
@@ -34,6 +34,14 @@ export type DomainRecord = {
|
||||
domain: string;
|
||||
};
|
||||
|
||||
export type DnsResolveRecord = {
|
||||
id: string;
|
||||
hostRecord: string;
|
||||
fullRecord: string;
|
||||
type: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export interface IDnsProvider<T = any> {
|
||||
onInstance(): Promise<void>;
|
||||
|
||||
@@ -59,6 +67,8 @@ export interface IDnsProvider<T = any> {
|
||||
usePunyCode(): boolean;
|
||||
|
||||
getDomainListPage(pager: PageSearch): Promise<PageRes<DomainRecord>>;
|
||||
|
||||
getRecordListPage?(domain: string, pager: PageSearch): Promise<PageRes<DnsResolveRecord>>;
|
||||
}
|
||||
|
||||
export interface ISubDomainsGetter {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { HttpClient, ILogger } from "@certd/basic";
|
||||
import { IAccessService, PageRes, PageSearch } from "@certd/pipeline";
|
||||
import punycode from "punycode.js";
|
||||
import { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, DomainRecord, IDnsProvider, RemoveRecordOptions } from "./api.js";
|
||||
import { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, DnsResolveRecord, DomainRecord, IDnsProvider, RemoveRecordOptions } from "./api.js";
|
||||
import { dnsProviderRegistry } from "./registry.js";
|
||||
export abstract class AbstractDnsProvider<T = any> implements IDnsProvider<T> {
|
||||
ctx!: DnsProviderContext;
|
||||
@@ -49,6 +49,10 @@ export abstract class AbstractDnsProvider<T = any> implements IDnsProvider<T> {
|
||||
async getDomainListPage(req: PageSearch): Promise<PageRes<DomainRecord>> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
async getRecordListPage(domain: string, req: PageSearch): Promise<PageRes<DnsResolveRecord>> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
export async function createDnsProvider(opts: { dnsProviderType: string; context: DnsProviderContext }): Promise<IDnsProvider> {
|
||||
|
||||
@@ -18,6 +18,7 @@ export default {
|
||||
subdomainConfirmTitle: "Subdomain Confirmation",
|
||||
subdomainConfirmContent: "{domain} appears to be a subdomain. Only delegated subdomains and free second-level subdomains need to be maintained here. Otherwise certificate application may fail. Continue?",
|
||||
importFromProvider: "Import from Domain Provider",
|
||||
importFromResolveRecords: "Import from DNS Records",
|
||||
syncExpirationDate: "Sync Domain Expiration Time",
|
||||
syncTaskSubmitted: "Sync task submitted",
|
||||
syncExpirationProgress: "Sync Domain Expiration Progress",
|
||||
|
||||
@@ -18,6 +18,7 @@ export default {
|
||||
subdomainConfirmTitle: "子域名确认",
|
||||
subdomainConfirmContent: "检测到{domain}为子域名,只有托管子域名和免费二级子域名才需要在此处维护,否则会导致申请证书失败,请确认是否继续?",
|
||||
importFromProvider: "从域名提供商导入",
|
||||
importFromResolveRecords: "从解析记录导入",
|
||||
syncExpirationDate: "同步域名过期时间",
|
||||
syncTaskSubmitted: "同步任务已提交",
|
||||
syncExpirationProgress: "同步域名过期时间进度",
|
||||
|
||||
@@ -8,4 +8,45 @@
|
||||
.vben-normal-menu__item.is-active {
|
||||
background-color: #3b3b3b !important;
|
||||
}
|
||||
|
||||
.cd-table {
|
||||
th,
|
||||
td {
|
||||
border-bottom: 1px solid #303030;
|
||||
border-left: 1px solid #303030;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #1f1f1f;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
border-top: 1px solid #303030;
|
||||
|
||||
&:last-child {
|
||||
border-right: 1px solid #303030;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
&:last-child {
|
||||
border-right: 1px solid #303030;
|
||||
}
|
||||
}
|
||||
|
||||
td.position-sticky-right {
|
||||
background-color: #141414;
|
||||
}
|
||||
|
||||
.position-sticky-right::before {
|
||||
background: #303030;
|
||||
}
|
||||
|
||||
tr.hover-color:hover td {
|
||||
background: #262626;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background: #1f3a23;
|
||||
color: #81c784;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,36 @@ export const siteInfoApi = {
|
||||
});
|
||||
},
|
||||
|
||||
async ImportTaskSave(body: any) {
|
||||
return await request({
|
||||
url: apiPrefix + "/import/save",
|
||||
method: "post",
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
async ImportTaskStatus() {
|
||||
return await request({
|
||||
url: apiPrefix + "/import/status",
|
||||
method: "post",
|
||||
});
|
||||
},
|
||||
async ImportTaskDelete(key: string) {
|
||||
return await request({
|
||||
url: apiPrefix + "/import/delete",
|
||||
method: "post",
|
||||
data: { key },
|
||||
});
|
||||
},
|
||||
async ImportTaskStart(key: string) {
|
||||
return await request({
|
||||
url: apiPrefix + "/import/start",
|
||||
method: "post",
|
||||
data: { key },
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
|
||||
async DisabledChange(id: number, disabled: boolean) {
|
||||
return await request({
|
||||
url: apiPrefix + "/disabledChange",
|
||||
|
||||
@@ -9,7 +9,7 @@ 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 { useSiteImport, useSiteImportTaskManage } from "/@/views/certd/monitor/site/use";
|
||||
import { ref } from "vue";
|
||||
import GroupSelector from "../../basic/group/group-selector.vue";
|
||||
import { createGroupDictRef } from "../../basic/group/api";
|
||||
@@ -53,6 +53,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
|
||||
const { openSiteIpMonitorDialog } = useSiteIpMonitor();
|
||||
const { openSiteImportDialog } = useSiteImport();
|
||||
const openSiteImportTaskManageDialog = useSiteImportTaskManage();
|
||||
|
||||
const certValidDaysRef = ref(10);
|
||||
|
||||
@@ -200,6 +201,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
actionbar: {
|
||||
buttons: {
|
||||
add: {
|
||||
icon: "ion:add-circle-outline",
|
||||
async click() {
|
||||
if (!settingsStore.isPlus) {
|
||||
// 非plus
|
||||
@@ -236,6 +238,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
show: hasActionPermission("write"),
|
||||
text: t("monitor.bulkImport"),
|
||||
type: "primary",
|
||||
icon: "ion:cloud-upload-outline",
|
||||
async click() {
|
||||
const defaultGroupId = getDefaultGroupId();
|
||||
openSiteImportDialog({
|
||||
@@ -246,10 +249,27 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
});
|
||||
},
|
||||
},
|
||||
importFromProvider: {
|
||||
show: hasActionPermission("write"),
|
||||
title: t("certd.domain.importFromResolveRecords"),
|
||||
text: t("certd.domain.importFromResolveRecords"),
|
||||
type: "primary",
|
||||
needPlus: true,
|
||||
color: "gold",
|
||||
icon: "mingcute:vip-1-line",
|
||||
click: async () => {
|
||||
await openSiteImportTaskManageDialog({
|
||||
afterSubmit: () => {
|
||||
crudExpose.doRefresh();
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
checkAll: {
|
||||
show: true,
|
||||
text: t("monitor.checkAll"),
|
||||
type: "primary",
|
||||
icon: "ion:play-circle-outline",
|
||||
click() {
|
||||
checkAll();
|
||||
},
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div class="site-info-import-task-status min-h-[300px]">
|
||||
<div class="action mb-5">
|
||||
<fs-button type="primary" icon="mingcute:vip-1-line" @click="addTask">{{ t("certd.domain.addImportTask") }}</fs-button>
|
||||
<fs-button type="primary" icon="ion:refresh-outline" class="ml-2" @click="loadImportTaskStatus">{{ t("certd.domain.refresh") }}</fs-button>
|
||||
</div>
|
||||
<div class="table-container overflow-auto mb-10">
|
||||
<table class="cd-table border-gray-300 w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-[220px]">{{ t("certd.sourcee") }}</th>
|
||||
<th class="">{{ t("certd.domain.progress") }}</th>
|
||||
<th class="w-[220px]">{{ t("certd.domain.operation") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in list" :key="item.key">
|
||||
<td class="ellipsis">
|
||||
<span class="flex items-center pointer" @click="editTask(item)">
|
||||
<span class="flex-1 ellipsis flex items-center">
|
||||
<fs-icon :icon="item.icon" class="mr-2"></fs-icon>
|
||||
{{ item.title }}
|
||||
</span>
|
||||
<fs-icon icon="ant-design:edit-outlined" class="ml-2" />
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div v-if="item.task">
|
||||
<div>
|
||||
<a-tag color="blue">{{ t("certd.domain.total") }}:{{ item.task?.total }}</a-tag>
|
||||
<a-tag color="success" class="ml-2">{{ t("certd.success") }}:{{ item.task?.successCount }}</a-tag>
|
||||
<a-tag type="info" class="ml-2">{{ t("certd.domain.skipped") }}:{{ item.task?.skipCount }}</a-tag>
|
||||
<a-tooltip v-if="item.task?.errors.length > 0">
|
||||
<template #title>
|
||||
<div v-for="error in item.task?.errors" :key="error">{{ error }}</div>
|
||||
</template>
|
||||
<a-tag color="red" class="ml-2">{{ t("certd.domain.failed") }}:{{ item.task?.errors.length }}</a-tag>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<a-progress :percent="item.task?.progress" size="small" status="active" />
|
||||
</div>
|
||||
<div v-else>{{ t("certd.domain.notExecuted") }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<fs-button type="primary" icon="ion:play-outline" :disabled="item.task?.status === 'running'" @click="startTask(item)">{{ t("certd.domain.execute") }}</fs-button>
|
||||
<fs-button type="primary" class="ml-2" danger icon="ion:trash-outline" @click="deleteTask(item)">{{ t("certd.domain.delete") }}</fs-button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Modal } from "ant-design-vue";
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
import * as api from "./api";
|
||||
import { useSiteImportTask } from "./use";
|
||||
import { useSettingStore } from "/@/store/settings";
|
||||
import { useI18n } from "/@/locales";
|
||||
defineOptions({
|
||||
name: "SiteInfoImportTaskStatus",
|
||||
});
|
||||
|
||||
const list = ref([]);
|
||||
const { t } = useI18n();
|
||||
|
||||
async function loadImportTaskStatus() {
|
||||
const res = await api.siteInfoApi.ImportTaskStatus();
|
||||
list.value = res || [];
|
||||
}
|
||||
|
||||
async function startTask(item: any) {
|
||||
settingStore.checkPlus();
|
||||
await api.siteInfoApi.ImportTaskStart(item.key);
|
||||
await loadImportTaskStatus();
|
||||
}
|
||||
|
||||
async function deleteTask(item: any) {
|
||||
Modal.confirm({
|
||||
title: t("certd.domain.confirmDelete"),
|
||||
okText: t("common.confirm"),
|
||||
okType: "danger",
|
||||
onOk: async () => {
|
||||
await api.siteInfoApi.ImportTaskDelete(item.key);
|
||||
await loadImportTaskStatus();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const openSiteImportTaskDialog = useSiteImportTask();
|
||||
const settingStore = useSettingStore();
|
||||
async function addTask() {
|
||||
settingStore.checkPlus();
|
||||
await openSiteImportTaskDialog({
|
||||
afterSubmit: async (res?: any) => {
|
||||
if (res) {
|
||||
await api.siteInfoApi.ImportTaskStart(res.key);
|
||||
}
|
||||
await loadImportTaskStatus();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function editTask(item: any) {
|
||||
settingStore.checkPlus();
|
||||
await openSiteImportTaskDialog({
|
||||
afterSubmit: async () => {
|
||||
await loadImportTaskStatus();
|
||||
},
|
||||
form: item,
|
||||
});
|
||||
}
|
||||
|
||||
const checkIntervalRef = ref();
|
||||
onMounted(async () => {
|
||||
await loadImportTaskStatus();
|
||||
checkIntervalRef.value = setInterval(async () => {
|
||||
await loadImportTaskStatus();
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(checkIntervalRef.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.site-info-import-task-status {
|
||||
.table-container {
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
.ant-progress {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,11 @@
|
||||
import { useFormWrapper } from "@fast-crud/fast-crud";
|
||||
import { useFormWrapper, compute } from "@fast-crud/fast-crud";
|
||||
import { siteInfoApi } from "./api";
|
||||
import { useI18n } from "/src/locales";
|
||||
import { useI18n } from "/@/locales";
|
||||
import { useSettingStore } from "/@/store/settings";
|
||||
import { useFormDialog } from "/@/use/use-dialog";
|
||||
import GroupSelector from "../../basic/group/group-selector.vue";
|
||||
import SiteInfoImportTaskStatus from "./import.vue";
|
||||
|
||||
export function useSiteImport() {
|
||||
const { t } = useI18n();
|
||||
const { openCrudFormDialog } = useFormWrapper();
|
||||
@@ -13,7 +17,7 @@ export function useSiteImport() {
|
||||
columns: {
|
||||
text: {
|
||||
type: "textarea",
|
||||
title: t("certd.domainList.title"), // 域名列表
|
||||
title: t("certd.domainList.title"),
|
||||
form: {
|
||||
helper: t("certd.domainList.helper"),
|
||||
rules: [{ required: true, message: t("certd.domainList.required") }],
|
||||
@@ -21,9 +25,7 @@ export function useSiteImport() {
|
||||
placeholder: t("certd.domainList.placeholder"),
|
||||
rows: 8,
|
||||
},
|
||||
col: {
|
||||
span: 24,
|
||||
},
|
||||
col: { span: 24 },
|
||||
},
|
||||
},
|
||||
groupId: {
|
||||
@@ -36,13 +38,10 @@ export function useSiteImport() {
|
||||
vModel: "modelValue",
|
||||
type: "site",
|
||||
},
|
||||
col: {
|
||||
span: 24,
|
||||
},
|
||||
col: { span: 24 },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
form: {
|
||||
async doSubmit({ form }) {
|
||||
return siteInfoApi.Import(form);
|
||||
@@ -53,7 +52,99 @@ export function useSiteImport() {
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
openSiteImportDialog,
|
||||
return { openSiteImportDialog };
|
||||
}
|
||||
|
||||
export function useSiteImportTask() {
|
||||
const { openFormDialog } = useFormDialog();
|
||||
const { t } = useI18n();
|
||||
|
||||
const columns = {
|
||||
dnsProviderType: {
|
||||
title: t("certd.domain.domainProvider"),
|
||||
type: "text",
|
||||
form: {
|
||||
component: {
|
||||
name: "dns-provider-selector",
|
||||
on: {
|
||||
selectedChange: ({ form, $event }: any) => {
|
||||
form.dnsProviderAccessType = $event.accessType;
|
||||
},
|
||||
},
|
||||
},
|
||||
valueChange({ form }: any) {
|
||||
form.dnsProviderAccessId = null;
|
||||
},
|
||||
},
|
||||
},
|
||||
dnsProviderAccessType: {
|
||||
title: t("certd.domain.domainProviderAccessType"),
|
||||
type: "text",
|
||||
form: { show: false },
|
||||
},
|
||||
dnsProviderAccessId: {
|
||||
title: t("certd.domain.domainProviderAccess"),
|
||||
type: "text",
|
||||
form: {
|
||||
component: {
|
||||
name: "access-selector",
|
||||
vModel: "modelValue",
|
||||
type: compute(({ form }: any) => form.dnsProviderAccessType || form.dnsProviderType),
|
||||
},
|
||||
},
|
||||
},
|
||||
groupId: {
|
||||
title: t("certd.fields.group"),
|
||||
type: "text",
|
||||
form: {
|
||||
component: {
|
||||
name: GroupSelector,
|
||||
vModel: "modelValue",
|
||||
type: "site",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return function openSiteImportTaskDialog(req: { afterSubmit?: (res?: any) => void; form?: any }) {
|
||||
openFormDialog({
|
||||
title: t("certd.domain.importFromProvider"),
|
||||
columns,
|
||||
initialForm: { ...req.form },
|
||||
onSubmit: async (form: any) => {
|
||||
const res = await siteInfoApi.ImportTaskSave({
|
||||
key: form.key,
|
||||
dnsProviderType: form.dnsProviderType,
|
||||
dnsProviderAccessId: form.dnsProviderAccessId,
|
||||
groupId: form.groupId,
|
||||
});
|
||||
if (req.afterSubmit) {
|
||||
req.afterSubmit(res);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function useSiteImportTaskManage() {
|
||||
const { openFormDialog } = useFormDialog();
|
||||
const { t } = useI18n();
|
||||
const settingStore = useSettingStore();
|
||||
return async function openSiteImportTaskManageDialog(req: {
|
||||
afterSubmit?: (res?: any) => void;
|
||||
form?: any;
|
||||
zIndex?: number;
|
||||
}) {
|
||||
settingStore.checkPlus();
|
||||
await openFormDialog({
|
||||
title: t("certd.domain.importFromProvider"),
|
||||
body: () => <SiteInfoImportTaskStatus />,
|
||||
zIndex: req.zIndex,
|
||||
onSubmit: async (form: any) => {
|
||||
if (req.afterSubmit) {
|
||||
req.afterSubmit(form);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -106,9 +106,13 @@ input:
|
||||
onSelectedChange: ctx.compute(({form})=>{
|
||||
return ($event)=>{
|
||||
form.dnsProviderAccessType = $event.accessType
|
||||
form.dnsProviderAccess = null
|
||||
}
|
||||
})
|
||||
}),
|
||||
onChange: ctx.compute(({form})=>{
|
||||
return ($event)=>{
|
||||
form.dnsProviderAccess = null
|
||||
}
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -136,6 +136,55 @@ export class SiteInfoController extends CrudController<SiteInfoService> {
|
||||
return this.ok();
|
||||
}
|
||||
|
||||
@Post("/import/save", { description: Constants.per.authOnly, summary: "保存站点证书监控导入任务" })
|
||||
async siteInfoImportSave(@Body(ALL) body: any) {
|
||||
const { projectId, userId } = await this.getProjectUserIdWrite();
|
||||
const { dnsProviderType, dnsProviderAccessId, key, groupId } = body;
|
||||
const item = await this.service.saveSiteInfoImportTask({
|
||||
userId: userId,
|
||||
projectId: projectId,
|
||||
dnsProviderType,
|
||||
dnsProviderAccessId,
|
||||
key,
|
||||
groupId,
|
||||
});
|
||||
return this.ok(item);
|
||||
}
|
||||
|
||||
@Post("/import/status", { description: Constants.per.authOnly, summary: "查询站点证书监控导入任务状态" })
|
||||
async siteInfoImportStatus() {
|
||||
const { projectId, userId } = await this.getProjectUserIdRead();
|
||||
const task = await this.service.getSiteInfoImportTaskStatus({
|
||||
userId: userId,
|
||||
projectId: projectId,
|
||||
});
|
||||
return this.ok(task);
|
||||
}
|
||||
|
||||
@Post("/import/delete", { description: Constants.per.authOnly, summary: "删除站点证书监控导入任务" })
|
||||
async siteInfoImportDelete(@Body(ALL) body: any) {
|
||||
const { projectId, userId } = await this.getProjectUserIdWrite();
|
||||
const { key } = body;
|
||||
await this.service.deleteSiteInfoImportTask({
|
||||
userId: userId,
|
||||
projectId: projectId,
|
||||
key,
|
||||
});
|
||||
return this.ok();
|
||||
}
|
||||
|
||||
@Post("/import/start", { description: Constants.per.authOnly, summary: "开始站点证书监控导入任务" })
|
||||
async siteInfoImportStart(@Body(ALL) body: any) {
|
||||
const { projectId, userId } = await this.getProjectUserIdWrite();
|
||||
const { key } = body;
|
||||
await this.service.startSiteInfoImportTask({
|
||||
key,
|
||||
userId: userId,
|
||||
projectId: projectId,
|
||||
});
|
||||
return this.ok();
|
||||
}
|
||||
|
||||
@Post("/ipCheckChange", { description: Constants.per.authOnly, summary: "修改IP检查设置" })
|
||||
async ipCheckChange(@Body(ALL) bean: any) {
|
||||
await this.checkOwner(this.service, bean.id, "read");
|
||||
|
||||
@@ -98,14 +98,17 @@ export class LegacyAcmeAccountAccessFix {
|
||||
continue;
|
||||
}
|
||||
const name = buildAcmeAccountAccessName(parsedKey.caType, parsedKey.email);
|
||||
const exists = await this.accessService.findOne({
|
||||
where: {
|
||||
const query = {
|
||||
userId: record.userId,
|
||||
projectId: record.projectId,
|
||||
type: "acmeAccount",
|
||||
subtype: parsedKey.caType,
|
||||
name,
|
||||
} as any,
|
||||
} as any
|
||||
if (record.projectId) {
|
||||
query.projectId = record.projectId;
|
||||
}
|
||||
const exists = await this.accessService.findOne({
|
||||
where:query,
|
||||
});
|
||||
if (exists) {
|
||||
continue;
|
||||
|
||||
@@ -58,3 +58,10 @@ export class UserDomainImportSetting extends BaseSettings {
|
||||
|
||||
domainImportList: { dnsProviderType: string; dnsProviderAccessId: number; key: string; title: string; icon?: string }[];
|
||||
}
|
||||
|
||||
export class UserSiteInfoImportSetting extends BaseSettings {
|
||||
static __title__ = "用户站点证书监控导入设置";
|
||||
static __key__ = "user.siteInfo.import";
|
||||
|
||||
siteInfoImportList: { dnsProviderType: string; dnsProviderAccessId: number; key: string; title: string; icon?: string; groupId?: number }[];
|
||||
}
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { BaseService, Constants, isEnterprise, NeedSuiteException, NeedVIPException, SysSettingsService } from "@certd/lib-server";
|
||||
import { InjectEntityModel } from "@midwayjs/typeorm";
|
||||
import { In, Repository } from "typeorm";
|
||||
import { SiteInfoEntity } from "../entity/site-info.js";
|
||||
import { siteTester } from "./site-tester.js";
|
||||
import dayjs from "dayjs";
|
||||
import { logger, utils } from "@certd/basic";
|
||||
import { PeerCertificate } from "tls";
|
||||
import { NotificationService } from "../../pipeline/service/notification-service.js";
|
||||
import { isComm, isPlus } from "@certd/plus-core";
|
||||
import { http, logger, utils } from "@certd/basic";
|
||||
import { UserSuiteService } from "@certd/commercial-core";
|
||||
import { UserSettingsService } from "../../mine/service/user-settings-service.js";
|
||||
import { UserSiteMonitorSetting } from "../../mine/service/models.js";
|
||||
import { SiteIpService } from "./site-ip-service.js";
|
||||
import { SiteIpEntity } from "../entity/site-ip.js";
|
||||
import { Cron } from "../../cron/cron.js";
|
||||
import { dnsContainer } from "./dns-custom.js";
|
||||
import { AccessService, BaseService, Constants, isEnterprise, NeedSuiteException, NeedVIPException, SysSettingsService } from "@certd/lib-server";
|
||||
import { Pager } from "@certd/pipeline";
|
||||
import { createDnsProvider, dnsProviderRegistry, DomainParser } from "@certd/plugin-lib";
|
||||
import { isComm, isPlus } from "@certd/plus-core";
|
||||
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { InjectEntityModel } from "@midwayjs/typeorm";
|
||||
import dayjs from "dayjs";
|
||||
import { merge } from "lodash-es";
|
||||
import { JobHistoryService } from "./job-history-service.js";
|
||||
import { JobHistoryEntity } from "../entity/job-history.js";
|
||||
import { PeerCertificate } from "tls";
|
||||
import { In, Repository } from "typeorm";
|
||||
import { BackTask, taskExecutor } from "../../basic/service/task-executor.js";
|
||||
import { Cron } from "../../cron/cron.js";
|
||||
import { UserSiteInfoImportSetting, UserSiteMonitorSetting } from "../../mine/service/models.js";
|
||||
import { UserSettingsService } from "../../mine/service/user-settings-service.js";
|
||||
import { TaskServiceBuilder } from "../../pipeline/service/getter/task-service-getter.js";
|
||||
import { NotificationService } from "../../pipeline/service/notification-service.js";
|
||||
import { UserService } from "../../sys/authority/service/user-service.js";
|
||||
import { ProjectService } from "../../sys/enterprise/service/project-service.js";
|
||||
import { JobHistoryEntity } from "../entity/job-history.js";
|
||||
import { SiteInfoEntity } from "../entity/site-info.js";
|
||||
import { SiteIpEntity } from "../entity/site-ip.js";
|
||||
import { dnsContainer } from "./dns-custom.js";
|
||||
import { JobHistoryService } from "./job-history-service.js";
|
||||
import { SiteIpService } from "./site-ip-service.js";
|
||||
import { siteTester } from "./site-tester.js";
|
||||
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
@@ -51,6 +55,12 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
|
||||
@Inject()
|
||||
projectService: ProjectService;
|
||||
|
||||
@Inject()
|
||||
accessService: AccessService;
|
||||
|
||||
@Inject()
|
||||
taskServiceBuilder: TaskServiceBuilder;
|
||||
|
||||
@Inject()
|
||||
cron: Cron;
|
||||
|
||||
@@ -64,7 +74,6 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
|
||||
//企业模式不限制
|
||||
return;
|
||||
}
|
||||
|
||||
if (isComm()) {
|
||||
const suiteSetting = await this.userSuiteService.getSuiteSetting();
|
||||
if (suiteSetting.enabled) {
|
||||
@@ -483,6 +492,219 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
|
||||
await batchAdd(list);
|
||||
}
|
||||
|
||||
async startSiteInfoImportTask(req: { userId: number; projectId: number; key: string }) {
|
||||
const key = req.key;
|
||||
const setting = await this.userSettingsService.getSetting<UserSiteInfoImportSetting>(req.userId, req.projectId, UserSiteInfoImportSetting);
|
||||
const item = setting.siteInfoImportList.find(item => item.key === key);
|
||||
if (!item) {
|
||||
throw new Error(`站点监控导入任务(${key})还未注册`);
|
||||
}
|
||||
const { dnsProviderType, dnsProviderAccessId, title, groupId } = item;
|
||||
|
||||
const TASK_TYPE = "siteInfoImportTask";
|
||||
taskExecutor.start(
|
||||
new BackTask({
|
||||
type: TASK_TYPE,
|
||||
key,
|
||||
title,
|
||||
run: async (task: BackTask) => {
|
||||
await this._syncSitesFromProvider(
|
||||
{
|
||||
userId: req.userId,
|
||||
projectId: req.projectId,
|
||||
dnsProviderType,
|
||||
dnsProviderAccessId,
|
||||
groupId,
|
||||
},
|
||||
task
|
||||
);
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private async _syncSitesFromProvider(req: { userId: number; projectId: number; dnsProviderType: string; dnsProviderAccessId: number; groupId?: number }, task: BackTask) {
|
||||
const { userId, projectId, dnsProviderType, dnsProviderAccessId, groupId } = req;
|
||||
|
||||
const serviceGetter = this.taskServiceBuilder.create({ userId, projectId });
|
||||
const subDomainGetter = await serviceGetter.getSubDomainsGetter();
|
||||
const domainParser = new DomainParser(subDomainGetter);
|
||||
|
||||
const access = await this.accessService.getById(dnsProviderAccessId, userId, projectId);
|
||||
const context = { access, logger, http, utils, domainParser, serviceGetter };
|
||||
const dnsProvider = await createDnsProvider({ dnsProviderType, context });
|
||||
|
||||
// 1. 先获取主域名列表(每个 domain 翻页)
|
||||
const domainPager = new Pager({ pageNo: 1, pageSize: 50 });
|
||||
const domainList: string[] = [];
|
||||
while (true) {
|
||||
const pageRet = await dnsProvider.getDomainListPage(domainPager);
|
||||
for (const item of pageRet.list || []) {
|
||||
domainList.push(item.domain);
|
||||
}
|
||||
if (!pageRet.list || pageRet.list.length < domainPager.pageSize) {
|
||||
break;
|
||||
}
|
||||
domainPager.pageNo++;
|
||||
}
|
||||
|
||||
// 2. 根据 provider 是否支持 getRecordListPage 决定处理方式
|
||||
const skipTypes = new Set(["TXT", "NS", "SOA", "SRV", "CAA", "PTR"]);
|
||||
for (const domain of domainList) {
|
||||
if (!dnsProvider.getRecordListPage) {
|
||||
// 不支持解析记录列表时,直接把主域名作为一个站点
|
||||
try {
|
||||
await this.add({
|
||||
userId,
|
||||
projectId,
|
||||
groupId,
|
||||
domain,
|
||||
name: domain,
|
||||
httpsPort: 443,
|
||||
} as any);
|
||||
task.incrementCurrent();
|
||||
} catch (e) {
|
||||
if (e.message && e.message.indexOf("已达上限") >= 0) {
|
||||
task.addError(`${domain}: ${e.message}`);
|
||||
break;
|
||||
}
|
||||
task.incrementSkip();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// 支持 getRecordListPage:翻页获取解析记录,过滤掉泛域名(*.)和不支持的类型
|
||||
const recordPager = new Pager({ pageNo: 1, pageSize: 100 });
|
||||
while (true) {
|
||||
const pageRet = await dnsProvider.getRecordListPage(domain, recordPager);
|
||||
for (const record of pageRet.list || []) {
|
||||
task.incrementCurrent();
|
||||
const typeUpper = (record.type || "").toUpperCase();
|
||||
if (skipTypes.has(typeUpper)) {
|
||||
task.incrementSkip();
|
||||
continue;
|
||||
}
|
||||
const fullRecord = record.fullRecord;
|
||||
if (!fullRecord || fullRecord.startsWith("*.") || fullRecord.startsWith("_acme-challenge")) {
|
||||
task.incrementSkip();
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await this.add({
|
||||
userId,
|
||||
projectId,
|
||||
groupId,
|
||||
domain: fullRecord,
|
||||
name: fullRecord,
|
||||
httpsPort: 443,
|
||||
} as any);
|
||||
} catch (e) {
|
||||
if (e.message && e.message.indexOf("已达上限") >= 0) {
|
||||
task.addError(`${fullRecord}: ${e.message}`);
|
||||
return;
|
||||
}
|
||||
task.incrementSkip();
|
||||
}
|
||||
}
|
||||
if (!pageRet.list || pageRet.list.length < recordPager.pageSize) {
|
||||
break;
|
||||
}
|
||||
recordPager.pageNo++;
|
||||
}
|
||||
}
|
||||
task.setTotal(task.current || task.total || 0);
|
||||
logger.info(`从域名提供商${dnsProviderType}导入站点完成,共处理${task.current}个记录,跳过${task.getSkipCount()}个,成功${task.getSuccessCount()}个,失败${task.getErrorCount()}个`);
|
||||
}
|
||||
|
||||
async getSiteInfoImportTaskStatus(req: { userId?: number; projectId?: number }) {
|
||||
const userId = req.userId || 0;
|
||||
const projectId = req.projectId;
|
||||
const setting = await this.userSettingsService.getSetting<UserSiteInfoImportSetting>(userId, projectId, UserSiteInfoImportSetting);
|
||||
const list = setting?.siteInfoImportList || [];
|
||||
const TASK_TYPE = "siteInfoImportTask";
|
||||
const taskList: any = [];
|
||||
for (const item of list) {
|
||||
const { key } = item;
|
||||
const task = taskExecutor.get(TASK_TYPE, key);
|
||||
taskList.push({ ...item, task });
|
||||
}
|
||||
return taskList;
|
||||
}
|
||||
|
||||
async getSiteInfoImportProviderTitle(req: { userId?: number; projectId?: number; dnsProviderType: string; dnsProviderAccessId: number }) {
|
||||
const userId = req.userId || 0;
|
||||
const projectId = req.projectId;
|
||||
const { dnsProviderType, dnsProviderAccessId } = req;
|
||||
const dnsProviderDefine = dnsProviderRegistry.getDefine(dnsProviderType);
|
||||
if (!dnsProviderDefine) {
|
||||
throw new Error(`该域名提供商(${dnsProviderType})不存在,请检查是否已被注册`);
|
||||
}
|
||||
const access = await this.accessService.getSimpleInfo(dnsProviderAccessId);
|
||||
if (!access || access.userId !== userId) {
|
||||
throw new Error(`该授权(${dnsProviderAccessId})不存在,请检查是否已被删除`);
|
||||
}
|
||||
if (projectId && access.projectId !== projectId) {
|
||||
throw new Error(`该授权(${dnsProviderAccessId})不存在,请检查是否已被删除`);
|
||||
}
|
||||
return {
|
||||
title: `${dnsProviderDefine.title}_${access.name || ""}`,
|
||||
icon: dnsProviderDefine.icon || "",
|
||||
};
|
||||
}
|
||||
|
||||
async addSiteInfoImportTask(req: { userId?: number; projectId?: number; dnsProviderType: string; dnsProviderAccessId: number; index?: number; groupId?: number }) {
|
||||
const userId = req.userId || 0;
|
||||
const projectId = req.projectId;
|
||||
const { dnsProviderType, dnsProviderAccessId, index = 0, groupId } = req;
|
||||
const key = `user_${userId}_${dnsProviderType}_${dnsProviderAccessId}`;
|
||||
const { title, icon } = await this.getSiteInfoImportProviderTitle(req);
|
||||
const setting = await this.userSettingsService.getSetting<UserSiteInfoImportSetting>(userId, projectId, UserSiteInfoImportSetting);
|
||||
setting.siteInfoImportList = setting.siteInfoImportList || [];
|
||||
if (setting.siteInfoImportList.find(item => item.key === key)) {
|
||||
throw new Error(`该站点监控导入任务${key}已存在`);
|
||||
}
|
||||
const access = await this.accessService.getAccessById(dnsProviderAccessId, true, userId, projectId);
|
||||
if (!access) {
|
||||
throw new Error(`该授权(${dnsProviderAccessId})不存在,请检查是否已被删除`);
|
||||
}
|
||||
const item = { dnsProviderType, dnsProviderAccessId, key, title, icon: icon || "", groupId };
|
||||
setting.siteInfoImportList.splice(index, 0, item);
|
||||
await this.userSettingsService.saveSetting(userId, projectId, setting);
|
||||
return item;
|
||||
}
|
||||
|
||||
async deleteSiteInfoImportTask(req: { userId?: number; projectId?: number; key: string }) {
|
||||
const userId = req.userId || 0;
|
||||
const projectId = req.projectId;
|
||||
const { key } = req;
|
||||
const setting = await this.userSettingsService.getSetting<UserSiteInfoImportSetting>(userId, projectId, UserSiteInfoImportSetting);
|
||||
setting.siteInfoImportList = setting.siteInfoImportList || [];
|
||||
const index = setting.siteInfoImportList.findIndex(item => item.key === key);
|
||||
if (index === -1) {
|
||||
throw new Error(`该站点监控导入任务${key}不存在`);
|
||||
}
|
||||
setting.siteInfoImportList.splice(index, 1);
|
||||
const TASK_TYPE = "siteInfoImportTask";
|
||||
taskExecutor.clear(TASK_TYPE, key);
|
||||
await this.userSettingsService.saveSetting(userId, projectId, setting);
|
||||
}
|
||||
|
||||
async saveSiteInfoImportTask(req: { userId?: number; projectId?: number; dnsProviderType: string; dnsProviderAccessId: number; key?: string; groupId?: number }) {
|
||||
const userId = req.userId || 0;
|
||||
const projectId = req.projectId;
|
||||
const { dnsProviderType, dnsProviderAccessId, key, groupId } = req;
|
||||
const setting = await this.userSettingsService.getSetting<UserSiteInfoImportSetting>(userId, projectId, UserSiteInfoImportSetting);
|
||||
setting.siteInfoImportList = setting.siteInfoImportList || [];
|
||||
let index = 0;
|
||||
if (key) {
|
||||
index = setting.siteInfoImportList.findIndex(item => item.key === key);
|
||||
if (index === -1) {
|
||||
throw new Error(`该站点监控导入任务${key}不存在`);
|
||||
}
|
||||
await this.deleteSiteInfoImportTask({ userId, projectId, key });
|
||||
}
|
||||
return await this.addSiteInfoImportTask({ userId, projectId, dnsProviderType, dnsProviderAccessId, index, groupId });
|
||||
}
|
||||
|
||||
clearSiteMonitorJob(userId: number, projectId?: number) {
|
||||
this.cron.remove(`siteMonitor_${userId}_${projectId || ""}`);
|
||||
}
|
||||
|
||||
+30
-1
@@ -1,4 +1,4 @@
|
||||
import { AbstractDnsProvider, CreateRecordOptions, DomainRecord, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert";
|
||||
import { AbstractDnsProvider, CreateRecordOptions, DomainRecord, DnsResolveRecord, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert";
|
||||
import { AliyunAccess } from "../../plugin-lib/aliyun/access/aliyun-access.js";
|
||||
import { AliyunClient } from "../../plugin-lib/aliyun/index.js";
|
||||
import { Pager, PageRes, PageSearch } from "@certd/pipeline";
|
||||
@@ -177,6 +177,35 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
|
||||
total: ret.TotalCount,
|
||||
};
|
||||
}
|
||||
|
||||
async getRecordListPage(domain: string, req: PageSearch): Promise<PageRes<DnsResolveRecord>> {
|
||||
const pager = new Pager(req);
|
||||
const params = {
|
||||
RegionId: "cn-hangzhou",
|
||||
DomainName: domain,
|
||||
PageSize: pager.pageSize,
|
||||
PageNumber: pager.pageNo,
|
||||
};
|
||||
|
||||
const requestOption = {
|
||||
method: "POST",
|
||||
};
|
||||
|
||||
const ret = await this.client.request("DescribeDomainRecords", params, requestOption);
|
||||
const rawList = ret.DomainRecords?.Record || [];
|
||||
const list = rawList.map(item => ({
|
||||
id: item.RecordId,
|
||||
hostRecord: item.RR,
|
||||
fullRecord: item.RR === "@" ? domain : `${item.RR}.${domain}`,
|
||||
type: item.Type,
|
||||
value: item.Value,
|
||||
}));
|
||||
|
||||
return {
|
||||
list,
|
||||
total: ret.TotalCount,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
new AliyunDnsProvider();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AbstractDnsProvider, CreateRecordOptions, DomainRecord, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert";
|
||||
import { AbstractDnsProvider, CreateRecordOptions, DomainRecord, DnsResolveRecord, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert";
|
||||
|
||||
import { CloudflareAccess } from "./access.js";
|
||||
import { Pager, PageRes, PageSearch } from "@certd/pipeline";
|
||||
@@ -137,6 +137,34 @@ export class CloudflareDnsProvider extends AbstractDnsProvider<CloudflareRecord>
|
||||
list,
|
||||
};
|
||||
}
|
||||
|
||||
async getRecordListPage(domain: string, req: PageSearch): Promise<PageRes<DnsResolveRecord>> {
|
||||
const pager = new Pager(req);
|
||||
|
||||
const zoneId = await this.getZoneId(domain);
|
||||
let url = `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records?page=${pager.pageNo}&per_page=${pager.pageSize}`;
|
||||
if (req.searchKey) {
|
||||
url += `&name=${req.searchKey}`;
|
||||
}
|
||||
const ret = await this.access.doRequestApi(url, null, "get");
|
||||
|
||||
let list = ret.result || [];
|
||||
list = list.map((item: any) => {
|
||||
const hostRecord = item.name === domain ? "@" : item.name.slice(0, item.name.length - domain.length - 1);
|
||||
return {
|
||||
id: item.id,
|
||||
hostRecord,
|
||||
fullRecord: item.name,
|
||||
type: item.type,
|
||||
value: item.content,
|
||||
};
|
||||
});
|
||||
const total = ret.result_info.total_count || list.length;
|
||||
return {
|
||||
total,
|
||||
list,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
//实例化这个provider,将其自动注册到系统中
|
||||
|
||||
-10
@@ -30,16 +30,6 @@ export class HipmDnsmgrDnsProvider extends AbstractDnsProvider<{ domainId: strin
|
||||
// 1. 获取域名 ID(双层查询策略)
|
||||
const domainId = await this.access.getDomainId(domain);
|
||||
this.logger.debug('[HiPM DNSMgr] 找到域名:', domain, 'ID:', domainId);
|
||||
// 1. 获取域名列表,找到对应的域名 ID
|
||||
const domainList = await this.access.getDomainList();
|
||||
const domainInfo = domainList.find((item: any) => item.domain === domain);
|
||||
|
||||
if (!domainInfo) {
|
||||
throw new Error(`[HiPM DNSMgr] 未找到域名:${domain}`);
|
||||
}
|
||||
|
||||
const domainId = String(domainInfo.id);
|
||||
this.logger.debug("[HiPM DNSMgr] 找到域名:", domain, "ID:", domainId);
|
||||
|
||||
// 2. 创建 DNS 记录
|
||||
const name = hostRecord; // 使用子域名,如 _acme-challenge
|
||||
|
||||
+25
-1
@@ -1,4 +1,4 @@
|
||||
import { AbstractDnsProvider, CreateRecordOptions, DomainRecord, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert";
|
||||
import { AbstractDnsProvider, CreateRecordOptions, DnsResolveRecord, DomainRecord, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert";
|
||||
import { TencentAccess } from "../../plugin-lib/tencent/index.js";
|
||||
import { Pager, PageRes, PageSearch } from "@certd/pipeline";
|
||||
|
||||
@@ -114,5 +114,29 @@ export class TencentDnsProvider extends AbstractDnsProvider {
|
||||
const total = ret.DomainCountInfo?.AllTotal || list.length;
|
||||
return { total, list };
|
||||
}
|
||||
|
||||
async getRecordListPage(domain: string, req: PageSearch): Promise<PageRes<DnsResolveRecord>> {
|
||||
const pager = new Pager(req);
|
||||
|
||||
const params: any = {
|
||||
Domain: domain,
|
||||
Offset: pager.getOffset(),
|
||||
Limit: pager.pageSize,
|
||||
};
|
||||
if (req.searchKey) {
|
||||
params.Subdomain = req.searchKey;
|
||||
}
|
||||
const ret = await this.client.DescribeRecordList(params);
|
||||
let list = ret.RecordList || [];
|
||||
list = list.map((item: any) => ({
|
||||
id: String(item.RecordId),
|
||||
hostRecord: item.Name,
|
||||
fullRecord: item.Name === "@" ? domain : `${item.Name}.${domain}`,
|
||||
type: item.Type,
|
||||
value: item.Value,
|
||||
}));
|
||||
const total = ret.TotalCount || list.length;
|
||||
return { total, list };
|
||||
}
|
||||
}
|
||||
new TencentDnsProvider();
|
||||
|
||||
@@ -1 +1 @@
|
||||
02:38
|
||||
23:31
|
||||
|
||||
Reference in New Issue
Block a user