feat: add resetpwd cmd

This commit is contained in:
engigu
2026-02-27 23:08:44 +08:00
parent 8775bec155
commit d9e73a9d18
10 changed files with 206 additions and 16 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ tmp_dir = "bin"
args_bin = [] args_bin = []
bin = "./bin/baihu" bin = "./bin/baihu"
cmd = ":" cmd = ":"
full_bin = "go run main.go" full_bin = "go run main.go server"
delay = 1000 delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata", "web", "data", "envs", "configs", "bin"] exclude_dir = ["assets", "tmp", "vendor", "testdata", "web", "data", "envs", "configs", "bin"]
exclude_file = [] exclude_file = []
+15
View File
@@ -0,0 +1,15 @@
package cmd
import (
"github.com/engigu/baihu-panel/cmd/reposync"
"github.com/engigu/baihu-panel/cmd/resetpwd"
)
// CommandHandler 定义命令执行函数
type CommandHandler func(args []string)
// Handlers 维护了除了 server 之外的命令的执行入口
var Handlers = map[string]CommandHandler{
"reposync": reposync.Run,
"resetpwd": resetpwd.Run,
}
+54
View File
@@ -0,0 +1,54 @@
package resetpwd
import (
"bufio"
"fmt"
"os"
"strings"
"github.com/engigu/baihu-panel/internal/bootstrap"
"github.com/engigu/baihu-panel/internal/services"
"github.com/engigu/baihu-panel/internal/utils"
)
func Run(args []string) {
fmt.Print("此操作将重置 admin 用户的密码,是否继续? (y/N): ")
reader := bufio.NewReader(os.Stdin)
answer, _ := reader.ReadString('\n')
answer = strings.TrimSpace(strings.ToLower(answer))
if answer != "y" && answer != "yes" {
fmt.Println("操作已取消。")
return
}
// 必须初始化环境与数据库才能修改密码
bootstrap.New()
userService := services.NewUserService()
adminUser := userService.GetUserByUsername("admin")
if adminUser == nil {
fmt.Println("找不到 admin 用户。")
return
}
fmt.Print("请输入 admin 用户的新密码 (留空则自动随机生成): ")
inputPwd, _ := reader.ReadString('\n')
newPassword := strings.TrimSpace(inputPwd)
if newPassword == "" {
newPassword = utils.RandomString(12)
fmt.Println("未输入密码,系统已自动生成。")
}
err := userService.UpdatePassword(adminUser.ID, newPassword)
if err != nil {
fmt.Printf("重置密码失败: %v\n", err)
return
}
fmt.Println("--------------------------------------------------")
fmt.Println("admin 用户密码已重置成功:")
fmt.Printf("新密码: %s\n", newPassword)
fmt.Println("请妥善保管您的新密码,并登录后及时修改。")
fmt.Println("--------------------------------------------------")
}
+19
View File
@@ -3,6 +3,8 @@ package bootstrap
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"runtime"
"github.com/engigu/baihu-panel/internal/constant" "github.com/engigu/baihu-panel/internal/constant"
"github.com/engigu/baihu-panel/internal/database" "github.com/engigu/baihu-panel/internal/database"
@@ -42,6 +44,23 @@ func (a *App) initConfig() {
if err != nil { if err != nil {
return return
} }
a.setupBaihuBin()
}
func (a *App) setupBaihuBin() {
binDir := filepath.Join(constant.DataDir, "bin")
_ = os.MkdirAll(binDir, 0755)
exe, err := os.Executable()
if err == nil {
linkPath := filepath.Join(binDir, "baihu")
if runtime.GOOS == "windows" {
linkPath += ".exe"
}
os.Remove(linkPath)
_ = os.Symlink(exe, linkPath)
}
} }
func (a *App) initDatabase() { func (a *App) initDatabase() {
+23
View File
@@ -0,0 +1,23 @@
package constant
// CommandInfo 定义了终端可用命令的说明信息
type CommandInfo struct {
Name string
Description string
}
// Commands 是系统的可用业务命令说明列表
var Commands = []CommandInfo{
// {
// Name: "server",
// Description: "启动后台服务进程",
// },
{
Name: "reposync",
Description: "同步远程 Git 仓库或文件到本地",
},
{
Name: "resetpwd",
Description: "重置 admin 用户密码(需要二次确认)",
},
}
@@ -112,6 +112,12 @@ func (tc *TerminalController) handlePtyMode(conn *websocket.Conn, userID int) {
cmd.Env = append(os.Environ(), "TERM=xterm-256color") cmd.Env = append(os.Environ(), "TERM=xterm-256color")
// 注入 baihu 命令环境变量
if absBinDir, err := filepath.Abs(filepath.Join(constant.DataDir, "bin")); err == nil {
pathStr := absBinDir + string(os.PathListSeparator) + os.Getenv("PATH")
cmd.Env = append(cmd.Env, "PATH="+pathStr)
}
// 注入环境变量 // 注入环境变量
envVars := tc.envService.GetEnvVarsByUserID(userID) envVars := tc.envService.GetEnvVarsByUserID(userID)
for _, env := range envVars { for _, env := range envVars {
@@ -180,6 +186,13 @@ func (tc *TerminalController) handlePipeMode(conn *websocket.Conn, userID int) {
// 注入环境变量 // 注入环境变量
cmd.Env = os.Environ() cmd.Env = os.Environ()
// 注入 baihu 命令环境变量
if absBinDir, err := filepath.Abs(filepath.Join(constant.DataDir, "bin")); err == nil {
pathStr := absBinDir + string(os.PathListSeparator) + os.Getenv("PATH")
cmd.Env = append(cmd.Env, "PATH="+pathStr)
}
envVars := tc.envService.GetEnvVarsByUserID(userID) envVars := tc.envService.GetEnvVarsByUserID(userID)
for _, env := range envVars { for _, env := range envVars {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", env.Name, env.Value)) cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", env.Name, env.Value))
@@ -279,3 +292,15 @@ func (tc *TerminalController) ExecuteShellCommand(c *gin.Context) {
"output": string(output), "output": string(output),
}) })
} }
// GetCommands 获取所有可用的 cmd 列表及说明
func (tc *TerminalController) GetCommands(c *gin.Context) {
var cmds []map[string]string
for _, cmdInfo := range constant.Commands {
cmds = append(cmds, map[string]string{
"name": cmdInfo.Name,
"description": cmdInfo.Description,
})
}
utils.Success(c, cmds)
}
+1
View File
@@ -181,6 +181,7 @@ func Setup(c *Controllers) *gin.Engine {
// 终端模块 // 终端模块
authorized.GET("/terminal/ws", c.Terminal.HandleWebSocket) authorized.GET("/terminal/ws", c.Terminal.HandleWebSocket)
authorized.POST("/terminal/exec", c.Terminal.ExecuteShellCommand) authorized.POST("/terminal/exec", c.Terminal.ExecuteShellCommand)
authorized.GET("/terminal/cmds", c.Terminal.GetCommands)
// 设置中心模块 // 设置中心模块
settings := authorized.Group("/settings") settings := authorized.Group("/settings")
+24 -11
View File
@@ -4,25 +4,38 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/engigu/baihu-panel/cmd/reposync" "github.com/engigu/baihu-panel/cmd"
"github.com/engigu/baihu-panel/internal/bootstrap" "github.com/engigu/baihu-panel/internal/bootstrap"
"github.com/engigu/baihu-panel/internal/constant"
) )
func printHelp() {
fmt.Println("Usage: baihu <command> [arguments]")
fmt.Println("Available commands:")
for _, info := range constant.Commands {
fmt.Printf(" %-12s %s\n", info.Name, info.Description)
}
}
func main() { func main() {
if len(os.Args) < 2 { if len(os.Args) < 2 {
printHelp()
os.Exit(1)
}
commandName := os.Args[1]
if commandName == "server" {
bootstrap.New().Run() bootstrap.New().Run()
return return
} }
cmd := os.Args[1] if handler, ok := cmd.Handlers[commandName]; ok {
switch cmd { handler(os.Args[2:])
case "server": return
bootstrap.New().Run()
case "reposync":
reposync.Run(os.Args[2:])
default:
fmt.Printf("Unknown command: %s\n", cmd)
fmt.Println("Available commands: server, reposync")
os.Exit(1)
} }
fmt.Printf("Unknown command: %s\n", commandName)
printHelp()
os.Exit(1)
} }
+3
View File
@@ -256,6 +256,9 @@ export const api = {
plugins: () => request<string[]>('/mise/plugins'), plugins: () => request<string[]>('/mise/plugins'),
versions: (plugin: string) => request<string[]>(`/mise/versions?plugin=${plugin}`), versions: (plugin: string) => request<string[]>(`/mise/versions?plugin=${plugin}`),
verifyCommand: (plugin: string, version: string) => request<{ command: string }>(`/mise/verify-cmd?plugin=${plugin}&version=${version}`) verifyCommand: (plugin: string, version: string) => request<{ command: string }>(`/mise/verify-cmd?plugin=${plugin}&version=${version}`)
},
terminal: {
cmds: () => request<{ name: string, description: string }[]>('/terminal/cmds')
} }
} }
+41 -4
View File
@@ -1,10 +1,22 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, onMounted } from 'vue'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { RefreshCw } from 'lucide-vue-next' import { RefreshCw, Info } from 'lucide-vue-next'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import XTerminal from '@/components/XTerminal.vue' import XTerminal from '@/components/XTerminal.vue'
import { api } from '@/api'
const terminalRef = ref<InstanceType<typeof XTerminal> | null>(null) const terminalRef = ref<InstanceType<typeof XTerminal> | null>(null)
const cmds = ref<{ name: string, description: string }[]>([])
onMounted(async () => {
try {
const res = await api.terminal.cmds()
cmds.value = res
} catch (error) {
console.error('Failed to load terminal commands', error)
}
})
function reconnect() { function reconnect() {
terminalRef.value?.reconnect() terminalRef.value?.reconnect()
@@ -14,8 +26,33 @@ function reconnect() {
<template> <template>
<div class="flex flex-col h-[calc(100vh-120px)] sm:h-[calc(100vh-100px)]"> <div class="flex flex-col h-[calc(100vh-120px)] sm:h-[calc(100vh-100px)]">
<div class="flex items-center justify-between p-2 border border-[#3c3c3c] rounded-t-md bg-[#252526]"> <div class="flex items-center justify-between p-2 border border-[#3c3c3c] rounded-t-md bg-[#252526]">
<span class="text-xs font-medium text-gray-300">终端</span> <div class="flex items-center gap-2">
<Button variant="ghost" size="icon" class="h-6 w-6 text-gray-400 hover:text-white" @click="reconnect" title="重新连接"> <span class="text-xs font-medium text-gray-300">终端</span>
<Popover>
<PopoverTrigger as-child>
<div class="flex items-center gap-1 cursor-pointer text-gray-400 hover:text-white transition-colors"
title="查看内置命令">
<span class="text-xs">内置命令</span>
<Info class="h-3.5 w-3.5" />
</div>
</PopoverTrigger>
<PopoverContent align="start" side="bottom" :side-offset="8"
class="w-80 border-[#3c3c3c] bg-[#252526] text-gray-300">
<div class="space-y-2">
<h4 class="text-sm font-medium text-white mb-2 pb-2 border-b border-[#3c3c3c]">内置命令说明</h4>
<div v-if="cmds.length === 0" class="text-xs text-gray-500">获取中...</div>
<div v-for="cmd in cmds" :key="cmd.name"
class="flex flex-col space-y-1 text-xs border-b border-[#3c3c3c] pb-2 last:border-0 last:pb-0">
<span class="font-bold text-blue-400">baihu {{ cmd.name }}</span>
<span class="text-gray-400">{{ cmd.description }}</span>
</div>
</div>
</PopoverContent>
</Popover>
</div>
<Button variant="ghost" size="icon" class="h-6 w-6 text-gray-400 hover:text-white" @click="reconnect"
title="重新连接">
<RefreshCw class="h-3 w-3" /> <RefreshCw class="h-3 w-3" />
</Button> </Button>
</div> </div>