将密码找回指引切换为本机管理命令
This commit is contained in:
parent
a0ed64d0b0
commit
6af7c5ab3b
@ -5,7 +5,6 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/admin"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/auth"
|
||||
)
|
||||
|
||||
@ -21,7 +20,6 @@ type passwordResetResolution struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Action string `json:"action"`
|
||||
ResetPath string `json:"reset_path,omitempty"`
|
||||
}
|
||||
|
||||
// resolvePasswordResetAccount 根据账户标识返回对应的密码处理内容。
|
||||
@ -52,8 +50,7 @@ func (s *Server) resolvePasswordResetAccount(c *gin.Context) {
|
||||
AccountLabel: "超级管理员",
|
||||
Title: "超级管理员密码重置",
|
||||
Description: "已识别为超级管理员账户。超级管理员不能通过普通重置页面修改密码,请使用 Scert-admin 开启的一次性重置入口。",
|
||||
Action: "use_super_admin_entry",
|
||||
ResetPath: admin.ResetPath,
|
||||
Action: "local_admin_command",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
46
backend/internal/httpapi/password_reset_local_test.go
Normal file
46
backend/internal/httpapi/password_reset_local_test.go
Normal file
@ -0,0 +1,46 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPasswordResetResolutionOnlyOffersLocalAdminCommand(t *testing.T) {
|
||||
cfg := readyTestConfig(t.TempDir())
|
||||
cfg.Admin.Username = "owner"
|
||||
cfg.Admin.Email = "owner@example.test"
|
||||
server := New(Dependencies{Config: cfg, Repository: &stubDomainRepository{}})
|
||||
for _, identifier := range []string{"owner", "owner@example.test"} {
|
||||
t.Run(identifier, func(t *testing.T) {
|
||||
body, err := json.Marshal(map[string]string{"identifier": identifier})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/password-reset/resolve", strings.NewReader(string(body)))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(response, request)
|
||||
if response.Code != http.StatusOK {
|
||||
t.Fatalf("查询失败:%d %s", response.Code, response.Body.String())
|
||||
}
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if payload["account_type"] != "super_admin" || payload["action"] != "local_admin_command" {
|
||||
t.Fatalf("超级管理员没有返回本机命令指引:%v", payload)
|
||||
}
|
||||
for _, field := range []string{"reset_path", "reset_url", "token", "nonce"} {
|
||||
if _, exists := payload[field]; exists {
|
||||
t.Fatalf("公开查询泄露了重置入口或会话字段:%s", field)
|
||||
}
|
||||
}
|
||||
if len(response.Result().Cookies()) != 0 {
|
||||
t.Fatal("公开查询不应创建重置会话")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,7 @@
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "node --test src/App.test.js src/views/SetupWizard/SetupWizard.test.js src/views/SetupWizard/SetupWizardBootstrap.test.js src/views/SetupWizard/DatabaseSetup.test.js src/views/SetupWizard/SetupWizardLayout.test.js src/views/SetupWizard/BootstrapPayload.test.js src/views/Dashboard/Dashboard.test.js src/views/UserManagement/UserManagement.test.js src/views/components/HelpTip/HelpTip.test.js",
|
||||
"test": "node --test src/App.test.js src/views/PasswordReset/PasswordReset.test.js src/views/SetupWizard/SetupWizard.test.js src/views/SetupWizard/SetupWizardBootstrap.test.js src/views/SetupWizard/DatabaseSetup.test.js src/views/SetupWizard/SetupWizardLayout.test.js src/views/SetupWizard/BootstrapPayload.test.js src/views/Dashboard/Dashboard.test.js src/views/UserManagement/UserManagement.test.js src/views/components/HelpTip/HelpTip.test.js",
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
|
||||
63
frontend/src/views/PasswordReset/PasswordReset.test.js
Normal file
63
frontend/src/views/PasswordReset/PasswordReset.test.js
Normal file
@ -0,0 +1,63 @@
|
||||
import assert from "node:assert/strict";
|
||||
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);
|
||||
}
|
||||
|
||||
function elements(node, predicate, matches = []) {
|
||||
if (predicate(node)) matches.push(node);
|
||||
for (const child of node.children || []) elements(child, predicate, matches);
|
||||
return matches;
|
||||
}
|
||||
|
||||
test("超级管理员查询保留本机处理标识,不生成网页重置链接", async (t) => {
|
||||
const payload = {
|
||||
account_type: "super_admin",
|
||||
action: "local_admin_command",
|
||||
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, "local_admin_command");
|
||||
assert.equal("reset_path" in state.resolution.value, false);
|
||||
assert.equal(state.error.value, "");
|
||||
assert.equal(state.submitting.value, false);
|
||||
state.reset();
|
||||
assert.equal(state.resolution.value, null);
|
||||
});
|
||||
|
||||
test("本机命令指引仅对接口明确标记的处理方式显示", () => {
|
||||
const root = template("./PasswordReset.vue");
|
||||
const guides = elements(root, (node) => node.tag === "LocalResetGuide");
|
||||
assert.equal(guides.length, 1);
|
||||
assert.equal(guides[0].props.find((prop) => prop.name === "if").exp.content,
|
||||
"resolution.action === 'local_admin_command'");
|
||||
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);
|
||||
const commands = elements(root, (node) => node.tag === "code")
|
||||
.map((node) => node.children.filter((child) => child.type === 2)
|
||||
.map((child) => child.content).join(""));
|
||||
assert.deepEqual(commands, ["systemctl start Scert-admin", ".\\scert-admin.exe"]);
|
||||
});
|
||||
@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import AccessShell from "../components/AccessShell/AccessShell.vue";
|
||||
import LocalResetGuide from "../components/LocalResetGuide/LocalResetGuide.vue";
|
||||
import { usePasswordReset } from "./PasswordReset.js";
|
||||
|
||||
const {
|
||||
@ -47,13 +48,14 @@ const {
|
||||
/>
|
||||
</label>
|
||||
|
||||
<p v-if="message" class="access-message">{{ message }}</p>
|
||||
<p v-if="message && !resolution" class="access-message">{{ message }}</p>
|
||||
<p v-if="error" class="access-error">{{ error }}</p>
|
||||
|
||||
<div v-if="resolution" class="reset-resolution" aria-live="polite">
|
||||
<span class="reset-resolution-label">处理方式</span>
|
||||
<strong>{{ resolution.title }}</strong>
|
||||
<p>{{ resolution.description }}</p>
|
||||
<LocalResetGuide v-if="resolution.action === 'local_admin_command'" />
|
||||
<a
|
||||
v-if="resolution.reset_path"
|
||||
class="access-submit reset-entry-link"
|
||||
|
||||
@ -0,0 +1,38 @@
|
||||
.local-reset-guide {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.local-reset-guide__heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.local-reset-guide__commands {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.local-reset-guide__commands > div {
|
||||
display: grid;
|
||||
grid-template-columns: 58px minmax(0, 1fr);
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.local-reset-guide__commands dt,
|
||||
.local-reset-guide__commands dd {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.local-reset-guide__commands code {
|
||||
overflow-wrap: anywhere;
|
||||
font-family: Consolas, monospace;
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
<script setup>
|
||||
import HelpTip from "../HelpTip/HelpTip.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="local-reset-guide">
|
||||
<div class="local-reset-guide__heading">
|
||||
<span>本机管理命令</span>
|
||||
<HelpTip>
|
||||
请由部署管理员在 Scert 所在主机执行命令。Linux 使用 root 终端;
|
||||
Windows 使用提升权限的控制台,在安装目录运行。
|
||||
验证码和新密码仅在本机终端输入,本页面不会开启重置会话。
|
||||
</HelpTip>
|
||||
</div>
|
||||
<dl class="local-reset-guide__commands">
|
||||
<div>
|
||||
<dt>Linux</dt>
|
||||
<dd><code>systemctl start Scert-admin</code></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Windows</dt>
|
||||
<dd><code>.\scert-admin.exe</code></dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped src="./LocalResetGuide.css"></style>
|
||||
Loading…
Reference in New Issue
Block a user