183 lines
5.5 KiB
Go
183 lines
5.5 KiB
Go
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
|
||
}
|