feat: 域名验证方法支持CNAME间接方式,此方式支持所有域名注册商,且无需提供Access授权,但是需要手动添加cname解析

This commit is contained in:
xiaojunnuo
2024-10-07 03:21:16 +08:00
parent 0c8e83e125
commit f3d35084ed
123 changed files with 2373 additions and 456 deletions
@@ -22,6 +22,9 @@
<script lang="ts" setup>
import { ref } from "vue";
defineOptions({
name: "CronEditor"
});
const props = defineProps<{
modelValue?: string;
disabled?: boolean;
@@ -1,97 +0,0 @@
<template>
<a-select class="pi-dns-provider-selector" :value="modelValue" :options="options" @update:value="onChanged">
</a-select>
</template>
<script lang="ts">
import { inject, Ref, ref, watch } from "vue";
import * as api from "./api";
export default {
name: "PiDnsProviderSelector",
props: {
modelValue: {
type: String,
default: undefined
}
},
emits: ["update:modelValue"],
setup(props:any, ctx:any) {
const options = ref<any[]>([]);
async function onCreate() {
const list = await api.GetList();
const array: any[] = [];
for (let item of list) {
array.push({
value: item.name,
label: item.title
});
}
options.value = array;
if (props.modelValue == null && options.value.length > 0) {
ctx.emit("update:modelValue", options.value[0].value);
}
}
onCreate();
function onChanged(value:any) {
ctx.emit("update:modelValue", value);
}
return {
options,
onChanged
};
}
};
</script>
<style lang="less">
.step-edit-form {
.body {
padding: 10px;
.ant-card {
margin-bottom: 10px;
&.current {
border-color: #00b7ff;
}
.ant-card-meta-title {
display: flex;
flex-direction: row;
justify-content: flex-start;
}
.ant-avatar {
width: 24px;
height: 24px;
flex-shrink: 0;
}
.title {
margin-left: 5px;
white-space: nowrap;
flex: 1;
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
}
.ant-card-body {
padding: 14px;
height: 100px;
overflow-y: hidden;
.ant-card-meta-description {
font-size: 10px;
line-height: 20px;
height: 40px;
color: #7f7f7f;
}
}
}
}
</style>
@@ -1,5 +1,5 @@
<template>
<div class="pi-editable" :class="{ disabled, 'hover-show': hoverShow }">
<div class="text-editable" :class="{ disabled, 'hover-show': hoverShow }">
<div v-if="isEdit" class="input">
<a-input ref="inputRef" v-model:value="valueRef" :validate-status="modelValue ? '' : 'error'" v-bind="input" @keyup.enter="save()" @blur="save()">
<template #suffix>
@@ -18,7 +18,7 @@
import { watch, ref, nextTick } from "vue";
export default {
name: "PiEditable",
name: "TextEditable",
props: {
modelValue: {
type: String,
@@ -73,7 +73,7 @@ export default {
</script>
<style lang="less">
.pi-editable {
.text-editable {
flex: 1;
line-height: 34px;
@@ -1,8 +1,5 @@
import PiContainer from "./container.vue";
import PiAccessSelector from "../views/certd/access/access-selector/index.vue";
import PiDnsProviderSelector from "./dns-provider-selector/index.vue";
import PiOutputSelector from "../views/certd/pipeline/pipeline/component/output-selector/index.vue";
import PiEditable from "./editable.vue";
import TextEditable from "./editable.vue";
import vip from "./vip-button/install.js";
import { CheckCircleOutlined, InfoCircleOutlined, UndoOutlined } from "@ant-design/icons-vue";
import CronEditor from "./cron-editor/index.vue";
@@ -13,10 +10,7 @@ import Plugins from "./plugins/index";
export default {
install(app: any) {
app.component("PiContainer", PiContainer);
app.component("PiAccessSelector", PiAccessSelector);
app.component("PiEditable", PiEditable);
app.component("PiOutputSelector", PiOutputSelector);
app.component("PiDnsProviderSelector", PiDnsProviderSelector);
app.component("TextEditable", TextEditable);
app.component("CronLight", CronLight);
app.component("CronEditor", CronEditor);
@@ -0,0 +1,48 @@
<template>
<a-select class="dns-provider-selector" :value="modelValue" :options="options" @update:value="onChanged"> </a-select>
</template>
<script lang="ts">
import { ref } from "vue";
import * as api from "./api";
export default {
name: "DnsProviderSelector",
props: {
modelValue: {
type: String,
default: undefined
}
},
emits: ["update:modelValue"],
setup(props: any, ctx: any) {
const options = ref<any[]>([]);
async function onCreate() {
const list = await api.GetList();
const array: any[] = [];
for (let item of list) {
array.push({
value: item.name,
label: item.title
});
}
options.value = array;
if (props.modelValue == null && options.value.length > 0) {
ctx.emit("update:modelValue", options.value[0].value);
}
}
onCreate();
function onChanged(value: any) {
ctx.emit("update:modelValue", value);
}
return {
options,
onChanged
};
}
};
</script>
<style lang="less"></style>
@@ -0,0 +1,26 @@
import { request } from "/src/api/service";
const apiPrefix = "/cname/record";
export type CnameRecord = {
id: number;
status: string;
};
export async function GetList() {
return await request({
url: apiPrefix + "/list",
method: "post"
});
}
export async function GetByDomain(domain: string) {
return await request({
url: apiPrefix + "/getByDomain",
method: "post",
data: {
domain,
createOnNotFound: true
}
});
}
@@ -0,0 +1,82 @@
<template>
<tr v-if="cnameRecord" class="cname-record-info">
<!-- <td class="domain">-->
<!-- {{ props.domain }}-->
<!-- </td>-->
<td class="host-record" :title="'域名:' + props.domain">
<fs-copyable v-model="cnameRecord.hostRecord"></fs-copyable>
</td>
<td class="record-value">
<fs-copyable v-model="cnameRecord.recordValue"></fs-copyable>
</td>
<td class="status center flex-center">
<fs-values-format v-model="cnameRecord.status" :dict="statusDict" />
<fs-icon icon="ion:refresh-outline" class="pointer" @click="doRefresh"></fs-icon>
</td>
</tr>
</template>
<script lang="ts" setup>
import { CnameRecord, GetByDomain } from "/@/components/plugins/cert/domains-verify-plan-editor/api";
import { ref, watch } from "vue";
import { dict } from "@fast-crud/fast-crud";
const statusDict = dict({
data: [
{ label: "待设置CNAME", value: "cname", color: "warning" },
{ label: "验证中", value: "validating", color: "primary" },
{ label: "验证成功", value: "valid", color: "success" },
{ label: "验证失败", value: "failed", color: "error" }
]
});
defineOptions({
name: "CnameRecordInfo"
});
const props = defineProps<{
domain: string;
}>();
const emit = defineEmits<{
change: {
id: number | null;
status: string | null;
};
}>();
const cnameRecord = ref<CnameRecord | null>(null);
function onRecordChange() {
emit("change", {
id: cnameRecord.value?.id,
status: cnameRecord.value?.status
});
}
async function doRefresh() {
if (!props.domain) {
return;
}
cnameRecord.value = await GetByDomain(props.domain);
onRecordChange();
}
watch(
() => props.domain,
async (value) => {
await doRefresh();
},
{
immediate: true
}
);
</script>
<style lang="less">
.cname-record-info {
.fs-copyable {
width: 100%;
}
}
</style>
@@ -0,0 +1,67 @@
<template>
<table class="cname-verify-plan">
<tr>
<td style="width: 160px">主机记录</td>
<td style="width: 250px">请设置CNAME记录</td>
<td style="width: 120px" class="center">状态</td>
</tr>
<template v-for="key in domains" :key="key">
<cname-record-info :domain="key" @change="onRecordChange(key, $event)" />
</template>
</table>
</template>
<script lang="ts" setup>
import { CnameRecord } from "/@/components/plugins/cert/domains-verify-plan-editor/api";
import CnameRecordInfo from "/@/components/plugins/cert/domains-verify-plan-editor/cname-record-info.vue";
import { computed } from "vue";
defineOptions({
name: "CnameVerifyPlan"
});
const emit = defineEmits<{
"update:modelValue": any;
change: Record<string, any>;
}>();
const props = defineProps<{
modelValue: Record<string, any>;
}>();
const domains = computed(() => {
return Object.keys(props.modelValue);
});
function onRecordChange(domain: string, record: CnameRecord) {
const value = { ...props.modelValue };
value[domain] = record;
emit("update:modelValue", value);
emit("change", value);
}
</script>
<style lang="less">
.cname-verify-plan {
width: 100%;
table-layout: fixed;
tr {
td {
border: 0 !important;
border-bottom: 1px solid #e8e8e8 !important;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&.center {
text-align: center;
}
}
&:last-child {
td {
border-bottom: 0 !important;
}
}
}
}
</style>
@@ -0,0 +1,315 @@
<template>
<div class="domains-verify-plan-editor" :class="{ fullscreen }">
<div class="fullscreen-modal" @click="fullscreenExit"></div>
<div class="plan-wrapper">
<div class="plan-box">
<div class="fullscreen-button pointer">
<fs-icon :icon="fullscreen ? 'material-symbols:fullscreen' : 'material-symbols:fullscreen-exit'" @click="fullscreen = !fullscreen"></fs-icon>
</div>
<table class="plan-table">
<tr>
<th>域名</th>
<th>验证方式</th>
<th>验证计划</th>
</tr>
<tr v-for="(item, key) of planRef" :key="key" class="row">
<td>{{ item.domain }}</td>
<td>
<div class="type">
<a-select v-model:value="item.type" size="small" :options="challengeTypeOptions" @change="onPlanChanged"></a-select>
</div>
</td>
<td style="padding: 0">
<div class="plan">
<div v-if="item.type === 'dns'" class="plan-dns">
<div class="form-item">
<span class="label">DNS类型</span>
<span class="input">
<fs-dict-select
v-model="item.dnsProviderType"
size="small"
:dict="dnsProviderTypeDict"
placeholder="DNS提供商"
@change="onPlanChanged"
></fs-dict-select>
</span>
</div>
<a-divider type="vertical" />
<div class="form-item">
<span class="label">DNS授权</span>
<span class="input">
<access-selector
v-model="item.dnsProviderAccessId"
size="small"
:type="item.dnsProviderType"
placeholder="请选择"
@change="onPlanChanged"
></access-selector>
</span>
</div>
</div>
<div v-if="item.type === 'cname'" class="plan-cname">
<cname-verify-plan v-model="item.cnameVerifyPlan" @change="onPlanChanged" />
</div>
</div>
</td>
</tr>
</table>
<div class="error">
{{ errorMessageRef }}
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
import { dict, FsDictSelect } from "@fast-crud/fast-crud";
import AccessSelector from "/@/views/certd/access/access-selector/index.vue";
import CnameVerifyPlan from "./cname-verify-plan.vue";
import psl from "psl";
defineOptions({
name: "DomainsVerifyPlanEditor"
});
type DomainVerifyPlanInput = {
domain: string;
type: "cname" | "dns";
dnsProviderType?: string;
dnsProviderAccessId?: number;
cnameVerifyPlan?: Record<string, CnameRecord>;
};
type DomainsVerifyPlanInput = {
[key: string]: DomainVerifyPlanInput;
};
const challengeTypeOptions = ref<any[]>([
{
label: "DNS验证",
value: "dns"
},
{
label: "CNAME验证",
value: "cname"
}
]);
const props = defineProps<{
modelValue?: DomainsVerifyPlanInput;
domains?: string[];
}>();
const emit = defineEmits<{
"update:modelValue": any;
}>();
const fullscreen = ref(false);
function fullscreenExit() {
if (fullscreen.value) {
fullscreen.value = false;
}
}
const planRef = ref<DomainsVerifyPlanInput>(props.modelValue || {});
const dnsProviderTypeDict = dict({
url: "pi/dnsProvider/dnsProviderTypeDict"
});
function onPlanChanged() {
debugger;
emit("update:modelValue", planRef.value);
}
const errorMessageRef = ref<string>("");
function showError(error: string) {
errorMessageRef.value = error;
}
type DomainGroup = Record<
string,
{
[key: string]: CnameRecord;
}
>[];
function onDomainsChanged(domains: string[]) {
console.log("域名变化", domains);
if (domains == null) {
return;
}
const domainGroups: DomainGroup = {};
for (let domain of domains) {
domain = domain.replace("*.", "");
const parsed = psl.parse(domain);
if (parsed.error) {
showError(`域名${domain}解析失败: ${JSON.stringify(parsed.error)}`);
continue;
}
const mainDomain = parsed.domain;
let group = domainGroups[mainDomain];
if (!group) {
group = {};
domainGroups[mainDomain] = group;
}
group[domain] = {
id: 0
};
}
for (const domain in domainGroups) {
let planItem = planRef.value[domain];
const subDomains = domainGroups[domain];
if (!planItem) {
planItem = {
domain,
type: "cname",
cnameVerifyPlan: {
...subDomains
}
};
planRef.value[domain] = planItem;
} else {
const cnamePlan = planItem.cnameVerifyPlan;
for (const subDomain in subDomains) {
if (!cnamePlan[subDomain]) {
cnamePlan[subDomain] = {
id: 0
};
}
}
for (const subDomain of Object.keys(cnamePlan)) {
if (!subDomains[subDomain]) {
delete cnamePlan[subDomain];
}
}
}
}
for (const domain of Object.keys(planRef.value)) {
const mainDomains = Object.keys(domainGroups);
if (!mainDomains.includes(domain)) {
delete planRef.value[domain];
}
}
}
watch(
() => {
return props.domains;
},
(domains: string[]) => {
onDomainsChanged(domains);
},
{
immediate: true,
deep: true
}
);
</script>
<style lang="less">
.domains-verify-plan-editor {
width: 100%;
min-height: 100px;
overflow-x: auto;
.fullscreen-modal {
display: none;
}
&.fullscreen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(74, 74, 74, 0.78);
z-index: 1000;
padding: 100px;
margin: auto;
.plan-wrapper {
width: 1400px;
margin: auto;
//background-color: #a3a3a3;
//padding: 50px;
.plan-box {
position: relative;
margin: auto;
background-color: #fff;
}
}
.fullscreen-modal {
display: block;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
}
}
.fullscreen-button {
position: absolute;
right: 10px;
top: 10px;
z-index: 1001;
}
.plan-table {
width: 100%;
height: 100%;
//table-layout: fixed;
th {
background-color: #f5f5f5;
border-top: 1px solid #e8e8e8;
border-left: 1px solid #e8e8e8;
border-bottom: 1px solid #e8e8e8;
text-align: left;
padding: 10px 6px;
}
td {
border-bottom: 1px solid #e8e8e8;
border-left: 1px solid #e8e8e8;
padding: 6px 6px;
}
.plan {
font-size: 14px;
.ant-select {
width: 100%;
}
.plan-dns {
display: flex;
flex-direction: row;
justify-content: start;
align-items: center;
.form-item {
min-width: 250px;
display: flex;
justify-content: center;
align-items: center;
.label {
width: 80px;
}
.input {
width: 150px;
}
}
}
.plan-cname {
.cname-row {
display: flex;
flex-direction: row;
.domain {
width: 100px;
}
.cname-record {
flex: 1;
}
}
}
}
}
}
</style>
@@ -1,6 +1,10 @@
<script setup lang="ts">
import { inject, ref, watch } from "vue";
defineOptions({
name: "CertDomainsGetter"
});
const props = defineProps<{
inputKey?: string;
modelValue?: string[];
@@ -0,0 +1,73 @@
<template>
<a-select class="output-selector" :value="modelValue" :options="options" @update:value="onChanged"> </a-select>
</template>
<script lang="ts">
import { inject, onMounted, Ref, ref, watch } from "vue";
export default {
name: "OutputSelector",
props: {
modelValue: {
type: String,
default: undefined
},
// eslint-disable-next-line vue/require-default-prop
from: {
type: [String, Array]
}
},
emits: ["update:modelValue"],
setup(props: any, ctx: any) {
const options = ref<any[]>([]);
const pipeline = inject("pipeline") as Ref<any>;
const currentStageIndex = inject("currentStageIndex") as Ref<number>;
const currentStepIndex = inject("currentStepIndex") as Ref<number>;
const currentTask = inject("currentTask") as Ref<any>;
const getPluginGroups = inject("getPluginGroups") as any;
const pluginGroups = getPluginGroups();
function onCreate() {
options.value = pluginGroups.getPreStepOutputOptions({
pipeline: pipeline.value,
currentStageIndex: currentStageIndex.value,
currentStepIndex: currentStepIndex.value,
currentTask: currentTask.value
});
if (props.from) {
if (typeof props.from === "string") {
options.value = options.value.filter((item: any) => item.type === props.from);
} else {
options.value = options.value.filter((item: any) => props.from.includes(item.type));
}
}
if (props.modelValue == null && options.value.length > 0) {
ctx.emit("update:modelValue", options.value[0].value);
}
}
onMounted(() => {
onCreate();
});
watch(
() => {
return pluginGroups.value?.map;
},
() => {
onCreate();
}
);
function onChanged(value: any) {
ctx.emit("update:modelValue", value);
}
return {
options,
onChanged
};
}
};
</script>
<style lang="less"></style>
@@ -2,6 +2,10 @@
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
import { ref, watch } from "vue";
defineOptions({
name: "RemoteSelect"
});
const props = defineProps<
{
watches: string[];
@@ -63,14 +67,14 @@ watch(
<template>
<div>
<a-select
class="remote-select"
show-search
:filter-option="filterOption"
:options="optionsRef"
:value="value"
@click="onClick"
@update:value="emit('update:value', $event)"
/>
class="remote-select"
show-search
:filter-option="filterOption"
:options="optionsRef"
:value="value"
@click="onClick"
@update:value="emit('update:value', $event)"
/>
<div class="helper">
{{ message }}
</div>
@@ -1,8 +1,18 @@
import SynologyIdDeviceGetter from "./synology/device-id-getter.vue";
import RemoteSelect from "./common/remote-select.vue";
import CertDomainsGetter from "./common/cert-domains-getter.vue";
import OutputSelector from "/@/components/plugins/common/output-selector/index.vue";
import DnsProviderSelector from "/@/components/plugins/cert/dns-provider-selector/index.vue";
import DomainsVerifyPlanEditor from "/@/components/plugins/cert/domains-verify-plan-editor/index.vue";
import AccessSelector from "/@/views/certd/access/access-selector/index.vue";
export default {
install(app: any) {
app.component("OutputSelector", OutputSelector);
app.component("DnsProviderSelector", DnsProviderSelector);
app.component("DomainsVerifyPlanEditor", DomainsVerifyPlanEditor);
app.component("AccessSelector", AccessSelector);
app.component("SynologyDeviceIdGetter", SynologyIdDeviceGetter);
app.component("RemoteSelect", RemoteSelect);
app.component("CertDomainsGetter", CertDomainsGetter);
@@ -14,6 +14,10 @@ import { defineProps, ref, useAttrs } from "vue";
import { Modal } from "ant-design-vue";
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
defineOptions({
name: "DeviceIdGetter"
});
const props = defineProps<ComponentPropsType>();
const emit = defineEmits<{