From 6f702cdc3616ea730a07c6d2b229b25286395f6c Mon Sep 17 00:00:00 2001 From: xuans Date: Sun, 4 Oct 2026 13:01:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E5=85=AC=E7=BD=91=E5=AF=86?= =?UTF-8?q?=E7=A0=81=E9=87=8D=E7=BD=AE=E5=AE=89=E5=85=A8=E9=98=BB=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 关闭公开密码重置流程,统一显示运维协助提示,保留本机受保护重置方向,并更新前端回归测试与整改清单。 --- .../httpapi/password_reset_guidance.go | 2 +- .../httpapi/password_reset_guidance_test.go | 2 +- doc/Scert整改合并优化清单.md | 11 ++ .../src/views/PasswordReset/PasswordReset.css | 22 +++- .../src/views/PasswordReset/PasswordReset.js | 55 +++----- .../views/PasswordReset/PasswordReset.test.js | 70 +--------- .../src/views/PasswordReset/PasswordReset.vue | 73 ++--------- .../views/SuperAdminReset/SuperAdminReset.css | 22 +++- .../views/SuperAdminReset/SuperAdminReset.js | 124 ++---------------- .../views/SuperAdminReset/SuperAdminReset.vue | 103 ++------------- 10 files changed, 112 insertions(+), 372 deletions(-) diff --git a/backend/internal/httpapi/password_reset_guidance.go b/backend/internal/httpapi/password_reset_guidance.go index c598f8c..a4b5256 100644 --- a/backend/internal/httpapi/password_reset_guidance.go +++ b/backend/internal/httpapi/password_reset_guidance.go @@ -38,7 +38,7 @@ func (s *Server) showPasswordResetGuidance(c *gin.Context) { c.JSON(http.StatusOK, publicPasswordResetGuidance{ Title: "密码重置", - Description: "请联系系统管理员协助处理密码重置。为保护账户安全,此页面不确认账户是否存在或所属角色。", + Description: "当前方式无法使用,请联系运维人员辅助解决。", Action: "contact_administrator", }) } diff --git a/backend/internal/httpapi/password_reset_guidance_test.go b/backend/internal/httpapi/password_reset_guidance_test.go index c038b3b..78116d9 100644 --- a/backend/internal/httpapi/password_reset_guidance_test.go +++ b/backend/internal/httpapi/password_reset_guidance_test.go @@ -36,7 +36,7 @@ func TestPublicPasswordResetGuidanceDoesNotEnumerateAccounts(t *testing.T) { if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil { t.Fatal(err) } - if len(payload) != 3 || payload["action"] != "contact_administrator" || payload["title"] != "密码重置" || payload["description"] != "请联系系统管理员协助处理密码重置。为保护账户安全,此页面不确认账户是否存在或所属角色。" { + if len(payload) != 3 || payload["action"] != "contact_administrator" || payload["title"] != "密码重置" || payload["description"] != "当前方式无法使用,请联系运维人员辅助解决。" { t.Fatalf("公开响应必须仅包含通用处理提示:%v", payload) } if response.Header().Get("Cache-Control") != "no-store" { diff --git a/doc/Scert整改合并优化清单.md b/doc/Scert整改合并优化清单.md index 677eecb..43e31f6 100644 --- a/doc/Scert整改合并优化清单.md +++ b/doc/Scert整改合并优化清单.md @@ -1,5 +1,16 @@ # Scert 整改、合并与优化清单 +## 2026年10月4日 公网超级管理员密码重置入口安全收口 + +- [x] Web 端 `/password-reset` 和 `/reset` 页面改为静态安全阻断提示,统一提示“当前方式无法使用,请联系运维人员辅助解决”,不再展示账号、验证码或新密码表单。 +- [x] 前端密码重置组件及兼容 composable 不再发起公网重置请求、不创建重置会话、不接收密码;页面明确说明必须由部署主机运维人员通过本机受保护终端处理。 +- [x] 公开密码处理接口仅返回通用提示,不查询账户、不发送验证码、不设置 Cookie;后端提示文案已与 Web 页面一致。 +- [x] 正式 HTTP 路由继续不注册 `/reset`,即使旧版 `Scert-admin` 状态文件存在,生产路径也不会回退到旧 HTML 重置表单;后端边界回归通过。 +- [x] 本地后端 `internal/httpapi` 测试、前端全量测试、前端生产构建及差异检查通过,变更等待复审。 +- [~] 仍需在第一台 Linux 部署候选包后,以真实 root 前台交互终端复核 Unix Socket 本机重置、系统邮件验证码送达和新密码登录闭环;未完成前不关闭生产验收门禁。 + +本项未开放公网端口,未操作第二台服务器,未创建辅助验收脚本,未读取或记录任何密码、验证码或邮件密钥。Windows 环境不在本轮范围。 + ## 2026年10月4日11:07 权限回归与第一台终态复核 - [✓] 邮件通知配置继续仅允许超管读写;前端直接调用设置入口也拒绝普通管理员和用户,并清除未保存密码。新增角色矩阵、降权、停用和删除会话回归通过,拒绝请求没有配置或秘密仓库副作用。 diff --git a/frontend/src/views/PasswordReset/PasswordReset.css b/frontend/src/views/PasswordReset/PasswordReset.css index a1e3ddb..5e79da7 100644 --- a/frontend/src/views/PasswordReset/PasswordReset.css +++ b/frontend/src/views/PasswordReset/PasswordReset.css @@ -2,6 +2,26 @@ max-width: 470px; } +.reset-blocked-message { + display: grid; + gap: 8px; + margin: 4px 0 16px; + padding: 16px; + border: 1px solid #f0d4a8; + border-radius: 14px; + background: #fff8eb; + color: #78531b; + line-height: 1.65; +} + +.reset-blocked-message strong { + color: #684511; +} + +.reset-blocked-message p { + margin: 0; +} + .reset-resolution { display: grid; gap: 8px; @@ -53,4 +73,4 @@ .access-submit:disabled { cursor: wait; opacity: 0.65; -} \ No newline at end of file +} diff --git a/frontend/src/views/PasswordReset/PasswordReset.js b/frontend/src/views/PasswordReset/PasswordReset.js index a078e6f..74d05c7 100644 --- a/frontend/src/views/PasswordReset/PasswordReset.js +++ b/frontend/src/views/PasswordReset/PasswordReset.js @@ -2,50 +2,27 @@ import { ref } from "vue"; export function usePasswordReset() { const identifier = ref(""); - const resolution = ref(null); - const message = ref(""); + const resolution = ref({ + action: "contact_administrator", + title: "密码重置", + description: "当前方式无法使用,请联系运维人员辅助解决。", + }); + const message = ref("当前方式无法使用,请联系运维人员辅助解决。"); const error = ref(""); const submitting = ref(false); - async function submit() { - if (submitting.value) { - return; - } - message.value = ""; - error.value = ""; - resolution.value = null; - if (!identifier.value.trim()) { - error.value = "请输入用户名或绑定邮箱。"; - return; - } - - submitting.value = true; - try { - const response = await fetch("/api/v1/password-reset/resolve", { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - credentials: "same-origin", - body: JSON.stringify({ identifier: identifier.value.trim() }), - }); - const result = await response.json().catch(() => ({})); - if (!response.ok) { - throw new Error(result.error || "暂时无法处理密码重置请求,请稍后重试。"); - } - resolution.value = result; - message.value = result.description || "已返回密码处理方式。"; - } catch (submitError) { - error.value = submitError.message || "查询密码重置方式失败,请稍后重试。"; - } finally { - submitting.value = false; - } + function submit() { + error.value = "当前方式无法使用,请联系运维人员辅助解决。"; } function reset() { - resolution.value = null; - message.value = ""; + identifier.value = ""; + resolution.value = { + action: "contact_administrator", + title: "密码重置", + description: "当前方式无法使用,请联系运维人员辅助解决。", + }; + message.value = "当前方式无法使用,请联系运维人员辅助解决。"; error.value = ""; } @@ -58,4 +35,4 @@ export function usePasswordReset() { submit, reset, }; -} \ No newline at end of file +} diff --git a/frontend/src/views/PasswordReset/PasswordReset.test.js b/frontend/src/views/PasswordReset/PasswordReset.test.js index b2e9577..0945760 100644 --- a/frontend/src/views/PasswordReset/PasswordReset.test.js +++ b/frontend/src/views/PasswordReset/PasswordReset.test.js @@ -3,9 +3,6 @@ import { readFileSync } from "node:fs"; import test from "node:test"; import { baseParse } from "@vue/compiler-dom"; import { parse } from "@vue/compiler-sfc"; -import { usePasswordReset } from "./PasswordReset.js"; -import { mountComposable } from "../../test-support/mount.js"; - function template(path) { const source = readFileSync(new URL(path, import.meta.url), "utf8"); return baseParse(parse(source).descriptor.template.content); @@ -17,66 +14,13 @@ function elements(node, predicate, matches = []) { return matches; } -test("超级管理员查询保留本机处理标识,不生成网页重置链接", async (t) => { - const payload = { - account_type: "super_admin", - action: "contact_administrator", - title: "超级管理员密码重置", - description: "请在部署主机操作。", - }; - const { state } = await mountComposable(t, usePasswordReset, async (url, options) => { - assert.equal(url, "/api/v1/password-reset/resolve"); - assert.equal(options.credentials, "same-origin"); - assert.deepEqual(JSON.parse(options.body), { identifier: "owner" }); - return Response.json(payload); - }); - state.identifier.value = " owner "; - await state.submit(); - assert.equal(state.resolution.value.action, "contact_administrator"); - assert.equal("reset_path" in state.resolution.value, false); - assert.doesNotMatch(JSON.stringify(state.resolution.value), /local_admin_command|systemctl|scert-admin/iu); - assert.equal(state.error.value, ""); - assert.equal(state.submitting.value, false); - state.reset(); - assert.equal(state.resolution.value, null); -}); - -test("密码重置页面不显示本机管理命令", () => { +test("公开密码重置页面只显示安全阻断提示", () => { const root = template("./PasswordReset.vue"); - const guides = elements(root, (node) => node.tag === "LocalResetGuide"); - assert.equal(guides.length, 0); const source = readFileSync(new URL("./PasswordReset.vue", import.meta.url), "utf8"); - assert.doesNotMatch(source, /systemctl|scert-admin|本机管理命令/iu); - const message = elements(root, (node) => node.tag === "p" - && node.props.some((prop) => prop.name === "if" - && prop.exp?.content === "message && !resolution")); - assert.equal(message.length, 1); -}); - -test("本机指引不包含可执行命令", () => { - const root = template("../components/LocalResetGuide/LocalResetGuide.vue"); - assert.equal(elements(root, (node) => ["a", "input", "form"].includes(node.tag)).length, 0); - assert.equal(elements(root, (node) => node.tag === "HelpTip").length, 1); - assert.equal(elements(root, (node) => node.tag === "code").length, 0); - const source = readFileSync(new URL("../components/LocalResetGuide/LocalResetGuide.vue", import.meta.url), "utf8"); - assert.doesNotMatch(source, /systemctl|scert-admin| { - const payload = { - title: "密码重置", - description: "请联系系统管理员协助处理密码重置。为保护账户安全,此页面不确认账户是否存在或所属角色。", - action: "contact_administrator", - }; - const { state } = await mountComposable(t, usePasswordReset, async () => Response.json(payload)); - for (const identifier of ["owner", "owner@example.test", "absent", "absent@example.test"]) { - state.identifier.value = identifier; - await state.submit(); - assert.deepEqual(state.resolution.value, payload); - assert.equal(state.error.value, ""); - assert.equal(state.submitting.value, false); - for (const field of ["account_type", "account_label", "reset_path", "reset_url", "token", "nonce"]) { - assert.equal(field in state.resolution.value, false); - } - } + assert.match(source, /当前方式无法使用/); + assert.match(source, /请联系运维人员辅助解决/); + assert.doesNotMatch(source, /fetch\(|password-reset\/resolve|systemctl|scert-admin/iu); + assert.equal(elements(root, (node) => node.tag === "form").length, 0); + assert.equal(elements(root, (node) => node.tag === "input").length, 0); + assert.equal(elements(root, (node) => node.tag === "button").length, 0); }); diff --git a/frontend/src/views/PasswordReset/PasswordReset.vue b/frontend/src/views/PasswordReset/PasswordReset.vue index b821285..921e250 100644 --- a/frontend/src/views/PasswordReset/PasswordReset.vue +++ b/frontend/src/views/PasswordReset/PasswordReset.vue @@ -1,80 +1,33 @@