Scert/backend/internal/adminconsole/flow.go
xuans 9cd8ce3530 优化本机重置不可用提示
当系统通知或本机重置通道不可用时,向运维人员显示明确的中文原因,并补充回归测试。
2026-10-05 03:15:09 +08:00

183 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package adminconsole
import (
"context"
"errors"
"fmt"
"io"
"net"
"strings"
"git.pcux.cn/xuans/Scert/backend/internal/localreset"
"git.pcux.cn/xuans/Scert/backend/internal/passwordpolicy"
)
var (
ErrUnauthorizedPeer = localreset.ErrUnauthorized
ErrInvalidRequest = localreset.ErrProtocol
)
type terminal interface {
Read(context.Context, string, bool) (string, error)
Show(string) error
}
type resetClient interface {
Methods() []localreset.Method
Select(string) error
Verify(string) error
Reset(string) error
Close() error
}
// Run 只接受无参数的本机交互,不从参数、环境变量、管道或文件读取密码。
func Run(parent context.Context, args []string) error {
if len(args) != 0 {
return errors.New("Scert-admin 不接受参数;Linux 请在管理员终端执行 systemctl start Scert-admin")
}
ctx, cancel := context.WithTimeout(parent, localreset.SessionTimeout)
defer cancel()
ctx, screen, closeScreen, err := openTerminal(ctx)
if err != nil {
return errors.New("本机管理员终端校验失败;Linux 请执行 systemctl start Scert-admin,Windows 请使用提升权限的控制台")
}
defer closeScreen()
conn, err := dialControl(ctx)
if err != nil {
_ = screen.Show("本机重置通道不可用,请确认 Scert 主服务已完成初始化并正常运行。")
return localreset.ErrUnavailable
}
return runConnected(ctx, conn, screen)
}
func runConnected(ctx context.Context, conn net.Conn, screen terminal) error {
client, err := localreset.Begin(ctx, conn)
if err != nil {
_ = screen.Show("主服务拒绝了本机重置会话,请检查初始化状态及绑定邮箱。")
return localreset.ErrUnavailable
}
defer client.Close()
if client.RecoveryAvailable() {
prompt := "本机管理:1 重置超级管理员密码;2 只读核查部署事务;其他输入退出"
if client.DispositionAvailable() {
prompt = "本机管理:1 重置超级管理员密码;2 只读核查部署事务;3 人工处置部署事务;4 查询人工处置结果;5 重新核查不确定处置;其他输入退出"
}
choice, err := screen.Read(ctx, prompt, false)
if err != nil {
return err
}
switch strings.TrimSpace(choice) {
case "1":
case "2":
if err := recoveryFlow(ctx, client, screen); err != nil {
_ = screen.Show("部署核查未完成:会话过期、输入无效或现场不可读取。未修改事务或远端文件。请重新启动本机管理。")
return err
}
return nil
case "3", "4", "5":
if !client.DispositionAvailable() {
return localreset.ErrUnavailable
}
if strings.TrimSpace(choice) == "4" {
return recoveryResultFlow(ctx, client, screen)
}
if strings.TrimSpace(choice) == "5" {
return recoveryReconciliationFlow(ctx, client, screen)
}
return recoveryDispositionFlow(ctx, client, screen)
default:
return nil
}
}
if err := resetFlow(ctx, client, screen); err != nil {
if errors.Is(err, localreset.ErrUnavailable) {
showResetFailure(screen, err)
return localreset.ErrProtocol
}
_ = screen.Show("密码重置未完成:操作已取消、验证失败、会话过期或保存失败。请检查服务日志后重新启动。")
return localreset.ErrProtocol
}
if err := screen.Show("超级管理员密码重置成功,旧登录会话已失效,请使用新密码登录。"); err != nil {
return errors.New("密码已重置,但无法向调用终端显示结果,请使用新密码登录")
}
return nil
}
func showResetFailure(screen terminal, err error) {
message := `密码重置未完成:操作已取消、验证失败、会话过期或保存失败。请检查服务日志后重新启动。`
if errors.Is(err, localreset.ErrUnavailable) {
message = `密码重置未完成:验证码发送服务或本机重置通道不可用。请先恢复系统通知服务后重新启动。`
}
_ = screen.Show(message)
}
func resetFlow(ctx context.Context, client resetClient, screen terminal) error {
methods := client.Methods()
if len(methods) != 1 || methods[0].ID != "email" {
return localreset.ErrUnavailable
}
choice, err := screen.Read(ctx, "验证方式:绑定邮箱验证码。输入 1 发送验证码,其他输入取消", false)
if err != nil || strings.TrimSpace(choice) != "1" {
return localreset.ErrProtocol
}
if err := ctx.Err(); err != nil {
return err
}
if err := client.Select("email"); err != nil {
return err
}
code, err := screen.Read(ctx, "请输入绑定邮箱收到的八位验证码(不回显)", true)
if err != nil {
return err
}
if err := ctx.Err(); err != nil {
return err
}
err = client.Verify(code)
code = ""
if err != nil {
return err
}
first, err := screen.Read(ctx, "请输入新密码(8 到 12 个字符,包含字母和数字,不回显)", true)
if err != nil {
return err
}
if err := passwordpolicy.Validate(first, ""); err != nil {
return err
}
second, err := screen.Read(ctx, "请再次输入新密码(不回显)", true)
if err != nil {
first = ""
return err
}
if first != second {
first, second = "", ""
return localreset.ErrProtocol
}
second = ""
if err := ctx.Err(); err != nil {
first = ""
return err
}
err = client.Reset(first)
first = ""
return err
}
func clearBytes(value []byte) { clear(value) }
func stripPasswordLineEnding(value []byte) []byte {
if len(value) > 0 && value[len(value)-1] == '\n' {
value = value[:len(value)-1]
}
if len(value) > 0 && value[len(value)-1] == '\r' {
value = value[:len(value)-1]
}
return value
}
func showLine(writer io.Writer, message string) error {
_, err := fmt.Fprintln(writer, message)
return err
}