feat: add resetpwd cmd
This commit is contained in:
@@ -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
@@ -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,
|
||||||
|
}
|
||||||
@@ -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("--------------------------------------------------")
|
||||||
|
}
|
||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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]">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-xs font-medium text-gray-300">终端</span>
|
<span class="text-xs font-medium text-gray-300">终端</span>
|
||||||
<Button variant="ghost" size="icon" class="h-6 w-6 text-gray-400 hover:text-white" @click="reconnect" title="重新连接">
|
<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>
|
||||||
|
|||||||
Reference in New Issue
Block a user