mirror of
https://github.com/certd/certd.git
synced 2026-06-19 16:07:33 +08:00
feat: 支持dns-persist-01持久化验证方式申请证书,优化Acme账号的存储方式
This commit is contained in:
+15
-8
@@ -11,14 +11,16 @@
|
||||
<td class="record-value" :title="cnameRecord.recordValue">
|
||||
<fs-copyable v-model="cnameRecord.recordValue"></fs-copyable>
|
||||
</td>
|
||||
<td class="status center flex-center">
|
||||
<fs-values-format v-model="cnameRecord.status" :dict="statusDict" />
|
||||
<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="t('certd.verifyPlan.resetStatusTooltip')">
|
||||
<fs-icon class="ml-2 color-yellow text-md pointer" icon="solar:undo-left-square-bold" @click="resetStatus"></fs-icon>
|
||||
</a-tooltip>
|
||||
<td class="status center">
|
||||
<span class="status-content">
|
||||
<fs-values-format v-model="cnameRecord.status" :dict="statusDict" />
|
||||
<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="t('certd.verifyPlan.resetStatusTooltip')">
|
||||
<fs-icon class="ml-2 color-yellow text-md pointer" icon="solar:undo-left-square-bold" @click="resetStatus"></fs-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</td>
|
||||
<td class="center">
|
||||
<template v-if="cnameRecord.status !== 'valid'">
|
||||
@@ -142,5 +144,10 @@ async function resetStatus() {
|
||||
.fs-copyable {
|
||||
width: 100%;
|
||||
}
|
||||
.status-content {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+20
-5
@@ -2,11 +2,11 @@
|
||||
<table class="cname-verify-plan">
|
||||
<thead>
|
||||
<tr>
|
||||
<td style="width: 160px">{{ t("certd.verifyPlan.hostRecord") }}</td>
|
||||
<td style="width: 100px; text-align: center">{{ t("certd.verifyPlan.recordType") }}</td>
|
||||
<td style="width: 250px">{{ t("certd.verifyPlan.setCnameRecord") }}</td>
|
||||
<td style="width: 120px" class="center">{{ t("certd.status") }}</td>
|
||||
<td style="width: 90px" class="center">{{ t("certd.verifyPlan.operation") }}</td>
|
||||
<td class="col-host">{{ t("certd.verifyPlan.hostRecord") }}</td>
|
||||
<td class="col-type center">{{ t("certd.verifyPlan.recordType") }}</td>
|
||||
<td class="col-value">{{ t("certd.verifyPlan.setCnameRecord") }}</td>
|
||||
<td class="col-status center">{{ t("certd.status") }}</td>
|
||||
<td class="col-action center">{{ t("certd.verifyPlan.operation") }}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<template v-for="key in domains" :key="key">
|
||||
@@ -49,6 +49,21 @@ function onRecordChange(domain: string, record: CnameRecord) {
|
||||
.cname-verify-plan {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
.col-host {
|
||||
width: 220px;
|
||||
}
|
||||
.col-type {
|
||||
width: 100px;
|
||||
}
|
||||
.col-value {
|
||||
width: 360px;
|
||||
}
|
||||
.col-status {
|
||||
width: 120px;
|
||||
}
|
||||
.col-action {
|
||||
width: 150px;
|
||||
}
|
||||
tbody tr td {
|
||||
border-top: 1px solid #e8e8e8 !important;
|
||||
}
|
||||
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<tbody class="dns-persist-record-info">
|
||||
<tr v-if="dnsPersistRecord">
|
||||
<td class="host-record" :title="dnsPersistRecord.hostRecord">
|
||||
<fs-copyable v-model="dnsPersistRecord.hostRecord"></fs-copyable>
|
||||
</td>
|
||||
<td style="text-align: center">TXT</td>
|
||||
<td class="record-value" :title="dnsPersistRecord.recordValue">
|
||||
<fs-copyable v-model="dnsPersistRecord.recordValue"></fs-copyable>
|
||||
</td>
|
||||
<td class="status center">
|
||||
<fs-values-format v-model="dnsPersistRecord.status" :dict="statusDict" />
|
||||
</td>
|
||||
<td class="center">
|
||||
<template v-if="dnsPersistRecord.status !== 'valid'">
|
||||
<a-space>
|
||||
<a-button type="primary" size="small" @click="openSettingDialog">设置TXT</a-button>
|
||||
<a-button type="primary" size="small" :loading="loading" @click="doVerify">校验</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<div v-else class="helper">请勿删除TXT记录</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td colspan="5" class="color-red">{{ errorMessage || "请先选择ACME账号授权" }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { dict } from "@fast-crud/fast-crud";
|
||||
import { message } from "ant-design-vue";
|
||||
import { ref, watch } from "vue";
|
||||
import { GetByDomain, Verify } from "/@/views/certd/cert/dns-persist/api";
|
||||
import { useDnsPersistSettingDialog } from "/@/views/certd/cert/dns-persist/use-setting-dialog";
|
||||
import { DnsPersistRecord } from "./type";
|
||||
|
||||
defineOptions({
|
||||
name: "DnsPersistRecordInfo",
|
||||
});
|
||||
|
||||
const props = defineProps<{
|
||||
domain: string;
|
||||
caType?: string;
|
||||
acmeAccountAccessId?: number;
|
||||
commonAcmeAccountAccessId?: number;
|
||||
wildcard?: boolean;
|
||||
persistUntil?: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [DnsPersistRecord];
|
||||
}>();
|
||||
|
||||
const statusDict = dict({
|
||||
data: [
|
||||
{ value: "pending", label: "待设置", color: "warning" },
|
||||
{ value: "validating", label: "校验中", color: "blue" },
|
||||
{ value: "valid", label: "有效", color: "green" },
|
||||
{ value: "failed", label: "请重试", color: "red" },
|
||||
],
|
||||
});
|
||||
|
||||
const dnsPersistRecord = ref<DnsPersistRecord | null>(null);
|
||||
const loading = ref(false);
|
||||
const errorMessage = ref("");
|
||||
const { openDnsPersistSettingDialog } = useDnsPersistSettingDialog();
|
||||
|
||||
function onRecordChange() {
|
||||
if (dnsPersistRecord.value) {
|
||||
emit("change", dnsPersistRecord.value);
|
||||
} else {
|
||||
emit("change", {
|
||||
domain: props.domain,
|
||||
status: null,
|
||||
} as any);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRecord() {
|
||||
errorMessage.value = "";
|
||||
dnsPersistRecord.value = null;
|
||||
if (!props.domain || (!props.acmeAccountAccessId && !props.commonAcmeAccountAccessId)) {
|
||||
onRecordChange();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
dnsPersistRecord.value = await GetByDomain({
|
||||
domain: props.domain,
|
||||
caType: props.caType,
|
||||
acmeAccountAccessId: props.acmeAccountAccessId,
|
||||
commonAcmeAccountAccessId: props.commonAcmeAccountAccessId,
|
||||
wildcard: props.wildcard,
|
||||
persistUntil: props.persistUntil,
|
||||
createOnNotFound: true,
|
||||
});
|
||||
onRecordChange();
|
||||
} catch (e: any) {
|
||||
errorMessage.value = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.domain, props.caType, props.acmeAccountAccessId, props.commonAcmeAccountAccessId, props.wildcard, props.persistUntil],
|
||||
async () => {
|
||||
await loadRecord();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
async function doVerify() {
|
||||
if (!dnsPersistRecord.value?.id) {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const ok = await Verify(dnsPersistRecord.value.id);
|
||||
message[ok ? "success" : "error"](ok ? "校验成功" : "未找到匹配的TXT记录,请稍后重试");
|
||||
await loadRecord();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openSettingDialog() {
|
||||
if (!dnsPersistRecord.value) {
|
||||
return;
|
||||
}
|
||||
openDnsPersistSettingDialog({
|
||||
record: dnsPersistRecord.value,
|
||||
onDone: loadRecord,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.dns-persist-record-info {
|
||||
.fs-copyable {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<table class="dns-persist-verify-plan">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="col-host">TXT主机名</td>
|
||||
<td class="col-type center">记录类型</td>
|
||||
<td class="col-value">请设置TXT记录(验证成功以后不要删除)</td>
|
||||
<td class="col-status center">状态</td>
|
||||
<td class="col-action center">操作</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<template v-for="key in domains" :key="key">
|
||||
<dns-persist-record-info
|
||||
:domain="key"
|
||||
:ca-type="caType"
|
||||
:acme-account-access-id="acmeAccountAccessId"
|
||||
:common-acme-account-access-id="commonAcmeAccountAccessId"
|
||||
:wildcard="modelValue[key]?.wildcard"
|
||||
:persist-until="modelValue[key]?.persistUntil"
|
||||
@change="onRecordChange(key, $event)"
|
||||
/>
|
||||
</template>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import DnsPersistRecordInfo from "./dns-persist-record-info.vue";
|
||||
import { DnsPersistRecord } from "./type";
|
||||
|
||||
defineOptions({
|
||||
name: "DnsPersistVerifyPlan",
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "change"]);
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: Record<string, DnsPersistRecord>;
|
||||
caType?: string;
|
||||
acmeAccountAccessId?: number;
|
||||
commonAcmeAccountAccessId?: number;
|
||||
}>();
|
||||
|
||||
const domains = computed(() => {
|
||||
return Object.keys(props.modelValue || {});
|
||||
});
|
||||
|
||||
function onRecordChange(domain: string, record: DnsPersistRecord) {
|
||||
const value = { ...props.modelValue };
|
||||
value[domain] = {
|
||||
...value[domain],
|
||||
...record,
|
||||
};
|
||||
emit("update:modelValue", value);
|
||||
emit("change", value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.dns-persist-verify-plan {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
.col-host {
|
||||
width: 220px;
|
||||
}
|
||||
.col-type {
|
||||
width: 100px;
|
||||
}
|
||||
.col-value {
|
||||
width: 360px;
|
||||
}
|
||||
.col-status {
|
||||
width: 120px;
|
||||
}
|
||||
.col-action {
|
||||
width: 150px;
|
||||
}
|
||||
tbody tr td {
|
||||
border-top: 1px solid #e8e8e8 !important;
|
||||
}
|
||||
tr {
|
||||
td {
|
||||
border: 0 !important;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&.center {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+48
-1
@@ -46,13 +46,28 @@
|
||||
<div class="form-item">
|
||||
<span class="label">{{ t("certd.verifyPlan.dnsAccess") }}:</span>
|
||||
<span class="input">
|
||||
<access-selector v-model="item.dnsProviderAccessId" size="small" :type="item.dnsProviderAccessType || item.dnsProviderType" :placeholder="t('certd.verifyPlan.pleaseSelect')" @change="onPlanChanged"></access-selector>
|
||||
<access-selector
|
||||
v-model="item.dnsProviderAccessId"
|
||||
size="small"
|
||||
:type="item.dnsProviderAccessType || item.dnsProviderType"
|
||||
:placeholder="t('certd.verifyPlan.pleaseSelect')"
|
||||
@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 v-if="item.type === 'dns-persist'" class="plan-dns-persist">
|
||||
<dns-persist-verify-plan
|
||||
v-model="item.dnsPersistVerifyPlan"
|
||||
:ca-type="caType"
|
||||
:acme-account-access-id="acmeAccountAccessId"
|
||||
:common-acme-account-access-id="commonAcmeAccountAccessId"
|
||||
@change="onPlanChanged"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="item.type === 'http'" class="plan-http">
|
||||
<http-verify-plan v-model="item.httpVerifyPlan" @change="onPlanChanged" />
|
||||
<div class="helper">{{ t("certd.verifyPlan.httpHelper") }}</div>
|
||||
@@ -76,6 +91,7 @@ import { useI18n } from "vue-i18n";
|
||||
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 DnsPersistVerifyPlan from "./dns-persist-verify-plan.vue";
|
||||
import HttpVerifyPlan from "./http-verify-plan.vue";
|
||||
import { Form } from "ant-design-vue";
|
||||
import { DomainsVerifyPlanInput } from "./type";
|
||||
@@ -92,6 +108,10 @@ const challengeTypeOptions = ref<any[]>([
|
||||
label: t("certd.verifyPlan.dnsChallenge"),
|
||||
value: "dns",
|
||||
},
|
||||
{
|
||||
label: "DNS持久验证",
|
||||
value: "dns-persist",
|
||||
},
|
||||
{
|
||||
label: t("certd.verifyPlan.cnameChallenge"),
|
||||
value: "cname",
|
||||
@@ -106,6 +126,9 @@ const props = defineProps<{
|
||||
modelValue?: DomainsVerifyPlanInput;
|
||||
domains?: string[];
|
||||
defaultType?: string;
|
||||
caType?: string;
|
||||
acmeAccountAccessId?: number;
|
||||
commonAcmeAccountAccessId?: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -189,11 +212,15 @@ async function onDomainsChanged(domains: string[]) {
|
||||
|
||||
const cnameOrigin = planItem.cnameVerifyPlan;
|
||||
const httpOrigin = planItem.httpVerifyPlan;
|
||||
const dnsPersistOrigin = planItem.dnsPersistVerifyPlan;
|
||||
planItem.cnameVerifyPlan = {};
|
||||
planItem.httpVerifyPlan = {};
|
||||
planItem.dnsPersistVerifyPlan = {};
|
||||
const cnamePlan = planItem.cnameVerifyPlan;
|
||||
const httpPlan = planItem.httpVerifyPlan;
|
||||
const dnsPersistPlan = planItem.dnsPersistVerifyPlan;
|
||||
for (const subDomain of domainGroupItem.keySubDomains) {
|
||||
const wildcard = true;
|
||||
if (!cnameOrigin[subDomain]) {
|
||||
//@ts-ignore
|
||||
planItem.cnameVerifyPlan[subDomain] = {
|
||||
@@ -225,6 +252,19 @@ async function onDomainsChanged(domains: string[]) {
|
||||
domain: subDomain,
|
||||
};
|
||||
}
|
||||
|
||||
if (!dnsPersistOrigin?.[subDomain]) {
|
||||
//@ts-ignore
|
||||
dnsPersistPlan[subDomain] = {
|
||||
domain: subDomain,
|
||||
wildcard,
|
||||
};
|
||||
} else {
|
||||
dnsPersistPlan[subDomain] = {
|
||||
...dnsPersistOrigin[subDomain],
|
||||
wildcard,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for (const subDomain of Object.keys(cnamePlan)) {
|
||||
@@ -238,6 +278,12 @@ async function onDomainsChanged(domains: string[]) {
|
||||
delete httpPlan[subDomain];
|
||||
}
|
||||
}
|
||||
|
||||
for (const subDomain of Object.keys(dnsPersistPlan)) {
|
||||
if (!domainGroupItem.keySubDomains.includes(subDomain)) {
|
||||
delete dnsPersistPlan[subDomain];
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const domain of Object.keys(planRef.value)) {
|
||||
const mainDomains = Object.keys(domainGroups);
|
||||
@@ -268,6 +314,7 @@ watch(
|
||||
overflow-x: auto;
|
||||
.fullscreen-modal {
|
||||
display: none;
|
||||
background-color: rgba(0, 0, 0, 0.42);
|
||||
}
|
||||
|
||||
&.fullscreen {
|
||||
|
||||
+18
-1
@@ -7,15 +7,32 @@ export type HttpRecord = {
|
||||
httpUploadRootDir: string;
|
||||
};
|
||||
|
||||
export type DnsPersistRecord = {
|
||||
id?: number;
|
||||
domain: string;
|
||||
mainDomain?: string;
|
||||
status?: string;
|
||||
hostRecord?: string;
|
||||
recordValue?: string;
|
||||
caType?: string;
|
||||
acmeAccountAccessId?: number;
|
||||
accountUri?: string;
|
||||
wildcard?: boolean;
|
||||
persistUntil?: number;
|
||||
dnsProviderType?: string;
|
||||
dnsProviderAccess?: number;
|
||||
};
|
||||
|
||||
export type DomainVerifyPlanInput = {
|
||||
domain: string;
|
||||
domains: string[];
|
||||
type: "cname" | "dns" | "http";
|
||||
type: "cname" | "dns" | "http" | "dns-persist";
|
||||
dnsProviderType?: string;
|
||||
dnsProviderAccessType?: string;
|
||||
dnsProviderAccessId?: number;
|
||||
cnameVerifyPlan?: Record<string, CnameRecord>;
|
||||
httpVerifyPlan?: Record<string, HttpRecord>;
|
||||
dnsPersistVerifyPlan?: Record<string, DnsPersistRecord>;
|
||||
};
|
||||
export type DomainsVerifyPlanInput = {
|
||||
[key: string]: DomainVerifyPlanInput;
|
||||
|
||||
+8
@@ -46,6 +46,14 @@ function checkDomainVerifyPlan(rule: any, value: DomainsVerifyPlanInput) {
|
||||
if (!value[domain].dnsProviderType || !value[domain].dnsProviderAccessId) {
|
||||
throw new Error($t("certd.verifyPlan.errors.dnsProviderRequired", { domain }));
|
||||
}
|
||||
} else if (type === "dns-persist") {
|
||||
const subDomains = Object.keys(value[domain].dnsPersistVerifyPlan || {});
|
||||
for (const subDomain of subDomains) {
|
||||
const plan = value[domain].dnsPersistVerifyPlan[subDomain];
|
||||
if (plan.status !== "valid") {
|
||||
throw new Error(`DNS持久验证记录(${subDomain})还未校验成功`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="refresh-input">
|
||||
<div class="refresh-input-line">
|
||||
<a-input class="refresh-input-control" :value="value" :placeholder="placeholder" allow-clear @update:value="emit('update:value', $event)"></a-input>
|
||||
<fs-button :loading="loading" type="primary" :text="buttonText" :icon="icon" @click="doRefresh"></fs-button>
|
||||
<a-input class="refresh-input-control" :value="value" :placeholder="placeholder" :allow-clear="!disabled" :disabled="disabled" @update:value="emit('update:value', $event)"></a-input>
|
||||
<fs-button :loading="loading" :disabled="disabled" type="primary" :text="buttonText" :icon="icon" @click="doRefresh"></fs-button>
|
||||
</div>
|
||||
<div class="helper" :class="{ error: hasError }">
|
||||
{{ message }}
|
||||
@@ -25,6 +25,7 @@ type RefreshInputProps = ComponentPropsType & {
|
||||
icon?: string;
|
||||
placeholder?: string;
|
||||
successMessage?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const fromType: any = inject("getFromType");
|
||||
@@ -49,6 +50,9 @@ const placeholder = computed(() => props.placeholder || "");
|
||||
const successMessage = computed(() => props.successMessage || "刷新成功,请保存配置");
|
||||
|
||||
const doRefresh = async () => {
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
if (loading.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ function createChallengeTypeDict() {
|
||||
return dict({
|
||||
data: [
|
||||
{ value: "dns", label: $t("certd.verifyPlan.dnsChallenge"), color: "green" },
|
||||
{ value: "dns-persist", label: "DNS持久验证", color: "cyan" },
|
||||
{ value: "cname", label: $t("certd.verifyPlan.cnameProxyChallenge"), color: "blue" },
|
||||
{ value: "http", label: $t("certd.verifyPlan.httpChallenge"), color: "yellow" },
|
||||
],
|
||||
@@ -39,7 +40,12 @@ export const Dicts = {
|
||||
sslProviderDict: dict({
|
||||
data: [
|
||||
{ value: "letsencrypt", label: "Let's Encrypt" },
|
||||
{ value: "letsencrypt_staging", label: "Let's Encrypt测试环境" },
|
||||
{ value: "google", label: "Google" },
|
||||
{ value: "zerossl", label: "ZeroSSL" },
|
||||
{ value: "sslcom", label: "SSL.com" },
|
||||
{ value: "litessl", label: "litessl" },
|
||||
{ value: "custom", label: "自定义ACME" },
|
||||
],
|
||||
}),
|
||||
get challengeTypeDict() {
|
||||
|
||||
Reference in New Issue
Block a user