diff --git a/configs/config.json b/configs/config.json
index 44ef8a5..3bb5178 100644
--- a/configs/config.json
+++ b/configs/config.json
@@ -14,7 +14,6 @@
"table_prefix": "baihu_"
},
"security": {
- "jwt_secret": "ql_panel_secret_key",
"password_salt": "ql_panel_salt"
},
"task": {
diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go
index 42860a4..c03fd0e 100644
--- a/internal/bootstrap/bootstrap.go
+++ b/internal/bootstrap/bootstrap.go
@@ -71,8 +71,8 @@ func (a *App) initDatabase() {
}
func (a *App) initRouter() {
- ctrls := router.RegisterControllers()
- a.Router = router.Setup(ctrls)
+ ctrls, settingsService := router.RegisterControllers()
+ a.Router = router.Setup(ctrls, settingsService)
}
func (a *App) Run() {
diff --git a/internal/constant/constant.go b/internal/constant/constant.go
index 5662ff1..6467614 100644
--- a/internal/constant/constant.go
+++ b/internal/constant/constant.go
@@ -29,31 +29,38 @@ const (
// CookieName Cookie 名称
CookieName = "BHToken"
- // TokenExpireDays Token 过期天数
- TokenExpireDays = 7
- // CookieMaxAge Cookie 有效期(秒)7天
- CookieMaxAge = 86400 * TokenExpireDays
-
- // DefaultJWTSecret 默认 JWT 密钥
- DefaultJWTSecret = "baihu-default-secret-key"
-
// DefaultTaskTimeout 默认任务超时时间(分钟)
DefaultTaskTimeout = 30
+
+ // Settings Section 常量
+ SectionSite = "site"
+ SectionSystem = "system"
+
+ // Site Settings Key 常量
+ KeyTitle = "title"
+ KeySubtitle = "subtitle"
+ KeyIcon = "icon"
+ KeyPageSize = "page_size"
+ KeyCookieDays = "cookie_days"
+
+ // System Settings Key 常量
+ KeyJWTSecret = "jwt_secret"
+ KeyInitialized = "initialized"
)
// TablePrefix 表前缀,可在运行时设置
var TablePrefix = DefaultTablePrefix
-// JWTSecret JWT 密钥,可通过配置文件设置
-var JWTSecret = DefaultJWTSecret
+// DefaultIcon 默认站点图标
+var DefaultIcon = ``
// DefaultSettings 默认系统设置
var DefaultSettings = map[string]map[string]string{
- "site": {
- "title": "白虎面板",
- "subtitle": "轻量级定时任务管理系统",
- "icon": "",
- "page_size": "10",
- "cookie_days": "7",
+ SectionSite: {
+ KeyTitle: "白虎面板",
+ KeySubtitle: "轻量级定时任务管理系统",
+ KeyIcon: DefaultIcon,
+ KeyPageSize: "10",
+ KeyCookieDays: "7",
},
}
diff --git a/internal/controllers/auth_controller.go b/internal/controllers/auth_controller.go
index 4e9ee5f..e037fc4 100644
--- a/internal/controllers/auth_controller.go
+++ b/internal/controllers/auth_controller.go
@@ -1,6 +1,9 @@
package controllers
import (
+ "strconv"
+
+ "baihu/internal/constant"
"baihu/internal/middleware"
"baihu/internal/services"
"baihu/internal/utils"
@@ -9,11 +12,12 @@ import (
)
type AuthController struct {
- userService *services.UserService
+ userService *services.UserService
+ settingsService *services.SettingsService
}
-func NewAuthController(userService *services.UserService) *AuthController {
- return &AuthController{userService: userService}
+func NewAuthController(userService *services.UserService, settingsService *services.SettingsService) *AuthController {
+ return &AuthController{userService: userService, settingsService: settingsService}
}
func (ac *AuthController) Login(c *gin.Context) {
@@ -33,15 +37,30 @@ func (ac *AuthController) Login(c *gin.Context) {
return
}
+ // 获取 cookie 过期天数
+ expireDays := 7
+ if days := ac.settingsService.Get(constant.SectionSite, constant.KeyCookieDays); days != "" {
+ if d, err := strconv.Atoi(days); err == nil && d > 0 {
+ expireDays = d
+ }
+ }
+
+ // 获取 JWT Secret
+ jwtSecret := ac.settingsService.Get(constant.SectionSystem, constant.KeyJWTSecret)
+ if jwtSecret == "" {
+ utils.ServerError(c, "系统配置错误")
+ return
+ }
+
// 生成 token
- token, err := utils.GenerateToken(user.ID, user.Username)
+ token, err := utils.GenerateToken(user.ID, user.Username, expireDays, jwtSecret)
if err != nil {
utils.ServerError(c, "登录失败")
return
}
// 设置 Cookie
- middleware.SetAuthCookie(c, token)
+ middleware.SetAuthCookie(c, token, expireDays)
utils.Success(c, gin.H{
"user": user.Username,
diff --git a/internal/controllers/settings_controller.go b/internal/controllers/settings_controller.go
index 02c08e6..8fc43a9 100644
--- a/internal/controllers/settings_controller.go
+++ b/internal/controllers/settings_controller.go
@@ -79,18 +79,18 @@ func (sc *SettingsController) CleanLogs(c *gin.Context) {
// GetSiteSettings 获取站点设置
func (sc *SettingsController) GetSiteSettings(c *gin.Context) {
- settings := sc.settingsService.GetSection("site")
+ settings := sc.settingsService.GetSection(constant.SectionSite)
utils.Success(c, settings)
}
// GetPublicSiteSettings 获取公开的站点设置(无需认证)
func (sc *SettingsController) GetPublicSiteSettings(c *gin.Context) {
- settings := sc.settingsService.GetSection("site")
+ settings := sc.settingsService.GetSection(constant.SectionSite)
// 只返回公开信息
utils.Success(c, gin.H{
- "title": settings["title"],
- "subtitle": settings["subtitle"],
- "icon": settings["icon"],
+ constant.KeyTitle: settings[constant.KeyTitle],
+ constant.KeySubtitle: settings[constant.KeySubtitle],
+ constant.KeyIcon: settings[constant.KeyIcon],
})
}
@@ -110,14 +110,14 @@ func (sc *SettingsController) UpdateSiteSettings(c *gin.Context) {
}
values := map[string]string{
- "title": req.Title,
- "subtitle": req.Subtitle,
- "icon": req.Icon,
- "page_size": req.PageSize,
- "cookie_days": req.CookieDays,
+ constant.KeyTitle: req.Title,
+ constant.KeySubtitle: req.Subtitle,
+ constant.KeyIcon: req.Icon,
+ constant.KeyPageSize: req.PageSize,
+ constant.KeyCookieDays: req.CookieDays,
}
- if err := sc.settingsService.SetSection("site", values); err != nil {
+ if err := sc.settingsService.SetSection(constant.SectionSite, values); err != nil {
utils.ServerError(c, "保存失败")
return
}
diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go
index b573768..9a9a72c 100644
--- a/internal/middleware/auth.go
+++ b/internal/middleware/auth.go
@@ -2,13 +2,14 @@ package middleware
import (
"baihu/internal/constant"
+ "baihu/internal/services"
"baihu/internal/utils"
"github.com/gin-gonic/gin"
)
// AuthRequired 认证中间件
-func AuthRequired() gin.HandlerFunc {
+func AuthRequired(settingsService *services.SettingsService) gin.HandlerFunc {
return func(c *gin.Context) {
token, err := c.Cookie(constant.CookieName)
if err != nil || token == "" {
@@ -17,8 +18,16 @@ func AuthRequired() gin.HandlerFunc {
return
}
+ // 获取 JWT Secret
+ jwtSecret := settingsService.Get(constant.SectionSystem, constant.KeyJWTSecret)
+ if jwtSecret == "" {
+ utils.Unauthorized(c, "系统配置错误")
+ c.Abort()
+ return
+ }
+
// 验证 token
- userID, username, err := utils.ParseToken(token)
+ userID, username, err := utils.ParseToken(token, jwtSecret)
if err != nil {
utils.Unauthorized(c, "登录已过期,请重新登录")
c.Abort()
@@ -32,9 +41,10 @@ func AuthRequired() gin.HandlerFunc {
}
}
-// SetAuthCookie 设置认证 Cookie
-func SetAuthCookie(c *gin.Context, token string) {
- c.SetCookie(constant.CookieName, token, constant.CookieMaxAge, "/", "", false, true)
+// SetAuthCookie 设置认证 Cookie,expireDays 为过期天数
+func SetAuthCookie(c *gin.Context, token string, expireDays int) {
+ maxAge := 86400 * expireDays
+ c.SetCookie(constant.CookieName, token, maxAge, "/", "", false, true)
}
// ClearAuthCookie 清除认证 Cookie
diff --git a/internal/router/register.go b/internal/router/register.go
index aacbb34..859ae50 100644
--- a/internal/router/register.go
+++ b/internal/router/register.go
@@ -8,18 +8,18 @@ import (
var cronService *services.CronService
-func RegisterControllers() *Controllers {
+func RegisterControllers() (*Controllers, *services.SettingsService) {
// Initialize services
+ settingsService := services.NewSettingsService()
+
+ // 执行系统初始化(返回 userService)
+ initService := services.NewInitService(settingsService)
+ userService := initService.Initialize()
+
taskService := services.NewTaskService()
- userService := services.NewUserService()
envService := services.NewEnvService()
scriptService := services.NewScriptService()
executorService := services.NewExecutorService(taskService)
- settingsService := services.NewSettingsService()
-
- // 执行系统初始化
- initService := services.NewInitService(settingsService, userService)
- initService.Initialize()
// Initialize cron service
cronService = services.NewCronService(taskService, executorService)
@@ -28,7 +28,7 @@ func RegisterControllers() *Controllers {
// Initialize and return controllers
return &Controllers{
Task: controllers.NewTaskController(taskService, cronService),
- Auth: controllers.NewAuthController(userService),
+ Auth: controllers.NewAuthController(userService, settingsService),
Env: controllers.NewEnvController(envService),
Script: controllers.NewScriptController(scriptService),
Executor: controllers.NewExecutorController(executorService),
@@ -37,7 +37,7 @@ func RegisterControllers() *Controllers {
Log: controllers.NewLogController(),
Terminal: controllers.NewTerminalController(),
Settings: controllers.NewSettingsController(userService),
- }
+ }, settingsService
}
// StopCron stops the cron service gracefully
diff --git a/internal/router/router.go b/internal/router/router.go
index 4e0732f..26437d6 100644
--- a/internal/router/router.go
+++ b/internal/router/router.go
@@ -6,6 +6,7 @@ import (
"baihu/internal/controllers"
"baihu/internal/middleware"
+ "baihu/internal/services"
"baihu/internal/static"
"github.com/gin-gonic/gin"
@@ -40,7 +41,7 @@ func cacheControl(value string) gin.HandlerFunc {
}
}
-func Setup(c *Controllers) *gin.Engine {
+func Setup(c *Controllers, settingsService *services.SettingsService) *gin.Engine {
gin.SetMode(gin.ReleaseMode)
router := gin.New()
router.Use(middleware.GinLogger(), middleware.GinRecovery())
@@ -94,7 +95,7 @@ func Setup(c *Controllers) *gin.Engine {
// 需要认证的路由
authorized := api.Group("")
- authorized.Use(middleware.AuthRequired())
+ authorized.Use(middleware.AuthRequired(settingsService))
{
// 获取当前用户
authorized.GET("/auth/me", c.Auth.GetCurrentUser)
diff --git a/internal/services/config_service.go b/internal/services/config_service.go
index 84619c7..0fa4511 100644
--- a/internal/services/config_service.go
+++ b/internal/services/config_service.go
@@ -58,11 +58,6 @@ func LoadConfig(path string) (*AppConfig, error) {
constant.TablePrefix = Config.Database.TablePrefix
}
- // 设置 JWT 密钥
- if Config.Security.JWTSecret != "" {
- constant.JWTSecret = Config.Security.JWTSecret
- }
-
return Config, nil
}
diff --git a/internal/services/init_service.go b/internal/services/init_service.go
index 89f5fa6..e890a36 100644
--- a/internal/services/init_service.go
+++ b/internal/services/init_service.go
@@ -1,62 +1,78 @@
package services
import (
- "baihu/internal/logger"
-)
+ "crypto/rand"
+ "encoding/hex"
-const (
- InitSection = "system"
- InitKey = "initialized"
- InitValue = "true"
+ "baihu/internal/constant"
+ "baihu/internal/logger"
)
type InitService struct {
settingsService *SettingsService
- userService *UserService
}
-func NewInitService(settingsService *SettingsService, userService *UserService) *InitService {
+func NewInitService(settingsService *SettingsService) *InitService {
return &InitService{
settingsService: settingsService,
- userService: userService,
}
}
-// Initialize 执行初始化,如果已初始化则跳过
-func (s *InitService) Initialize() {
- //if s.IsInitialized() {
- // logger.Info("系统已初始化,跳过")
- // return
- //}
-
+// Initialize 执行系统初始化,返回 UserService
+func (s *InitService) Initialize() *UserService {
logger.Info("开始初始化系统...")
- // 创建管理员账号
- s.createAdminUser()
-
- // 初始化默认设置(每次启动都检查)
+ // 初始化默认设置
if err := s.settingsService.InitSettings(); err != nil {
logger.Warnf("初始化设置失败: %v", err)
}
- //// 标记为已初始化
- //s.settingsService.Set(InitSection, InitKey, InitValue)
- //logger.Info("系统初始化完成")
+ // 初始化 JWT Secret(也用作密码 salt,必须在创建 UserService 之前)
+ s.initJWTSecret()
+
+ // 创建 UserService(依赖 settingsService 获取 salt)
+ userService := NewUserService(s.settingsService)
+ // 创建管理员账号
+ s.initializeAdmin(userService)
+
+ return userService
}
-// IsInitialized 检查是否已初始化
-func (s *InitService) IsInitialized() bool {
- return s.settingsService.Get(InitSection, InitKey) == InitValue
-}
-
-// createAdminUser 创建管理员账号
-func (s *InitService) createAdminUser() {
- existingUser := s.userService.GetUserByUsername("admin")
+// initializeAdmin 创建管理员账号
+func (s *InitService) initializeAdmin(userService *UserService) {
+ existingUser := userService.GetUserByUsername("admin")
if existingUser != nil {
logger.Info("管理员账号已存在,跳过创建")
return
}
- s.userService.CreateUser("admin", "123456", "admin@local", "admin")
+ userService.CreateUser("admin", "123456", "admin@local", "admin")
logger.Info("管理员账号创建成功: admin / 123456")
}
+
+// IsInitialized 检查是否已初始化
+func (s *InitService) IsInitialized() bool {
+ return s.settingsService.Get(constant.SectionSystem, constant.KeyInitialized) == "true"
+}
+
+// initJWTSecret 初始化 JWT Secret,如果不存在则生成随机值
+func (s *InitService) initJWTSecret() {
+ existing := s.settingsService.Get(constant.SectionSystem, constant.KeyJWTSecret)
+ if existing != "" {
+ return
+ }
+
+ // 生成 32 字节随机密钥
+ bytes := make([]byte, 32)
+ if _, err := rand.Read(bytes); err != nil {
+ logger.Warnf("生成 JWT Secret 失败: %v", err)
+ return
+ }
+
+ secret := hex.EncodeToString(bytes)
+ if err := s.settingsService.Set(constant.SectionSystem, constant.KeyJWTSecret, secret); err != nil {
+ logger.Warnf("保存 JWT Secret 失败: %v", err)
+ return
+ }
+ logger.Info("JWT Secret 已生成")
+}
diff --git a/internal/services/user_service.go b/internal/services/user_service.go
index 0e9932e..296de28 100644
--- a/internal/services/user_service.go
+++ b/internal/services/user_service.go
@@ -4,21 +4,22 @@ import (
"crypto/sha256"
"encoding/hex"
+ "baihu/internal/constant"
"baihu/internal/database"
"baihu/internal/models"
)
-type UserService struct{}
+type UserService struct {
+ settingsService *SettingsService
+}
-func NewUserService() *UserService {
- return &UserService{}
+func NewUserService(settingsService *SettingsService) *UserService {
+ return &UserService{settingsService: settingsService}
}
func (us *UserService) hashPassword(password string) string {
- salt := ""
- if Config != nil {
- salt = Config.Security.PasswordSalt
- }
+ // 使用 JWT Secret 作为密码 salt
+ salt := us.settingsService.Get(constant.SectionSystem, constant.KeyJWTSecret)
hash := sha256.Sum256([]byte(password + salt))
return hex.EncodeToString(hash[:])
}
diff --git a/internal/utils/token.go b/internal/utils/token.go
index 166f889..d125b65 100644
--- a/internal/utils/token.go
+++ b/internal/utils/token.go
@@ -4,8 +4,6 @@ import (
"errors"
"time"
- "baihu/internal/constant"
-
"github.com/golang-jwt/jwt/v5"
)
@@ -16,24 +14,24 @@ type Claims struct {
}
// GenerateToken 生成 JWT token
-func GenerateToken(userID uint, username string) (string, error) {
+func GenerateToken(userID uint, username string, expireDays int, secret string) (string, error) {
claims := Claims{
UserID: userID,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
- ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(constant.TokenExpireDays) * 24 * time.Hour)),
+ ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expireDays) * 24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
- return token.SignedString([]byte(constant.JWTSecret))
+ return token.SignedString([]byte(secret))
}
// ParseToken 解析 JWT token
-func ParseToken(tokenString string) (uint, string, error) {
+func ParseToken(tokenString string, secret string) (uint, string, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (any, error) {
- return []byte(constant.JWTSecret), nil
+ return []byte(secret), nil
})
if err != nil {
diff --git a/web/src/components/Pagination.vue b/web/src/components/Pagination.vue
index a44f8c1..78c9137 100644
--- a/web/src/components/Pagination.vue
+++ b/web/src/components/Pagination.vue
@@ -2,18 +2,20 @@
import { computed } from 'vue'
import { Button } from '@/components/ui/button'
import { ChevronLeft, ChevronRight } from 'lucide-vue-next'
+import { useSiteSettings } from '@/composables/useSiteSettings'
const props = defineProps<{
total: number
page: number
- pageSize: number
}>()
const emit = defineEmits<{
'update:page': [page: number]
}>()
-const totalPages = computed(() => Math.ceil(props.total / props.pageSize) || 1)
+const { pageSize } = useSiteSettings()
+
+const totalPages = computed(() => Math.ceil(props.total / pageSize.value) || 1)
function prevPage() {
if (props.page > 1) {
diff --git a/web/src/composables/useSiteSettings.ts b/web/src/composables/useSiteSettings.ts
index 714e52a..2a67010 100644
--- a/web/src/composables/useSiteSettings.ts
+++ b/web/src/composables/useSiteSettings.ts
@@ -1,4 +1,4 @@
-import { ref } from 'vue'
+import { ref, computed } from 'vue'
import { api, type SiteSettings } from '@/api'
const siteSettings = ref({
@@ -27,6 +27,8 @@ function updateFavicon(svgContent: string) {
link.href = url
}
+const pageSize = computed(() => parseInt(siteSettings.value.page_size) || 10)
+
export function useSiteSettings() {
async function loadSettings() {
if (loaded) return
@@ -48,6 +50,7 @@ export function useSiteSettings() {
return {
siteSettings,
+ pageSize,
loadSettings,
refreshSettings,
updateFavicon
diff --git a/web/src/views/environments/Environments.vue b/web/src/views/environments/Environments.vue
index 59e73e7..c0db58e 100644
--- a/web/src/views/environments/Environments.vue
+++ b/web/src/views/environments/Environments.vue
@@ -10,6 +10,9 @@ import Pagination from '@/components/Pagination.vue'
import { Plus, Pencil, Trash2, Eye, EyeOff, Search } from 'lucide-vue-next'
import { api, type EnvVar } from '@/api'
import { toast } from 'vue-sonner'
+import { useSiteSettings } from '@/composables/useSiteSettings'
+
+const { pageSize } = useSiteSettings()
const envVars = ref([])
const showDialog = ref(false)
@@ -21,7 +24,6 @@ const deleteEnvId = ref(null)
const filterName = ref('')
const currentPage = ref(1)
-const pageSize = ref(10)
const total = ref(0)
let searchTimer: ReturnType | null = null
@@ -155,7 +157,7 @@ onMounted(loadEnvVars)
-
+