feat: add DNS credential management with Web UI
- New config/credential_store.go: persistent credential storage (credentials.json) - New API: CRUD for DNS credentials + credential-types endpoint - Certificate model now supports credential_id reference - Create certificate auto-resolves credential_id to DNS config - New Vue page: Credentials.vue for managing saved DNS keys - CertCreate.vue: select existing credential or manual input + save as new - Secrets masked in API responses, never exposed in list
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="140px"
|
||||
style="max-width: 600px"
|
||||
style="max-width: 620px"
|
||||
>
|
||||
<el-form-item label="域名" prop="domain">
|
||||
<el-input v-model="form.domain" placeholder="example.com" />
|
||||
@@ -32,39 +32,77 @@
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="DNS 提供商" v-if="form.challenge_type === 'dns'" prop="dns_provider">
|
||||
<el-select v-model="form.dns_provider" placeholder="选择 DNS 提供商" style="width: 100%">
|
||||
<el-option label="阿里云 DNS (Aliyun)" value="alidns" />
|
||||
<el-option label="Cloudflare" value="cloudflare" />
|
||||
<el-option label="DNSPod" value="dnspod" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- DNS mode: credential or manual -->
|
||||
<template v-if="form.challenge_type === 'dns'">
|
||||
<el-form-item label="使用方式">
|
||||
<el-radio-group v-model="credMode">
|
||||
<el-radio value="credential">选择已有凭证</el-radio>
|
||||
<el-radio value="manual">手动输入</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- Aliyun DNS Config -->
|
||||
<template v-if="form.challenge_type === 'dns' && form.dns_provider === 'alidns'">
|
||||
<el-form-item label="AccessKey ID" prop="ali_key">
|
||||
<el-input v-model="dnsConfig.ali_key" placeholder="Aliyun AccessKey ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="AccessKey Secret" prop="ali_secret">
|
||||
<el-input v-model="dnsConfig.ali_secret" type="password" show-password placeholder="Aliyun AccessKey Secret" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
<!-- Select existing credential -->
|
||||
<template v-if="credMode === 'credential'">
|
||||
<el-form-item label="选择凭证">
|
||||
<el-select v-model="selectedCredId" placeholder="选择已保存的 DNS 凭证" style="width: 100%" @change="onCredSelected">
|
||||
<el-option
|
||||
v-for="cred in credentials"
|
||||
:key="cred.id"
|
||||
:label="cred.name + ' (' + typeLabel(cred.type) + ')'"
|
||||
:value="cred.id"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="form-tip" v-if="credentials.length === 0">
|
||||
暂无凭证,请先在 <router-link to="/credentials">DNS 凭证</router-link> 中添加
|
||||
</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- Cloudflare Config -->
|
||||
<template v-if="form.challenge_type === 'dns' && form.dns_provider === 'cloudflare'">
|
||||
<el-form-item label="API Token">
|
||||
<el-input v-model="dnsConfig.cf_api_token" type="password" show-password placeholder="Cloudflare API Token" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
<!-- Manual input -->
|
||||
<template v-if="credMode === 'manual'">
|
||||
<el-form-item label="DNS 提供商" prop="dns_provider">
|
||||
<el-select v-model="form.dns_provider" placeholder="选择 DNS 提供商" style="width: 100%" @change="onProviderChange">
|
||||
<el-option label="阿里云 DNS (Aliyun)" value="alidns" />
|
||||
<el-option label="Cloudflare" value="cloudflare" />
|
||||
<el-option label="DNSPod" value="dnspod" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- DNSPod Config -->
|
||||
<template v-if="form.challenge_type === 'dns' && form.dns_provider === 'dnspod'">
|
||||
<el-form-item label="ID">
|
||||
<el-input v-model="dnsConfig.dnspod_id" placeholder="DNSPod ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Key">
|
||||
<el-input v-model="dnsConfig.dnspod_key" type="password" show-password placeholder="DNSPod Key" />
|
||||
</el-form-item>
|
||||
<!-- Aliyun DNS Config -->
|
||||
<template v-if="form.dns_provider === 'alidns'">
|
||||
<el-form-item label="AccessKey ID">
|
||||
<el-input v-model="manualConfig.ali_key" placeholder="Aliyun AccessKey ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="AccessKey Secret">
|
||||
<el-input v-model="manualConfig.ali_secret" type="password" show-password placeholder="Aliyun AccessKey Secret" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- Cloudflare Config -->
|
||||
<template v-if="form.dns_provider === 'cloudflare'">
|
||||
<el-form-item label="API Token">
|
||||
<el-input v-model="manualConfig.cf_api_token" type="password" show-password placeholder="Cloudflare API Token" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- DNSPod Config -->
|
||||
<template v-if="form.dns_provider === 'dnspod'">
|
||||
<el-form-item label="ID">
|
||||
<el-input v-model="manualConfig.dnspod_id" placeholder="DNSPod ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Key">
|
||||
<el-input v-model="manualConfig.dnspod_key" type="password" show-password placeholder="DNSPod Key" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- Save as credential option -->
|
||||
<el-form-item v-if="form.dns_provider">
|
||||
<el-checkbox v-model="saveAsCredential">保存为新凭证</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="saveAsCredential && form.dns_provider" label="凭证名称">
|
||||
<el-input v-model="newCredName" placeholder="例如:阿里云生产环境" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<el-form-item label="自动续期">
|
||||
@@ -87,14 +125,28 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { certApi } from '../api'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { certApi, credApi } from '../api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
interface Credential {
|
||||
id: number
|
||||
name: string
|
||||
type: string
|
||||
masked: string
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const formRef = ref()
|
||||
const submitting = ref(false)
|
||||
const credentials = ref<Credential[]>([])
|
||||
|
||||
// Credential mode: select existing or manual input
|
||||
const credMode = ref<'credential' | 'manual'>('credential')
|
||||
const selectedCredId = ref<number | null>(null)
|
||||
const saveAsCredential = ref(false)
|
||||
const newCredName = ref('')
|
||||
|
||||
const form = reactive({
|
||||
domain: '',
|
||||
@@ -106,7 +158,7 @@ const form = reactive({
|
||||
renew_days: 30,
|
||||
})
|
||||
|
||||
const dnsConfig = reactive({
|
||||
const manualConfig = reactive({
|
||||
ali_key: '',
|
||||
ali_secret: '',
|
||||
cf_api_token: '',
|
||||
@@ -122,15 +174,92 @@ const rules = {
|
||||
],
|
||||
}
|
||||
|
||||
const typeLabel = (type: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
alidns: '阿里云 DNS',
|
||||
cloudflare: 'Cloudflare',
|
||||
dnspod: 'DNSPod',
|
||||
}
|
||||
return labels[type] || type
|
||||
}
|
||||
|
||||
const loadCredentials = async () => {
|
||||
try {
|
||||
const res = await credApi.list()
|
||||
credentials.value = res.data
|
||||
} catch {
|
||||
// Ignore errors loading credentials
|
||||
}
|
||||
}
|
||||
|
||||
const onChallengeChange = () => {
|
||||
// Reset DNS config when switching
|
||||
Object.keys(dnsConfig).forEach(k => (dnsConfig as any)[k] = '')
|
||||
// Reset DNS config when switching challenge type
|
||||
Object.keys(manualConfig).forEach(k => (manualConfig as any)[k] = '')
|
||||
selectedCredId.value = null
|
||||
credMode.value = 'credential'
|
||||
saveAsCredential.value = false
|
||||
newCredName.value = ''
|
||||
}
|
||||
|
||||
const onProviderChange = () => {
|
||||
Object.keys(manualConfig).forEach(k => (manualConfig as any)[k] = '')
|
||||
}
|
||||
|
||||
const onCredSelected = (credId: number) => {
|
||||
// Just store the selection, backend resolves DNS config
|
||||
}
|
||||
|
||||
const buildDnsConfigJson = (): string => {
|
||||
switch (form.dns_provider) {
|
||||
case 'alidns':
|
||||
return JSON.stringify({
|
||||
ali_key: manualConfig.ali_key,
|
||||
ali_secret: manualConfig.ali_secret,
|
||||
})
|
||||
case 'cloudflare':
|
||||
return JSON.stringify({
|
||||
cf_api_token: manualConfig.cf_api_token,
|
||||
})
|
||||
case 'dnspod':
|
||||
return JSON.stringify({
|
||||
dnspod_id: manualConfig.dnspod_id,
|
||||
dnspod_key: manualConfig.dnspod_key,
|
||||
})
|
||||
default:
|
||||
return '{}'
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const valid = await formRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
// Validate manual fields
|
||||
if (form.challenge_type === 'dns' && credMode.value === 'manual') {
|
||||
if (!form.dns_provider) {
|
||||
ElMessage.warning('请选择 DNS 提供商')
|
||||
return
|
||||
}
|
||||
// Check required fields
|
||||
if (form.dns_provider === 'alidns' && (!manualConfig.ali_key || !manualConfig.ali_secret)) {
|
||||
ElMessage.warning('请填写阿里云 AccessKey')
|
||||
return
|
||||
}
|
||||
if (form.dns_provider === 'cloudflare' && !manualConfig.cf_api_token) {
|
||||
ElMessage.warning('请填写 Cloudflare API Token')
|
||||
return
|
||||
}
|
||||
if (form.dns_provider === 'dnspod' && (!manualConfig.dnspod_id || !manualConfig.dnspod_key)) {
|
||||
ElMessage.warning('请填写 DNSPod 凭证')
|
||||
return
|
||||
}
|
||||
|
||||
if (saveAsCredential.value && !newCredName.value) {
|
||||
ElMessage.warning('请输入凭证名称')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const payload: any = {
|
||||
@@ -143,12 +272,31 @@ const handleSubmit = async () => {
|
||||
}
|
||||
|
||||
if (form.challenge_type === 'dns') {
|
||||
payload.dns_provider = form.dns_provider
|
||||
payload.dns_config = JSON.stringify(dnsConfig)
|
||||
if (credMode.value === 'credential' && selectedCredId.value) {
|
||||
// Use existing credential
|
||||
payload.credential_id = selectedCredId.value
|
||||
} else if (credMode.value === 'manual') {
|
||||
// Manual input
|
||||
payload.dns_provider = form.dns_provider
|
||||
payload.dns_config = buildDnsConfigJson()
|
||||
|
||||
// Optionally save as new credential
|
||||
if (saveAsCredential.value && newCredName.value) {
|
||||
await credApi.create({
|
||||
name: newCredName.value,
|
||||
type: form.dns_provider,
|
||||
data: payload.dns_config,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
ElMessage.warning('请选择 DNS 凭证或选择手动输入')
|
||||
submitting.value = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
await certApi.create(payload)
|
||||
ElMessage.success(`证书申请已提交,后台处理中`)
|
||||
ElMessage.success('证书申请已提交,后台处理中')
|
||||
router.push('/certificates')
|
||||
} catch (e: any) {
|
||||
ElMessage.error('提交失败: ' + (e.response?.data?.error || e.message))
|
||||
@@ -156,6 +304,8 @@ const handleSubmit = async () => {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadCredentials)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -164,4 +314,19 @@ const handleSubmit = async () => {
|
||||
font-size: 24px;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.form-tip a {
|
||||
color: #1890ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.form-tip a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user