继续修复DNS账户编辑功能
保存当前DNS账户编辑功能的阶段性修改,后续继续完善测试与权限校验。
This commit is contained in:
parent
030624a1b1
commit
4aca34c16d
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@ onBeforeUnmount(() => previousFocus?.focus?.());
|
||||
<button type="button" :disabled="saving" aria-label="关闭账户编辑" @click="emit('close')">关闭</button>
|
||||
</header>
|
||||
<fieldset :disabled="saving">
|
||||
<label v-if="['create', 'rename'].includes(editor.action)">
|
||||
<label v-if="['create', 'edit', 'rename'].includes(editor.action)">
|
||||
账户名称
|
||||
<input v-model="editor.name" autofocus required maxlength="128" aria-label="账户名称" />
|
||||
</label>
|
||||
@ -46,19 +46,22 @@ onBeforeUnmount(() => previousFocus?.focus?.());
|
||||
</select>
|
||||
</label>
|
||||
</template>
|
||||
<template v-if="['create', 'rotate'].includes(editor.action)">
|
||||
<template v-if="['create', 'edit', 'rotate'].includes(editor.action)">
|
||||
<label v-for="field in provider?.credential_fields || []" :key="`${editor.provider}-${field.name}`" :title="field.placeholder">
|
||||
{{ field.label }}{{ field.required ? '(必填)' : '(可选)' }}
|
||||
<input
|
||||
v-model="editor.credentials[field.name]"
|
||||
:type="field.secret ? 'password' : 'text'"
|
||||
:required="field.required"
|
||||
:required="field.required && editor.action !== 'edit'"
|
||||
:aria-label="field.label"
|
||||
:placeholder="field.placeholder"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</label>
|
||||
<p v-if="editor.action === 'edit'" class="dns-dialog-hint">
|
||||
凭据字段留空表示保留当前配置;填写任意字段将替换全部凭据,并需要重新验证。
|
||||
</p>
|
||||
</template>
|
||||
<label v-if="editor.action === 'verify'" title="仅填写你有权管理的域名。验证会创建、查询并清理临时 DNS 记录。">
|
||||
授权测试域名
|
||||
|
||||
@ -2,7 +2,7 @@ import { computed, onBeforeUnmount, ref } from "vue";
|
||||
import { requestDNS } from "./DNSAccountRequest.js";
|
||||
|
||||
const titles = {
|
||||
create: "新增 DNS 账户", rename: "修改账户名称", rotate: "轮换 DNS 凭据",
|
||||
create: "新增 DNS 账户", edit: "编辑 DNS 账户", rename: "修改账户名称", rotate: "轮换 DNS 凭据",
|
||||
verify: "验证 DNS 权限", status: "变更账户状态",
|
||||
};
|
||||
|
||||
@ -60,7 +60,7 @@ export function useDNSAccountEditor(projects, providers, loading, refresh) {
|
||||
return;
|
||||
}
|
||||
const body = form.action === "create" ? {} : { revision: target.revision };
|
||||
if (["create", "rename"].includes(form.action)) {
|
||||
if (["create", "edit", "rename"].includes(form.action)) {
|
||||
const name = form.name.trim();
|
||||
if (!name || [...name].length > 128 || /[\u0000-\u001f\u007f-\u009f]/u.test(name)) {
|
||||
mutationError.value = "账户名称必须为不含控制字符的 1~128 个字符";
|
||||
@ -68,21 +68,22 @@ export function useDNSAccountEditor(projects, providers, loading, refresh) {
|
||||
}
|
||||
body.name = name;
|
||||
}
|
||||
if (["create", "rotate"].includes(form.action)) {
|
||||
if (["create", "edit", "rotate"].includes(form.action)) {
|
||||
const provider = providers.value.find((item) => item.id === (target?.provider || form.provider));
|
||||
if (!provider?.available || !Array.isArray(provider.credential_fields)) {
|
||||
mutationError.value = "请选择可用的 DNS 服务商";
|
||||
return;
|
||||
}
|
||||
body.credentials = {};
|
||||
const credentials = {};
|
||||
for (const field of provider.credential_fields) {
|
||||
const value = String(form.credentials[field.name] || "").trim();
|
||||
if (field.required && !value) {
|
||||
if (field.required && form.action !== "edit" && !value) {
|
||||
mutationError.value = `请填写${field.label}`;
|
||||
return;
|
||||
}
|
||||
if (value) body.credentials[field.name] = value;
|
||||
if (value) credentials[field.name] = value;
|
||||
}
|
||||
if (form.action !== "edit" || Object.keys(credentials).length > 0) body.credentials = credentials;
|
||||
if (form.action === "create") Object.assign(body, { provider: provider.id, project_id: form.project_id });
|
||||
}
|
||||
if (form.action === "verify") {
|
||||
@ -93,7 +94,7 @@ export function useDNSAccountEditor(projects, providers, loading, refresh) {
|
||||
}
|
||||
}
|
||||
if (form.action === "status") body.enabled = target.status !== "active";
|
||||
const suffix = { rename: "", rotate: "/credentials", verify: "/verify", status: "/status" };
|
||||
const suffix = { edit: "", rename: "", rotate: "/credentials", verify: "/verify", status: "/status" };
|
||||
const path = "/api/v1/dns/accounts" + (form.action === "create" ? "" : `/${encodeURIComponent(target.id)}${suffix[form.action]}`);
|
||||
saving.value = true;
|
||||
notice.value = "";
|
||||
@ -104,7 +105,7 @@ export function useDNSAccountEditor(projects, providers, loading, refresh) {
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!mounted) return;
|
||||
notice.value = ["create", "rotate"].includes(form.action)
|
||||
notice.value = ["create", "edit", "rotate"].includes(form.action)
|
||||
? "账户已保存,请使用有权管理的测试域名执行在线验证" : "账户操作已完成";
|
||||
} catch (reason) {
|
||||
if (!mounted) return;
|
||||
|
||||
@ -62,6 +62,7 @@ test("DNS 新建只发送白名单字段并清空凭据", async (t) => {
|
||||
});
|
||||
|
||||
for (const [action, suffix, body, method] of [
|
||||
["edit", "", { revision: 7, name: "新名称", credentials: { api_token: "replacement-token" } }, "PUT"],
|
||||
["rename", "", { revision: 7, name: "新名称" }, "PUT"],
|
||||
["rotate", "/credentials", { revision: 7, credentials: { api_token: "replacement-token" } }, "PUT"],
|
||||
["status", "/status", { revision: 7, enabled: false }, "PUT"],
|
||||
@ -82,9 +83,18 @@ for (const [action, suffix, body, method] of [
|
||||
});
|
||||
}
|
||||
|
||||
test("编辑账户时凭据留空表示保留原配置", async (t) => {
|
||||
const { state, calls } = await setup(t);
|
||||
state.openEditor("edit", account);
|
||||
state.editor.value.name = "仅修改名称";
|
||||
await state.saveEditor();
|
||||
const write = calls.find((item) => item.method);
|
||||
assert.deepEqual(JSON.parse(write.body), { revision: 7, name: "仅修改名称" });
|
||||
});
|
||||
|
||||
test("只读项目和历史配置不能发起账户写操作", async (t) => {
|
||||
const { state, calls } = await setup(t, undefined, { manage: false });
|
||||
for (const action of ["create", "rename", "rotate", "status", "verify"]) {
|
||||
for (const action of ["create", "edit", "rename", "rotate", "status", "verify"]) {
|
||||
state.openEditor(action, account);
|
||||
assert.equal(state.editor.value, null);
|
||||
}
|
||||
|
||||
@ -63,9 +63,8 @@ watch(() => props.refreshKey, loadAccounts);
|
||||
</td>
|
||||
<td>
|
||||
<div v-if="canManage(account)" class="dns-row-actions">
|
||||
<button type="button" :disabled="saving" @click="openEditor('rename', account)">改名</button>
|
||||
<button type="button" :disabled="saving" @click="openEditor('edit', account)">编辑</button>
|
||||
<button type="button" :disabled="saving || account.status !== 'active'" @click="openEditor('verify', account)">验证</button>
|
||||
<button type="button" :disabled="saving" title="保存新凭据并使旧验证状态失效" @click="openEditor('rotate', account)">轮换</button>
|
||||
<button type="button" :disabled="saving" @click="openEditor('status', account)">{{ account.status === 'active' ? '停用' : '启用' }}</button>
|
||||
</div>
|
||||
<span v-else class="dns-muted" :title="account.managed ? '当前项目只读,操作权限由服务器实时复核' : '历史系统账户尚未迁移,此处仅展示,不支持独立账户操作'">{{ account.managed ? '只读' : '历史配置' }}</span>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user