feat: 支持修改DNS账户归属项目及防冲突校验
This commit is contained in:
parent
673727962d
commit
3e260cf6d1
@ -33,6 +33,8 @@ func (s *Server) writeDNSAccountError(c *gin.Context, err error) {
|
||||
s.writeError(c, http.StatusForbidden, "无权操作此 DNS 账户", nil)
|
||||
case errors.Is(err, repository.ErrDNSAccountConflict):
|
||||
s.writeError(c, http.StatusConflict, "DNS 账户已变更,请刷新后重试", nil)
|
||||
case errors.Is(err, repository.ErrDNSAccountBound):
|
||||
s.writeError(c, http.StatusConflict, err.Error(), nil)
|
||||
case errors.Is(err, repository.ErrDNSAccountBusy):
|
||||
s.writeError(c, http.StatusConflict, "DNS 账户仍有执行租约,请等待任务及挑战清理完成", nil)
|
||||
default:
|
||||
@ -123,6 +125,7 @@ func (s *Server) renameManagedDNSAccount(c *gin.Context) {
|
||||
}
|
||||
var request struct {
|
||||
Name string `json:"name"`
|
||||
ProjectID string `json:"project_id"`
|
||||
Revision int64 `json:"revision"`
|
||||
Credentials map[string]string `json:"credentials"`
|
||||
}
|
||||
@ -139,14 +142,23 @@ func (s *Server) renameManagedDNSAccount(c *gin.Context) {
|
||||
if !s.authorizeProjectPermissions(c, item.ProjectID, domain.PermissionDNSManage) {
|
||||
return
|
||||
}
|
||||
targetProjectID := strings.TrimSpace(request.ProjectID)
|
||||
if targetProjectID == "" {
|
||||
targetProjectID = item.ProjectID
|
||||
} else if targetProjectID != item.ProjectID {
|
||||
if !s.authorizeProjectPermissions(c, targetProjectID, domain.PermissionDNSManage) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if item.Revision != request.Revision {
|
||||
s.writeDNSAccountError(c, repository.ErrDNSAccountConflict)
|
||||
return
|
||||
}
|
||||
item.Name = strings.TrimSpace(request.Name)
|
||||
item.ProjectID = targetProjectID
|
||||
item.CredentialRef = "secret://dns/validation"
|
||||
if err := item.Validate(); err != nil {
|
||||
s.writeError(c, http.StatusBadRequest, "DNS 账户名称无效", nil)
|
||||
s.writeError(c, http.StatusBadRequest, "DNS 账户名称或项目无效", nil)
|
||||
return
|
||||
}
|
||||
reference := ""
|
||||
@ -156,7 +168,7 @@ func (s *Server) renameManagedDNSAccount(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
updated, err := repo.UpdateDNSAccount(c.Request.Context(), c.GetString(authenticatedActorKey), c.Param("id"), request.Revision, item.Name, reference)
|
||||
updated, err := repo.UpdateDNSAccount(c.Request.Context(), c.GetString(authenticatedActorKey), c.Param("id"), request.Revision, item.Name, reference, targetProjectID)
|
||||
if err != nil {
|
||||
s.writeDNSAccountError(c, err)
|
||||
return
|
||||
|
||||
@ -13,6 +13,8 @@ import (
|
||||
|
||||
var ErrDNSAccountConflict = errors.New("DNS 账户已变更,请刷新后重试")
|
||||
|
||||
var ErrDNSAccountBound = errors.New("该 DNS 账户已被当前项目下的域名绑定,请先解除绑定")
|
||||
|
||||
var ErrLegacyDNSAccountConflict = errors.New("旧系统 DNS 账户标识已被其他账户占用")
|
||||
|
||||
// DNSAccountRepository 的公开读取不返回凭据引用;秘密读取只能使用独立的使用权限入口。
|
||||
@ -23,7 +25,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)
|
||||
UpdateDNSAccount(context.Context, string, string, int64, string, 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)
|
||||
@ -205,15 +207,74 @@ 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
|
||||
func (r *SQLProjectMemberRepository) UpdateDNSAccount(ctx context.Context, actorID, id string, revision int64, name, reference, projectID string) (domain.DNSAccount, error) {
|
||||
if err := r.ensureDB(); err != nil {
|
||||
return domain.DNSAccount{}, err
|
||||
}
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return domain.DNSAccount{}, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
item, err := scanDNSAccount(tx.QueryRowContext(ctx, Rebind(r.driver, `SELECT `+dnsAccountColumns+` FROM dns_accounts WHERE id = ?`+r.resourceWriteLock()), id))
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return domain.DNSAccount{}, ErrProjectAccessDenied
|
||||
}
|
||||
if err != nil {
|
||||
return domain.DNSAccount{}, err
|
||||
}
|
||||
if err := r.authorizeProjectTransaction(ctx, tx, actorID, item.ProjectID, domain.PermissionDNSManage); err != nil {
|
||||
return domain.DNSAccount{}, err
|
||||
}
|
||||
targetProjectID := strings.TrimSpace(projectID)
|
||||
if targetProjectID != "" && targetProjectID != item.ProjectID {
|
||||
if err := r.authorizeProjectTransaction(ctx, tx, actorID, targetProjectID, domain.PermissionDNSManage); err != nil {
|
||||
return domain.DNSAccount{}, err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
var boundCount int
|
||||
err := tx.QueryRowContext(ctx, Rebind(r.driver, `SELECT COUNT(*) FROM domain_dns_accounts WHERE project_id = ? AND dns_account_id = ?`), item.ProjectID, id).Scan(&boundCount)
|
||||
if err != nil {
|
||||
return domain.DNSAccount{}, err
|
||||
}
|
||||
if boundCount > 0 {
|
||||
return domain.DNSAccount{}, ErrDNSAccountBound
|
||||
}
|
||||
item.ProjectID = targetProjectID
|
||||
}
|
||||
if item.Revision != revision {
|
||||
return domain.DNSAccount{}, ErrDNSAccountConflict
|
||||
}
|
||||
if err := r.rejectActiveDNSLease(ctx, tx, "account_id", id); err != nil {
|
||||
return domain.DNSAccount{}, err
|
||||
}
|
||||
item.Name = name
|
||||
if reference != "" {
|
||||
item.CredentialRef = reference
|
||||
item.VerificationState = domain.DNSAccountUnverified
|
||||
}
|
||||
item.Revision++
|
||||
item.UpdatedAt = time.Now().UTC()
|
||||
if err := item.Validate(); err != nil {
|
||||
return domain.DNSAccount{}, err
|
||||
}
|
||||
result, err := tx.ExecContext(ctx, Rebind(r.driver, `UPDATE dns_accounts SET name = ?, project_id = ?, credential_ref = ?, status = ?, verification_state = ?, revision = ?, updated_at = ? WHERE id = ? AND revision = ?`),
|
||||
item.Name, item.ProjectID, item.CredentialRef, item.Status, item.VerificationState, item.Revision, item.UpdatedAt, item.ID, revision)
|
||||
if err != nil {
|
||||
if r.driver == "sqlite" && isSQLiteWriteConflict(err) {
|
||||
return domain.DNSAccount{}, ErrDNSAccountConflict
|
||||
}
|
||||
return domain.DNSAccount{}, errors.New("更新 DNS 账户失败,请检查凭据引用是否重复")
|
||||
}
|
||||
if count, err := result.RowsAffected(); err != nil || count != 1 {
|
||||
return domain.DNSAccount{}, ErrDNSAccountConflict
|
||||
}
|
||||
if err := writeDNSAccountAudit(ctx, tx, r.driver, actorID, item, "dns_account.update"); err != nil {
|
||||
return domain.DNSAccount{}, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return domain.DNSAccount{}, err
|
||||
}
|
||||
return publicDNSAccount(item), nil
|
||||
}
|
||||
|
||||
// 首次验证允许读取尚未验证的账户,但必须实时拥有管理权限,且账户未被禁用。
|
||||
|
||||
@ -32,13 +32,13 @@ onBeforeUnmount(() => previousFocus?.focus?.());
|
||||
账户名称
|
||||
<input v-model="editor.name" autofocus required maxlength="128" aria-label="账户名称" />
|
||||
</label>
|
||||
<label v-if="['create', 'edit'].includes(editor.action)">
|
||||
归属项目
|
||||
<select v-model="editor.project_id" required aria-label="归属项目">
|
||||
<option v-for="project in projects" :key="project.id" :value="project.id">{{ project.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<template v-if="editor.action === 'create'">
|
||||
<label>
|
||||
归属项目
|
||||
<select v-model="editor.project_id" required aria-label="归属项目">
|
||||
<option v-for="project in projects" :key="project.id" :value="project.id">{{ project.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
DNS 服务商
|
||||
<select v-model="editor.provider" required aria-label="DNS 服务商" @change="emit('provider-change')">
|
||||
|
||||
@ -55,9 +55,16 @@ export function useDNSAccountEditor(projects, providers, loading, refresh) {
|
||||
const form = editor.value;
|
||||
if (!mounted || !form || saving.value || loading.value) return;
|
||||
mutationError.value = "";
|
||||
if (form.action === "create" ? !managedProjects.value.some((item) => item.id === form.project_id) : !canManage(target)) {
|
||||
mutationError.value = "当前项目没有 DNS 管理权限,请刷新列表";
|
||||
return;
|
||||
if (form.action === "create") {
|
||||
if (!managedProjects.value.some((item) => item.id === form.project_id)) {
|
||||
mutationError.value = "当前项目没有 DNS 管理权限,请刷新列表";
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!canManage(target) || (form.action === "edit" && !managedProjects.value.some((item) => item.id === form.project_id))) {
|
||||
mutationError.value = "目标项目没有 DNS 管理权限,请刷新列表";
|
||||
return;
|
||||
}
|
||||
}
|
||||
const body = form.action === "create" ? {} : { revision: target.revision };
|
||||
if (["create", "edit", "rename"].includes(form.action)) {
|
||||
@ -67,6 +74,9 @@ export function useDNSAccountEditor(projects, providers, loading, refresh) {
|
||||
return;
|
||||
}
|
||||
body.name = name;
|
||||
if (form.action === "edit" && form.project_id && form.project_id !== target?.project_id) {
|
||||
body.project_id = form.project_id;
|
||||
}
|
||||
}
|
||||
if (["create", "edit", "rotate"].includes(form.action)) {
|
||||
const provider = providers.value.find((item) => item.id === (target?.provider || form.provider));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user