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) - + diff --git a/web/src/views/history/History.vue b/web/src/views/history/History.vue index b7bd6db..1705f21 100644 --- a/web/src/views/history/History.vue +++ b/web/src/views/history/History.vue @@ -8,13 +8,15 @@ import { RefreshCw, X, Search, Maximize2 } from 'lucide-vue-next' import { api, type TaskLog, type LogDetail } from '@/api' import { toast } from 'vue-sonner' import pako from 'pako' +import { useSiteSettings } from '@/composables/useSiteSettings' + +const { pageSize } = useSiteSettings() const logs = ref([]) const selectedLog = ref(null) const logDetail = ref(null) const filterKeyword = ref('') const currentPage = ref(1) -const pageSize = ref(10) const total = ref(0) let searchTimer: ReturnType | null = null @@ -150,7 +152,7 @@ onMounted(loadLogs) - + diff --git a/web/src/views/tasks/Tasks.vue b/web/src/views/tasks/Tasks.vue index e031545..daf5b82 100644 --- a/web/src/views/tasks/Tasks.vue +++ b/web/src/views/tasks/Tasks.vue @@ -9,6 +9,9 @@ import Pagination from '@/components/Pagination.vue' import { Plus, Play, Pencil, Trash2, Search } from 'lucide-vue-next' import { api, type Task } from '@/api' import { toast } from 'vue-sonner' +import { useSiteSettings } from '@/composables/useSiteSettings' + +const { pageSize } = useSiteSettings() const tasks = ref([]) const showDialog = ref(false) @@ -19,7 +22,6 @@ const deleteTaskId = ref(null) const filterName = ref('') const currentPage = ref(1) -const pageSize = ref(10) const total = ref(0) let searchTimer: ReturnType | null = null @@ -174,7 +176,7 @@ onMounted(loadTasks) - +