diff --git a/backend/internal/httpapi/dns_account_mutations.go b/backend/internal/httpapi/dns_account_mutations.go index 53261c3..c6464a1 100644 --- a/backend/internal/httpapi/dns_account_mutations.go +++ b/backend/internal/httpapi/dns_account_mutations.go @@ -122,8 +122,9 @@ func (s *Server) renameManagedDNSAccount(c *gin.Context) { return } var request struct { - Name string `json:"name"` - Revision int64 `json:"revision"` + Name string `json:"name"` + Revision int64 `json:"revision"` + Credentials map[string]string `json:"credentials"` } if err := bindJSON(c, &request, setupRequestLimit); err != nil || request.Revision < 1 { s.writeError(c, http.StatusBadRequest, "DNS 账户名称或版本无效", nil) @@ -135,12 +136,20 @@ func (s *Server) renameManagedDNSAccount(c *gin.Context) { s.writeDNSAccountError(c, err) return } - item.Name, item.CredentialRef = strings.TrimSpace(request.Name), "secret://dns/validation" + item.Name = strings.TrimSpace(request.Name) + item.CredentialRef = "secret://dns/validation" if err := item.Validate(); err != nil { s.writeError(c, http.StatusBadRequest, "DNS 账户名称无效", nil) return } - updated, err := repo.RenameDNSAccount(c.Request.Context(), c.GetString(authenticatedActorKey), c.Param("id"), request.Revision, item.Name) + reference := "" + if len(request.Credentials) > 0 { + reference = "secret://dns/" + domain.NewID() + if !s.storeManagedDNSCredential(c, item.Provider, reference, request.Credentials) { + return + } + } + updated, err := repo.UpdateDNSAccount(c.Request.Context(), c.GetString(authenticatedActorKey), c.Param("id"), request.Revision, item.Name, reference) if err != nil { s.writeDNSAccountError(c, err) return diff --git a/backend/internal/httpapi/dns_account_mutations_test.go b/backend/internal/httpapi/dns_account_mutations_test.go index 5913ca4..9c4067d 100644 --- a/backend/internal/httpapi/dns_account_mutations_test.go +++ b/backend/internal/httpapi/dns_account_mutations_test.go @@ -74,13 +74,13 @@ func TestManagedDNSAccountLifecycle(t *testing.T) { oldReference := stored.CredentialRef managedDNSRequest(t, f, "manager", http.MethodPut, path, `{"revision":2,"name":"更名后的账户"}`, http.StatusOK) managedDNSRequest(t, f, "manager", http.MethodPut, path, `{"revision":2,"name":"旧版本"}`, http.StatusConflict) - managedDNSRequest(t, f, "manager", http.MethodPut, path+"/status", `{"revision":3,"enabled":false}`, http.StatusOK) - managedDNSRequest(t, f, "manager", http.MethodPost, path+"/verify", `{"revision":4,"test_domain":"example.test"}`, http.StatusForbidden) + managedDNSRequest(t, f, "manager", http.MethodPut, path+"/status", `{"revision":4,"enabled":false}`, http.StatusOK) + managedDNSRequest(t, f, "manager", http.MethodPost, path+"/verify", `{"revision":5,"test_domain":"example.test"}`, http.StatusForbidden) if verifications != 1 { t.Fatal("已禁用账户仍调用服务商") } - managedDNSRequest(t, f, "manager", http.MethodPut, path+"/credentials", `{"revision":4,"credentials":{"api_token":"rotated-sensitive-token"}}`, http.StatusOK) - managedDNSRequest(t, f, "manager", http.MethodPut, path+"/status", `{"revision":5,"enabled":true}`, http.StatusOK) + managedDNSRequest(t, f, "manager", http.MethodPut, path+"/credentials", `{"revision":5,"credentials":{"api_token":"rotated-sensitive-token"}}`, http.StatusOK) + managedDNSRequest(t, f, "manager", http.MethodPut, path+"/status", `{"revision":6,"enabled":true}`, http.StatusOK) stored, err = f.members.GetDNSAccountForVerification(t.Context(), "manager", item.ID) if err != nil || stored.CredentialRef == oldReference || stored.VerificationState != domain.DNSAccountUnverified || len(secrets.data) != 2 { t.Fatal("轮换没有生成新引用、保留旧引用或清除验证状态", err) @@ -91,9 +91,9 @@ func TestManagedDNSAccountLifecycle(t *testing.T) { } return dnsverification.Report{}, errors.New("internal-sensitive-error") }) - managedDNSRequest(t, f, "manager", http.MethodPost, path+"/verify", `{"revision":6,"test_domain":"example.test"}`, http.StatusUnprocessableEntity) + managedDNSRequest(t, f, "manager", http.MethodPost, path+"/verify", `{"revision":7,"test_domain":"example.test"}`, http.StatusUnprocessableEntity) stored, err = f.members.GetVisibleDNSAccount(t.Context(), "manager", item.ID) - if err != nil || stored.VerificationState != domain.DNSAccountVerificationFailed || stored.Revision != 7 { + if err != nil || stored.VerificationState != domain.DNSAccountVerificationFailed || stored.Revision != 8 { t.Fatal("服务商失败没有记录到对应版本", err) } var count int diff --git a/backend/internal/repository/dns_account_repository.go b/backend/internal/repository/dns_account_repository.go index 41ca3dd..97c56c9 100644 --- a/backend/internal/repository/dns_account_repository.go +++ b/backend/internal/repository/dns_account_repository.go @@ -23,6 +23,7 @@ type DNSAccountRepository interface { GetVisibleDNSAccount(context.Context, string, string) (domain.DNSAccount, error) GetDNSAccountForUse(context.Context, string, string, string) (domain.DNSAccount, error) GetDNSAccountForVerification(context.Context, string, string) (domain.DNSAccount, error) + UpdateDNSAccount(context.Context, string, string, int64, string, string) (domain.DNSAccount, error) RenameDNSAccount(context.Context, string, string, int64, string) (domain.DNSAccount, error) SetDNSAccountEnabled(context.Context, string, string, int64, bool) (domain.DNSAccount, error) RotateDNSAccountCredential(context.Context, string, string, int64, string) (domain.DNSAccount, error) @@ -204,6 +205,17 @@ func (r *SQLProjectMemberRepository) RenameDNSAccount(ctx context.Context, actor }) } +func (r *SQLProjectMemberRepository) UpdateDNSAccount(ctx context.Context, actorID, id string, revision int64, name, reference string) (domain.DNSAccount, error) { + return r.changeDNSAccount(ctx, actorID, id, revision, "dns_account.update", func(item *domain.DNSAccount) error { + item.Name = name + if reference != "" { + item.CredentialRef = reference + item.VerificationState = domain.DNSAccountUnverified + } + return nil + }) +} + // 首次验证允许读取尚未验证的账户,但必须实时拥有管理权限,且账户未被禁用。 // 结果落库仍须核对读取时版本,不能将此快照作为长时间授权或执行租约。 func (r *SQLProjectMemberRepository) GetDNSAccountForVerification(ctx context.Context, actorID, id string) (domain.DNSAccount, error) { diff --git a/frontend/src/views/DNSAccounts/DNSAccountDialog.css b/frontend/src/views/DNSAccounts/DNSAccountDialog.css index 1da07a1..e37531c 100644 --- a/frontend/src/views/DNSAccounts/DNSAccountDialog.css +++ b/frontend/src/views/DNSAccounts/DNSAccountDialog.css @@ -78,6 +78,14 @@ button[type="submit"] { overflow-wrap: anywhere; } +.dns-dialog-hint { + grid-column: 1 / -1; + margin: -4px 0 0; + color: #64748b; + font-size: 13px; + line-height: 1.6; +} + .dns-dialog-error { color: #a92e2e; } diff --git a/frontend/src/views/DNSAccounts/DNSAccountDialog.vue b/frontend/src/views/DNSAccounts/DNSAccountDialog.vue index 659952b..1479b56 100644 --- a/frontend/src/views/DNSAccounts/DNSAccountDialog.vue +++ b/frontend/src/views/DNSAccounts/DNSAccountDialog.vue @@ -28,7 +28,7 @@ onBeforeUnmount(() => previousFocus?.focus?.());
- -