完善系统设置DNS验证保存
This commit is contained in:
parent
4a46512771
commit
f1c7fc34bd
@ -1,6 +1,7 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"reflect"
|
||||
@ -51,8 +52,9 @@ type settingsACMEView struct {
|
||||
}
|
||||
|
||||
type settingsDNSView struct {
|
||||
Provider string `json:"provider"`
|
||||
VerificationState string `json:"verification_state"`
|
||||
Provider string `json:"provider"`
|
||||
VerificationState string `json:"verification_state"`
|
||||
CredentialConfigured bool `json:"credential_configured"`
|
||||
}
|
||||
|
||||
type settingsNotificationView struct {
|
||||
@ -122,7 +124,10 @@ type settingsACMERequest struct {
|
||||
}
|
||||
|
||||
type settingsDNSRequest struct {
|
||||
Provider *string `json:"provider"`
|
||||
Provider *string `json:"provider"`
|
||||
CredentialRef *string `json:"credential_ref"`
|
||||
VerificationToken *string `json:"verification_token"`
|
||||
TestDomain *string `json:"test_domain"`
|
||||
}
|
||||
|
||||
type settingsNotificationRequest struct {
|
||||
@ -182,6 +187,19 @@ func (s *Server) updateSettings(c *gin.Context) {
|
||||
s.writeError(c, http.StatusBadRequest, "保存通知凭据失败", err)
|
||||
return
|
||||
}
|
||||
dnsCredentialRef, dnsVerificationToken, dnsTestDomain, err := s.applySettingsDNS(c.Request.Context(), &updated, previous, request.DNS)
|
||||
if err != nil {
|
||||
s.rollbackSetupCredentialsWithLog(createdCredentialRefs)
|
||||
if dnsCredentialRef != "" && dnsCredentialRef != previous.DNS.CredentialRef {
|
||||
s.rollbackSetupCredentialsWithLog([]string{dnsCredentialRef})
|
||||
}
|
||||
s.configMu.Unlock()
|
||||
s.writeError(c, http.StatusBadRequest, "保存 DNS 设置失败", err)
|
||||
return
|
||||
}
|
||||
if dnsCredentialRef != "" && dnsCredentialRef != previous.DNS.CredentialRef {
|
||||
createdCredentialRefs = append(createdCredentialRefs, dnsCredentialRef)
|
||||
}
|
||||
if err := updated.Validate(); err != nil {
|
||||
s.rollbackSetupCredentialsWithLog(createdCredentialRefs)
|
||||
s.configMu.Unlock()
|
||||
@ -208,10 +226,12 @@ func (s *Server) updateSettings(c *gin.Context) {
|
||||
s.config = updated
|
||||
s.configMu.Unlock()
|
||||
oldCredentialRefs := []string{
|
||||
previous.DNS.CredentialRef,
|
||||
previous.Notification.HTTPCredentialRef,
|
||||
previous.Notification.SMTPCredentialRef,
|
||||
}
|
||||
newCredentialRefs := map[string]struct{}{
|
||||
updated.DNS.CredentialRef: {},
|
||||
updated.Notification.HTTPCredentialRef: {},
|
||||
updated.Notification.SMTPCredentialRef: {},
|
||||
}
|
||||
@ -227,7 +247,12 @@ func (s *Server) updateSettings(c *gin.Context) {
|
||||
}
|
||||
if len(obsoleteCredentialRefs) > 0 {
|
||||
if err := s.rollbackSetupCredentials(obsoleteCredentialRefs); err != nil && s.logger != nil {
|
||||
s.logger.Warn("\u6e05\u7406\u5df2\u66ff\u6362\u7684\u901a\u77e5\u654f\u611f\u51ed\u636e\u5931\u8d25", "error", err)
|
||||
s.logger.Warn("\u6e05\u7406\u5df2\u66ff\u6362\u7684\u654f\u611f\u51ed\u636e\u5931\u8d25", "error", err)
|
||||
}
|
||||
}
|
||||
if dnsVerificationToken != "" && s.setupDNS != nil {
|
||||
if err := s.setupDNS.Consume(dnsVerificationToken, updated.DNS.Provider, dnsCredentialRef, dnsTestDomain); err != nil && s.logger != nil {
|
||||
s.logger.Warn("系统设置已保存,但清理 DNS 验证令牌失败", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -238,6 +263,39 @@ func (s *Server) updateSettings(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) applySettingsDNS(ctx context.Context, cfg *config.Config, previous config.Config, request *settingsDNSRequest) (string, string, string, error) {
|
||||
if cfg == nil || request == nil {
|
||||
return "", "", "", nil
|
||||
}
|
||||
providerChanged := cfg.DNS.Provider != strings.TrimSpace(previous.DNS.Provider)
|
||||
hasVerificationInput := request.CredentialRef != nil || request.VerificationToken != nil || request.TestDomain != nil
|
||||
if !hasVerificationInput {
|
||||
if providerChanged {
|
||||
cfg.DNS.CredentialRef = ""
|
||||
cfg.DNS.VerificationState = "pending"
|
||||
}
|
||||
return "", "", "", nil
|
||||
}
|
||||
if s.setupDNS == nil {
|
||||
return "", "", "", errors.New("DNS 在线验证服务尚未初始化")
|
||||
}
|
||||
if request.CredentialRef == nil || request.VerificationToken == nil || request.TestDomain == nil {
|
||||
return "", "", "", errors.New("DNS 验证参数不完整,请先完成在线测试")
|
||||
}
|
||||
credentialRef := strings.TrimSpace(*request.CredentialRef)
|
||||
verificationToken := strings.TrimSpace(*request.VerificationToken)
|
||||
testDomain := strings.TrimSpace(*request.TestDomain)
|
||||
if credentialRef == "" || verificationToken == "" || testDomain == "" {
|
||||
return credentialRef, verificationToken, testDomain, errors.New("DNS 验证参数不完整,请先完成在线测试")
|
||||
}
|
||||
if err := s.setupDNS.Check(verificationToken, cfg.DNS.Provider, credentialRef, testDomain); err != nil {
|
||||
return credentialRef, verificationToken, testDomain, err
|
||||
}
|
||||
cfg.DNS.CredentialRef = credentialRef
|
||||
cfg.DNS.VerificationState = "verified"
|
||||
return credentialRef, verificationToken, testDomain, nil
|
||||
}
|
||||
|
||||
func settingsViewFromConfig(cfg config.Config) settingsView {
|
||||
return settingsView{
|
||||
Server: settingsServerView{Host: cfg.Server.Host, Port: cfg.Server.Port, HTTPSMode: cfg.Server.HTTPSMode},
|
||||
@ -250,7 +308,10 @@ func settingsViewFromConfig(cfg config.Config) settingsView {
|
||||
RenewalMaxRetries: cfg.ACME.RenewalMaxRetries, RenewalRetryIntervalMins: cfg.ACME.RenewalRetryIntervalMins,
|
||||
PropagationTimeoutSecs: cfg.ACME.PropagationTimeoutSecs, PropagationPollingIntervalSecs: cfg.ACME.PropagationPollingIntervalSecs,
|
||||
},
|
||||
DNS: settingsDNSView{Provider: cfg.DNS.Provider, VerificationState: cfg.DNS.VerificationState},
|
||||
DNS: settingsDNSView{
|
||||
Provider: cfg.DNS.Provider, VerificationState: cfg.DNS.VerificationState,
|
||||
CredentialConfigured: strings.TrimSpace(cfg.DNS.CredentialRef) != "",
|
||||
},
|
||||
Notification: settingsNotificationView{
|
||||
HTTPEnabled: cfg.Notification.HTTPEnabled, SMTPEnabled: cfg.Notification.SMTPEnabled, Provider: cfg.Notification.Provider,
|
||||
SMTPHost: cfg.Notification.SMTPHost, SMTPPort: cfg.Notification.SMTPPort, SMTPUsername: cfg.Notification.SMTPUsername,
|
||||
|
||||
@ -3,8 +3,10 @@ package httpapi
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@ -12,6 +14,7 @@ import (
|
||||
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/certificate"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/config"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/setup"
|
||||
)
|
||||
|
||||
func TestSettingsAPIRequiresAuthentication(t *testing.T) {
|
||||
@ -439,3 +442,238 @@ func TestSettingsAPIRollsBackEarlierCredentialWhenLaterCredentialFails(t *testin
|
||||
t.Fatalf("earlier HTTP credentials should be rolled back: values=%v deleted=%v", secrets.values, secrets.deleted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsAPIDNSRequiresCompleteVerification(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := config.Default(dir)
|
||||
cfg.SetupReady = true
|
||||
cfg.DNS.VerificationState = "verified"
|
||||
cfg.DNS.Provider = "cloudflare"
|
||||
cfg.DNS.CredentialRef = "secret://dns/cloudflare/old"
|
||||
cfg.Web.Hostname = "scert.example.test"
|
||||
cfg.Admin.Username = "admin"
|
||||
cfg.Admin.Email = "admin@example.test"
|
||||
cfg.Admin.PasswordHash = mustTestPasswordHash(t, "Password123")
|
||||
configPath := dir + "\\config\\scert.json"
|
||||
if err := cfg.Save(configPath); err != nil {
|
||||
t.Fatalf("保存测试配置失败:%v", err)
|
||||
}
|
||||
server := New(Dependencies{
|
||||
Config: cfg, ConfigPath: configPath,
|
||||
SetupDNS: &fakeSetupDNSVerifier{},
|
||||
})
|
||||
cookie := loginForSettingsTest(t, server)
|
||||
|
||||
request := httptest.NewRequest(http.MethodPut, "/api/v1/settings", strings.NewReader(`{
|
||||
"dns": {"provider": "cloudflare", "verification_token": "token-only"}
|
||||
}`))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.AddCookie(cookie)
|
||||
response := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(response, request)
|
||||
if response.Code != http.StatusBadRequest {
|
||||
t.Fatalf("DNS 验证参数不完整应返回 400,实际为 %d:%s", response.Code, response.Body.String())
|
||||
}
|
||||
if !strings.Contains(response.Body.String(), "保存 DNS 设置失败") {
|
||||
t.Fatalf("错误响应应说明 DNS 设置保存失败:%s", response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsAPIDNSProviderChangeRequiresReverification(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := config.Default(dir)
|
||||
cfg.SetupReady = true
|
||||
cfg.DNS.VerificationState = "verified"
|
||||
cfg.DNS.Provider = "cloudflare"
|
||||
cfg.DNS.CredentialRef = "secret://dns/cloudflare/old"
|
||||
cfg.Web.Hostname = "scert.example.test"
|
||||
cfg.Admin.Username = "admin"
|
||||
cfg.Admin.Email = "admin@example.test"
|
||||
cfg.Admin.PasswordHash = mustTestPasswordHash(t, "Password123")
|
||||
configPath := dir + "\\config\\scert.json"
|
||||
if err := cfg.Save(configPath); err != nil {
|
||||
t.Fatalf("保存测试配置失败:%v", err)
|
||||
}
|
||||
server := New(Dependencies{Config: cfg, ConfigPath: configPath})
|
||||
cookie := loginForSettingsTest(t, server)
|
||||
|
||||
request := httptest.NewRequest(http.MethodPut, "/api/v1/settings", strings.NewReader(`{
|
||||
"dns": {"provider": "aliyun"}
|
||||
}`))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.AddCookie(cookie)
|
||||
response := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(response, request)
|
||||
if response.Code != http.StatusBadRequest {
|
||||
t.Fatalf("更换 DNS 服务商但未重新验证应返回 400,实际为 %d:%s", response.Code, response.Body.String())
|
||||
}
|
||||
if !strings.Contains(response.Body.String(), "设置内容校验失败") {
|
||||
t.Fatalf("错误响应应说明设置校验失败:%s", response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsAPISavesVerifiedDNSCredentialWithoutReturningSecret(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := config.Default(dir)
|
||||
cfg.SetupReady = true
|
||||
cfg.DNS.VerificationState = "verified"
|
||||
cfg.DNS.Provider = "cloudflare"
|
||||
cfg.DNS.CredentialRef = "secret://dns/cloudflare/old"
|
||||
cfg.Web.Hostname = "scert.example.test"
|
||||
cfg.Admin.Username = "admin"
|
||||
cfg.Admin.Email = "admin@example.test"
|
||||
cfg.Admin.PasswordHash = mustTestPasswordHash(t, "Password123")
|
||||
configPath := dir + "\\config\\scert.json"
|
||||
if err := cfg.Save(configPath); err != nil {
|
||||
t.Fatalf("保存测试配置失败:%v", err)
|
||||
}
|
||||
const newCredentialRef = "secret://dns/cloudflare/new"
|
||||
verifier := &fakeSetupDNSVerifier{verifyResult: setup.DNSVerificationResult{
|
||||
CredentialRef: newCredentialRef, VerificationToken: "new-verification-token",
|
||||
}}
|
||||
secrets := &fakeSetupSecretWriter{values: map[string]map[string]string{
|
||||
cfg.DNS.CredentialRef: {"api_token": "old-token"},
|
||||
newCredentialRef: {"api_token": "new-token"},
|
||||
}}
|
||||
server := New(Dependencies{Config: cfg, ConfigPath: configPath, SetupDNS: verifier, Secrets: secrets})
|
||||
cookie := loginForSettingsTest(t, server)
|
||||
|
||||
request := httptest.NewRequest(http.MethodPut, "/api/v1/settings", strings.NewReader(`{
|
||||
"dns": {
|
||||
"provider": "cloudflare",
|
||||
"credential_ref": "secret://dns/cloudflare/new",
|
||||
"verification_token": "new-verification-token",
|
||||
"test_domain": "example.com"
|
||||
}
|
||||
}`))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.AddCookie(cookie)
|
||||
response := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(response, request)
|
||||
if response.Code != http.StatusOK {
|
||||
t.Fatalf("保存已验证 DNS 设置应成功,实际为 %d:%s", response.Code, response.Body.String())
|
||||
}
|
||||
body := response.Body.String()
|
||||
for _, secret := range []string{"new-verification-token", newCredentialRef, "new-token", "credential_ref"} {
|
||||
if strings.Contains(body, secret) {
|
||||
t.Fatalf("设置响应不应返回 DNS 敏感内容 %q:%s", secret, body)
|
||||
}
|
||||
}
|
||||
if !verifier.consumed {
|
||||
t.Fatal("保存成功后应消费 DNS 验证令牌")
|
||||
}
|
||||
if _, ok := secrets.values[cfg.DNS.CredentialRef]; ok {
|
||||
t.Fatal("替换后的旧 DNS 凭据应被清理")
|
||||
}
|
||||
if _, ok := secrets.values[newCredentialRef]; !ok {
|
||||
t.Fatal("新的 DNS 凭据应继续保留")
|
||||
}
|
||||
saved, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("读取保存后的配置失败:%v", err)
|
||||
}
|
||||
if saved.DNS.CredentialRef != newCredentialRef || saved.DNS.VerificationState != "verified" {
|
||||
t.Fatalf("DNS 配置没有正确持久化:%+v", saved.DNS)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsAPIDNSCheckFailureRollsBackCredential(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := config.Default(dir)
|
||||
cfg.SetupReady = true
|
||||
cfg.DNS.VerificationState = "verified"
|
||||
cfg.DNS.Provider = "cloudflare"
|
||||
cfg.DNS.CredentialRef = "secret://dns/cloudflare/old"
|
||||
cfg.Web.Hostname = "scert.example.test"
|
||||
cfg.Admin.Username = "admin"
|
||||
cfg.Admin.Email = "admin@example.test"
|
||||
cfg.Admin.PasswordHash = mustTestPasswordHash(t, "Password123")
|
||||
configPath := dir + "\\config\\scert.json"
|
||||
if err := cfg.Save(configPath); err != nil {
|
||||
t.Fatalf("保存测试配置失败:%v", err)
|
||||
}
|
||||
const newCredentialRef = "secret://dns/cloudflare/new"
|
||||
verifier := &fakeSetupDNSVerifier{checkErr: errors.New("DNS 验证令牌无效")}
|
||||
secrets := &fakeSetupSecretWriter{values: map[string]map[string]string{
|
||||
newCredentialRef: {"api_token": "new-token"},
|
||||
}}
|
||||
server := New(Dependencies{Config: cfg, ConfigPath: configPath, SetupDNS: verifier, Secrets: secrets})
|
||||
cookie := loginForSettingsTest(t, server)
|
||||
|
||||
request := httptest.NewRequest(http.MethodPut, "/api/v1/settings", strings.NewReader(`{
|
||||
"dns": {
|
||||
"provider": "cloudflare",
|
||||
"credential_ref": "secret://dns/cloudflare/new",
|
||||
"verification_token": "invalid-token",
|
||||
"test_domain": "example.com"
|
||||
}
|
||||
}`))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.AddCookie(cookie)
|
||||
response := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(response, request)
|
||||
if response.Code != http.StatusBadRequest {
|
||||
t.Fatalf("无效 DNS 验证令牌应返回 400,实际为 %d:%s", response.Code, response.Body.String())
|
||||
}
|
||||
if _, ok := secrets.values[newCredentialRef]; ok {
|
||||
t.Fatal("DNS 验证失败后不应保留新凭据")
|
||||
}
|
||||
if len(secrets.deleted) != 1 || secrets.deleted[0] != newCredentialRef {
|
||||
t.Fatalf("DNS 验证失败后应删除新凭据:%v", secrets.deleted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsAPIDNSSaveFailureRollsBackCredential(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := config.Default(dir)
|
||||
cfg.SetupReady = true
|
||||
cfg.DNS.VerificationState = "verified"
|
||||
cfg.DNS.Provider = "cloudflare"
|
||||
cfg.DNS.CredentialRef = "secret://dns/cloudflare/old"
|
||||
cfg.Web.Hostname = "scert.example.test"
|
||||
cfg.Admin.Username = "admin"
|
||||
cfg.Admin.Email = "admin@example.test"
|
||||
cfg.Admin.PasswordHash = mustTestPasswordHash(t, "Password123")
|
||||
initialConfigPath := dir + "\\config\\initial.json"
|
||||
if err := cfg.Save(initialConfigPath); err != nil {
|
||||
t.Fatalf("保存测试配置失败:%v", err)
|
||||
}
|
||||
blockedPath := dir + "\\blocked"
|
||||
if err := os.WriteFile(blockedPath, []byte("not a directory"), 0o600); err != nil {
|
||||
t.Fatalf("创建保存失败测试文件失败:%v", err)
|
||||
}
|
||||
const newCredentialRef = "secret://dns/cloudflare/new"
|
||||
verifier := &fakeSetupDNSVerifier{verifyResult: setup.DNSVerificationResult{
|
||||
CredentialRef: newCredentialRef, VerificationToken: "new-verification-token",
|
||||
}}
|
||||
secrets := &fakeSetupSecretWriter{values: map[string]map[string]string{
|
||||
newCredentialRef: {"api_token": "new-token"},
|
||||
}}
|
||||
server := New(Dependencies{Config: cfg, ConfigPath: blockedPath + "\\scert.json", SetupDNS: verifier, Secrets: secrets})
|
||||
cookie := loginForSettingsTest(t, server)
|
||||
|
||||
request := httptest.NewRequest(http.MethodPut, "/api/v1/settings", strings.NewReader(`{
|
||||
"dns": {
|
||||
"provider": "cloudflare",
|
||||
"credential_ref": "secret://dns/cloudflare/new",
|
||||
"verification_token": "new-verification-token",
|
||||
"test_domain": "example.com"
|
||||
}
|
||||
}`))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.AddCookie(cookie)
|
||||
response := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(response, request)
|
||||
if response.Code != http.StatusInternalServerError {
|
||||
t.Fatalf("配置保存失败应返回 500,实际为 %d:%s", response.Code, response.Body.String())
|
||||
}
|
||||
if _, ok := secrets.values[newCredentialRef]; ok {
|
||||
t.Fatal("配置保存失败后不应保留新 DNS 凭据")
|
||||
}
|
||||
if len(secrets.deleted) != 1 || secrets.deleted[0] != newCredentialRef {
|
||||
t.Fatalf("配置保存失败后应删除新 DNS 凭据:%v", secrets.deleted)
|
||||
}
|
||||
if verifier.consumed {
|
||||
t.Fatal("配置保存失败后不应消费 DNS 验证令牌")
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ export function useDashboard() {
|
||||
const tasks = ref([]);
|
||||
const auditLogs = ref([]);
|
||||
const settings = ref(null);
|
||||
const dnsProviderCatalog = ref([]);
|
||||
const domainServers = ref({});
|
||||
const loading = ref(true);
|
||||
const saving = ref(false);
|
||||
@ -34,6 +35,8 @@ export function useDashboard() {
|
||||
const showSettings = ref(false);
|
||||
const settingsSaving = ref(false);
|
||||
const settingsError = ref("");
|
||||
const dnsTesting = ref(false);
|
||||
const dnsTestError = ref("");
|
||||
const settingsForm = ref(createEmptySettingsForm());
|
||||
const showCertificateImport = ref(false);
|
||||
const importDomain = ref(null);
|
||||
@ -184,6 +187,27 @@ export function useDashboard() {
|
||||
const formTitle = computed(() =>
|
||||
isEditing.value ? "编辑域名配置" : "添加托管域名",
|
||||
);
|
||||
const selectedDNSProvider = computed(() =>
|
||||
dnsProviderCatalog.value.find(
|
||||
(provider) => provider.id === settingsForm.value.dns.provider,
|
||||
) || null,
|
||||
);
|
||||
const dnsVerificationRequired = computed(() => {
|
||||
const current = settingsForm.value.dns;
|
||||
const saved = settings.value?.dns;
|
||||
if (!current.provider) return false;
|
||||
return (
|
||||
current.provider !== saved?.provider ||
|
||||
!saved?.credential_configured ||
|
||||
current.verification_state === "pending"
|
||||
);
|
||||
});
|
||||
const dnsReadyToSave = computed(
|
||||
() =>
|
||||
!dnsVerificationRequired.value ||
|
||||
(Boolean(settingsForm.value.dns.credential_ref) &&
|
||||
Boolean(settingsForm.value.dns.verification_token)),
|
||||
);
|
||||
|
||||
const defaultCertificatePath = "/ssl/{域名}/{证书类型}/cert.pem";
|
||||
const defaultCertificateChainPath = "/ssl/{域名}/{证书类型}/fullchain.pem";
|
||||
@ -304,7 +328,16 @@ export function useDashboard() {
|
||||
propagation_timeout_secs: 900,
|
||||
propagation_polling_interval_secs: 10,
|
||||
},
|
||||
dns: { provider: "" },
|
||||
dns: {
|
||||
provider: "",
|
||||
credential_configured: false,
|
||||
verification_state: "not_configured",
|
||||
test_domain: "",
|
||||
credentials: {},
|
||||
credential_ref: "",
|
||||
verification_token: "",
|
||||
test_report: null,
|
||||
},
|
||||
notification: {
|
||||
http_enabled: false,
|
||||
smtp_enabled: false,
|
||||
@ -344,7 +377,15 @@ export function useDashboard() {
|
||||
web: { ...defaults.web, ...(source.web || {}) },
|
||||
paths: { ...defaults.paths, ...(source.paths || {}) },
|
||||
acme: { ...defaults.acme, ...(source.acme || {}) },
|
||||
dns: { ...defaults.dns, ...(source.dns || {}) },
|
||||
dns: {
|
||||
...defaults.dns,
|
||||
...(source.dns || {}),
|
||||
credentials: {},
|
||||
credential_ref: "",
|
||||
verification_token: "",
|
||||
test_domain: "",
|
||||
test_report: null,
|
||||
},
|
||||
notification: {
|
||||
...defaults.notification,
|
||||
...(source.notification || {}),
|
||||
@ -682,8 +723,84 @@ export function useDashboard() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleDNSProviderChange() {
|
||||
const dns = settingsForm.value.dns;
|
||||
const saved = settings.value?.dns;
|
||||
dnsTestError.value = "";
|
||||
dns.credential_ref = "";
|
||||
dns.verification_token = "";
|
||||
dns.test_report = null;
|
||||
if (dns.provider && dns.provider === saved?.provider && saved.credential_configured) {
|
||||
dns.verification_state = saved.verification_state || "verified";
|
||||
return;
|
||||
}
|
||||
dns.verification_state = dns.provider ? "pending" : "not_configured";
|
||||
}
|
||||
|
||||
function clearDNSCredentials() {
|
||||
settingsForm.value.dns.credentials = {};
|
||||
dnsTestError.value = "";
|
||||
}
|
||||
|
||||
async function testDNS() {
|
||||
const dns = settingsForm.value.dns;
|
||||
const provider = selectedDNSProvider.value;
|
||||
dnsTestError.value = "";
|
||||
dns.test_report = null;
|
||||
if (!dns.provider || !provider) {
|
||||
dnsTestError.value = "请选择 DNS 验证服务商";
|
||||
return;
|
||||
}
|
||||
if (!dns.test_domain.trim()) {
|
||||
dnsTestError.value = "请输入 DNS 在线测试域名";
|
||||
return;
|
||||
}
|
||||
const credentials = Object.fromEntries(
|
||||
Object.entries(dns.credentials || {})
|
||||
.map(([key, value]) => [key, String(value || "").trim()])
|
||||
.filter(([, value]) => value),
|
||||
);
|
||||
const missingField = (provider.credential_fields || []).find(
|
||||
(field) => field.required && !credentials[field.name],
|
||||
);
|
||||
if (missingField) {
|
||||
dnsTestError.value = `请填写${missingField.label}`;
|
||||
return;
|
||||
}
|
||||
dnsTesting.value = true;
|
||||
try {
|
||||
const response = await fetchAdmin("/api/v1/dns/test", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
provider: dns.provider,
|
||||
test_domain: dns.test_domain.trim(),
|
||||
credentials,
|
||||
}),
|
||||
});
|
||||
const result = await response.json().catch(() => ({}));
|
||||
dns.test_report = result.test_report || null;
|
||||
if (!response.ok) {
|
||||
throw new Error(result.error || "DNS API 在线测试失败");
|
||||
}
|
||||
dns.credential_ref = result.credential_ref || "";
|
||||
dns.verification_token = result.verification_token || "";
|
||||
dns.verification_state = "verified";
|
||||
dns.credential_configured = true;
|
||||
notice.value = "DNS API 在线测试通过,可以保存系统设置";
|
||||
} catch (err) {
|
||||
dns.credential_ref = "";
|
||||
dns.verification_token = "";
|
||||
dns.verification_state = "pending";
|
||||
dnsTestError.value = err.message || "DNS API 在线测试失败";
|
||||
} finally {
|
||||
dnsTesting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openSettings() {
|
||||
settingsError.value = "";
|
||||
dnsTestError.value = "";
|
||||
settingsForm.value = normalizeSettings(settings.value);
|
||||
showSettings.value = true;
|
||||
}
|
||||
@ -694,12 +811,28 @@ export function useDashboard() {
|
||||
|
||||
async function saveSettings() {
|
||||
settingsError.value = "";
|
||||
if (!dnsReadyToSave.value) {
|
||||
settingsError.value = "DNS 服务商配置尚未通过在线测试,请先完成测试";
|
||||
return;
|
||||
}
|
||||
settingsSaving.value = true;
|
||||
try {
|
||||
const current = settingsForm.value;
|
||||
const payload = {
|
||||
...current,
|
||||
dns: {
|
||||
provider: current.dns.provider,
|
||||
},
|
||||
};
|
||||
if (current.dns.credential_ref && current.dns.verification_token) {
|
||||
payload.dns.credential_ref = current.dns.credential_ref;
|
||||
payload.dns.verification_token = current.dns.verification_token;
|
||||
payload.dns.test_domain = current.dns.test_domain;
|
||||
}
|
||||
const response = await fetchAdmin("/api/v1/settings", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(settingsForm.value),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const result = await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new Error(result.error || "保存系统设置失败");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user