diff --git a/.gitignore b/.gitignore index 8d47f36..b122a4f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ baihu data/ # logs/ # scripts/ +configs/config.ini # IDE .idea/ diff --git a/Dockerfile b/Dockerfile index 3d4cccf..586d5c1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,7 +50,7 @@ RUN sed -i 's@deb.debian.org@mirrors.tuna.tsinghua.edu.cn@g' /etc/apt/sources.li && echo "${TZ}" > /etc/timezone \ && ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime \ && apt update \ - && apt install -y tzdata git gcc curl wget vim ca-certificates procps htop \ + && apt install -y tzdata git gcc curl wget vim ca-certificates procps htop e2fsprogs \ && rm -rf /var/lib/apt/lists/* \ && curl -sSL https://gh-proxy.com/https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh -o /tmp/miniforge.sh \ && bash /tmp/miniforge.sh -b -p ${CONDA_DIR} \ @@ -68,11 +68,12 @@ COPY --from=backend-builder /app/baihu . # Copy config files COPY --from=backend-builder /app/configs ./configs -# Create directories -RUN mkdir -p ./data ./logs ./scripts +# Copy entrypoint script +COPY docker-entrypoint.sh . +RUN chmod +x docker-entrypoint.sh # Expose port EXPOSE 8052 -# Run -CMD ["./baihu"] +# Run with entrypoint +CMD ["./docker-entrypoint.sh"] diff --git a/README.md b/README.md index d928385..3ad2e22 100644 --- a/README.md +++ b/README.md @@ -162,31 +162,32 @@ make build-all │ ├── ql.db # SQLite 数据库 │ └── scripts/ # 脚本文件存储 └── configs/ - └── config.json # 配置文件(自动创建) + └── config.ini # 配置文件(自动创建) ``` --- ## ⚙️ 配置说明 -配置文件路径:`configs/config.json` +配置文件路径:`configs/config.ini` -```json -{ - "server": { - "port": 8052, - "host": "0.0.0.0" - }, - "database": { - "type": "sqlite", - "path": "./data/ql.db", - "table_prefix": "baihu_" - }, - "task": { - "default_timeout": 3600, - "log_retention_days": 30 - } -} +```ini +[server] +port = 8052 +host = 0.0.0.0 + +[database] +type = sqlite +host = localhost +port = 3306 +user = root +password = +dbname = ql_panel +table_prefix = baihu_ + +[task] +default_timeout = 3600 +log_retention_days = 30 ``` ### 配置项说明 @@ -196,7 +197,6 @@ make build-all | `server.port` | 服务端口 | 8052 | | `server.host` | 监听地址 | 0.0.0.0 | | `database.type` | 数据库类型 | sqlite | -| `database.path` | 数据库路径 | ./data/ql.db | | `database.table_prefix` | 表前缀 | baihu_ | | `task.default_timeout` | 任务默认超时(秒) | 3600 | | `task.log_retention_days` | 日志保留天数 | 30 | diff --git a/configs/config.example.ini b/configs/config.example.ini new file mode 100644 index 0000000..76e08a9 --- /dev/null +++ b/configs/config.example.ini @@ -0,0 +1,16 @@ +[server] +port = 8052 +host = 0.0.0.0 + +[database] +type = sqlite +host = localhost +port = 3306 +user = root +password = +dbname = ql_panel +table_prefix = baihu_ + +[task] +default_timeout = 3600 +log_retention_days = 30 diff --git a/configs/config.json b/configs/config.json deleted file mode 100644 index 3bb5178..0000000 --- a/configs/config.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "server": { - "port": 8052, - "host": "0.0.0.0" - }, - "database": { - "type": "sqlite", - "host": "localhost", - "port": 3306, - "user": "root", - "password": "", - "dbname": "ql_panel", - "path": "./data/ql.db", - "table_prefix": "baihu_" - }, - "security": { - "password_salt": "ql_panel_salt" - }, - "task": { - "default_timeout": 3600, - "log_retention_days": 30 - } -} \ No newline at end of file diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..2d5b2ad --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# 创建必要的目录 +mkdir -p /app/data /app/data/scripts /app/configs + +# 如果数据库文件存在,设置不可变属性防止误删 +if [ -f /app/data/ql.db ]; then + chattr +i /app/data/ql.db 2>/dev/null || true +fi + +# 如果 configs 目录存在,设置不可变属性 +if [ -d /app/configs ]; then + chattr +i /app/configs 2>/dev/null || true +fi + +# 如果 scripts 目录存在,设置不可变属性 +if [ -d /app/data/scripts ]; then + chattr +i /app/data/scripts 2>/dev/null || true +fi + +# 启动应用 +exec ./baihu diff --git a/go.mod b/go.mod index 5a8d33d..37ac774 100644 --- a/go.mod +++ b/go.mod @@ -64,6 +64,7 @@ require ( golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.39.0 // indirect google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/libc v1.22.5 // indirect modernc.org/mathutil v1.5.0 // indirect diff --git a/go.sum b/go.sum index a766e11..660d760 100644 --- a/go.sum +++ b/go.sum @@ -152,6 +152,8 @@ google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index c03fd0e..8c4fb4f 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -62,17 +62,11 @@ func (a *App) initDatabase() { if err := database.Migrate(); err != nil { logger.Fatalf("Failed to migrate database: %v", err) } - - // 初始化默认设置 - settingsService := services.NewSettingsService() - if err := settingsService.InitSettings(); err != nil { - logger.Warnf("Failed to init settings: %v", err) - } } func (a *App) initRouter() { - ctrls, settingsService := router.RegisterControllers() - a.Router = router.Setup(ctrls, settingsService) + ctrls := router.RegisterControllers() + a.Router = router.Setup(ctrls) } func (a *App) Run() { diff --git a/internal/cache/site_cache.go b/internal/cache/site_cache.go new file mode 100644 index 0000000..874af8b --- /dev/null +++ b/internal/cache/site_cache.go @@ -0,0 +1,94 @@ +package cache + +import ( + "sync" + + "baihu/internal/constant" + "baihu/internal/database" + "baihu/internal/models" +) + +// siteCache 站点设置内存缓存 +var ( + siteCache = make(map[string]string) + siteCacheMu sync.RWMutex + siteCacheInit bool +) + +// LoadSiteCache 从数据库加载站点设置到缓存 +func LoadSiteCache() { + siteCacheMu.Lock() + defer siteCacheMu.Unlock() + + // 先填充默认值 + if defaults, ok := constant.DefaultSettings[constant.SectionSite]; ok { + for k, v := range defaults { + siteCache[k] = v + } + } + + // 从数据库加载覆盖 + var settings []models.Setting + database.DB.Where("section = ?", constant.SectionSite).Find(&settings) + for _, setting := range settings { + siteCache[setting.Key] = setting.Value + } + siteCacheInit = true +} + +// ensureSiteCache 确保站点缓存已初始化 +func ensureSiteCache() { + siteCacheMu.RLock() + init := siteCacheInit + siteCacheMu.RUnlock() + + if !init { + LoadSiteCache() + } +} + +// GetSiteCache 从缓存获取站点设置 +func GetSiteCache(key string) string { + ensureSiteCache() + + siteCacheMu.RLock() + defer siteCacheMu.RUnlock() + + if val, ok := siteCache[key]; ok { + return val + } + if def, ok := constant.DefaultSettings[constant.SectionSite][key]; ok { + return def + } + return "" +} + +// SetSiteCache 更新缓存中的站点设置 +func SetSiteCache(key, value string) { + siteCacheMu.Lock() + siteCache[key] = value + siteCacheMu.Unlock() +} + +// GetSiteCacheAll 获取整个站点设置缓存 +func GetSiteCacheAll() map[string]string { + ensureSiteCache() + + siteCacheMu.RLock() + defer siteCacheMu.RUnlock() + + result := make(map[string]string) + for k, v := range siteCache { + result[k] = v + } + return result +} + +// SetSiteCacheBatch 批量更新缓存 +func SetSiteCacheBatch(values map[string]string) { + siteCacheMu.Lock() + for k, v := range values { + siteCache[k] = v + } + siteCacheMu.Unlock() +} diff --git a/internal/constant/constant.go b/internal/constant/constant.go index 6467614..459f464 100644 --- a/internal/constant/constant.go +++ b/internal/constant/constant.go @@ -3,11 +3,14 @@ package constant const ( // ConfigPath 配置文件路径 - ConfigPath = "configs/config.json" + ConfigPath = "configs/config.ini" // DataDir 数据目录 DataDir = "./data" + // DefaultDBPath 默认数据库路径 + DefaultDBPath = "./data/ql.db" + // WebDistDir 前端构建目录 WebDistDir = "./web/dist" @@ -17,15 +20,9 @@ const ( // AdminRole 管理员角色 AdminRole = "admin" - // DefaultTablePrefix 默认表前缀 - DefaultTablePrefix = "baihu_" - // ScriptsWorkDir 脚本工作目录 ScriptsWorkDir = "./data/scripts" - // DefaultPageSize 默认分页大小 - DefaultPageSize = 10 - // CookieName Cookie 名称 CookieName = "BHToken" @@ -44,12 +41,14 @@ const ( KeyCookieDays = "cookie_days" // System Settings Key 常量 - KeyJWTSecret = "jwt_secret" KeyInitialized = "initialized" ) -// TablePrefix 表前缀,可在运行时设置 -var TablePrefix = DefaultTablePrefix +// TablePrefix 表前缀,从配置文件读取 +var TablePrefix string + +// Secret JWT和密码salt密钥,从配置文件读取 +var Secret string // DefaultIcon 默认站点图标 var DefaultIcon = `` diff --git a/internal/controllers/auth_controller.go b/internal/controllers/auth_controller.go index e037fc4..08783d0 100644 --- a/internal/controllers/auth_controller.go +++ b/internal/controllers/auth_controller.go @@ -14,10 +14,15 @@ import ( type AuthController struct { userService *services.UserService settingsService *services.SettingsService + loginLogService *services.LoginLogService } -func NewAuthController(userService *services.UserService, settingsService *services.SettingsService) *AuthController { - return &AuthController{userService: userService, settingsService: settingsService} +func NewAuthController(userService *services.UserService, settingsService *services.SettingsService, loginLogService *services.LoginLogService) *AuthController { + return &AuthController{ + userService: userService, + settingsService: settingsService, + loginLogService: loginLogService, + } } func (ac *AuthController) Login(c *gin.Context) { @@ -26,6 +31,9 @@ func (ac *AuthController) Login(c *gin.Context) { Password string `json:"password" binding:"required"` } + ip := c.ClientIP() + userAgent := c.GetHeader("User-Agent") + if err := c.ShouldBindJSON(&req); err != nil { utils.BadRequest(c, err.Error()) return @@ -33,6 +41,8 @@ func (ac *AuthController) Login(c *gin.Context) { user := ac.userService.GetUserByUsername(req.Username) if user == nil || !ac.userService.ValidatePassword(user, req.Password) { + // 记录登录失败日志 + ac.loginLogService.Create(req.Username, ip, userAgent, "failed", "用户名或密码错误") utils.Unauthorized(c, "用户名或密码错误") return } @@ -45,16 +55,10 @@ func (ac *AuthController) Login(c *gin.Context) { } } - // 获取 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, expireDays, jwtSecret) + token, err := utils.GenerateToken(user.ID, user.Username, expireDays, constant.Secret) if err != nil { + ac.loginLogService.Create(req.Username, ip, userAgent, "failed", "Token生成失败") utils.ServerError(c, "登录失败") return } @@ -62,6 +66,9 @@ func (ac *AuthController) Login(c *gin.Context) { // 设置 Cookie middleware.SetAuthCookie(c, token, expireDays) + // 记录登录成功日志 + ac.loginLogService.Create(req.Username, ip, userAgent, "success", "登录成功") + utils.Success(c, gin.H{ "user": user.Username, }) diff --git a/internal/controllers/settings_controller.go b/internal/controllers/settings_controller.go index 8fc43a9..ef25be0 100644 --- a/internal/controllers/settings_controller.go +++ b/internal/controllers/settings_controller.go @@ -1,6 +1,8 @@ package controllers import ( + "strconv" + "baihu/internal/constant" "baihu/internal/database" "baihu/internal/models" @@ -17,12 +19,14 @@ import ( type SettingsController struct { userService *services.UserService settingsService *services.SettingsService + loginLogService *services.LoginLogService } -func NewSettingsController(userService *services.UserService) *SettingsController { +func NewSettingsController(userService *services.UserService, loginLogService *services.LoginLogService) *SettingsController { return &SettingsController{ userService: userService, settingsService: services.NewSettingsService(), + loginLogService: loginLogService, } } @@ -186,3 +190,31 @@ func formatDuration(d time.Duration) string { } return fmt.Sprintf("%d秒", seconds) } + + +// GetLoginLogs 获取登录日志 +func (sc *SettingsController) GetLoginLogs(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10")) + username := c.Query("username") + + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 100 { + pageSize = 10 + } + + logs, total, err := sc.loginLogService.List(page, pageSize, username) + if err != nil { + utils.ServerError(c, "获取登录日志失败") + return + } + + utils.Success(c, gin.H{ + "data": logs, + "total": total, + "page": page, + "page_size": pageSize, + }) +} diff --git a/internal/database/migrate.go b/internal/database/migrate.go index ab47ad5..08f5954 100644 --- a/internal/database/migrate.go +++ b/internal/database/migrate.go @@ -12,5 +12,6 @@ func Migrate() error { &models.Script{}, &models.EnvironmentVariable{}, &models.Setting{}, + &models.LoginLog{}, ) } diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index 9a9a72c..79ebea2 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -2,14 +2,13 @@ package middleware import ( "baihu/internal/constant" - "baihu/internal/services" "baihu/internal/utils" "github.com/gin-gonic/gin" ) // AuthRequired 认证中间件 -func AuthRequired(settingsService *services.SettingsService) gin.HandlerFunc { +func AuthRequired() gin.HandlerFunc { return func(c *gin.Context) { token, err := c.Cookie(constant.CookieName) if err != nil || token == "" { @@ -18,16 +17,8 @@ func AuthRequired(settingsService *services.SettingsService) 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, jwtSecret) + userID, username, err := utils.ParseToken(token, constant.Secret) if err != nil { utils.Unauthorized(c, "登录已过期,请重新登录") c.Abort() diff --git a/internal/models/login_log.go b/internal/models/login_log.go new file mode 100644 index 0000000..864afc8 --- /dev/null +++ b/internal/models/login_log.go @@ -0,0 +1,20 @@ +package models + +import ( + "baihu/internal/constant" +) + +// LoginLog 登录日志 +type LoginLog struct { + ID uint `json:"id" gorm:"primaryKey"` + Username string `json:"username" gorm:"size:100;index;not null"` + IP string `json:"ip" gorm:"size:50"` + UserAgent string `json:"user_agent" gorm:"size:500"` + Status string `json:"status" gorm:"size:20"` // success, failed + Message string `json:"message" gorm:"size:255"` + CreatedAt LocalTime `json:"created_at" gorm:"index"` +} + +func (LoginLog) TableName() string { + return constant.TablePrefix + "login_logs" +} diff --git a/internal/router/register.go b/internal/router/register.go index 859ae50..f8b4ea3 100644 --- a/internal/router/register.go +++ b/internal/router/register.go @@ -8,9 +8,10 @@ import ( var cronService *services.CronService -func RegisterControllers() (*Controllers, *services.SettingsService) { +func RegisterControllers() *Controllers { // Initialize services settingsService := services.NewSettingsService() + loginLogService := services.NewLoginLogService() // 执行系统初始化(返回 userService) initService := services.NewInitService(settingsService) @@ -28,7 +29,7 @@ func RegisterControllers() (*Controllers, *services.SettingsService) { // Initialize and return controllers return &Controllers{ Task: controllers.NewTaskController(taskService, cronService), - Auth: controllers.NewAuthController(userService, settingsService), + Auth: controllers.NewAuthController(userService, settingsService, loginLogService), Env: controllers.NewEnvController(envService), Script: controllers.NewScriptController(scriptService), Executor: controllers.NewExecutorController(executorService), @@ -36,8 +37,8 @@ func RegisterControllers() (*Controllers, *services.SettingsService) { Dashboard: controllers.NewDashboardController(cronService, executorService), Log: controllers.NewLogController(), Terminal: controllers.NewTerminalController(), - Settings: controllers.NewSettingsController(userService), - }, settingsService + Settings: controllers.NewSettingsController(userService, loginLogService), + } } // StopCron stops the cron service gracefully diff --git a/internal/router/router.go b/internal/router/router.go index 26437d6..c5b89f9 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -6,7 +6,6 @@ import ( "baihu/internal/controllers" "baihu/internal/middleware" - "baihu/internal/services" "baihu/internal/static" "github.com/gin-gonic/gin" @@ -41,7 +40,7 @@ func cacheControl(value string) gin.HandlerFunc { } } -func Setup(c *Controllers, settingsService *services.SettingsService) *gin.Engine { +func Setup(c *Controllers) *gin.Engine { gin.SetMode(gin.ReleaseMode) router := gin.New() router.Use(middleware.GinLogger(), middleware.GinRecovery()) @@ -95,7 +94,7 @@ func Setup(c *Controllers, settingsService *services.SettingsService) *gin.Engin // 需要认证的路由 authorized := api.Group("") - authorized.Use(middleware.AuthRequired(settingsService)) + authorized.Use(middleware.AuthRequired()) { // 获取当前用户 authorized.GET("/auth/me", c.Auth.GetCurrentUser) @@ -174,6 +173,7 @@ func Setup(c *Controllers, settingsService *services.SettingsService) *gin.Engin settings.GET("/site", c.Settings.GetSiteSettings) settings.PUT("/site", c.Settings.UpdateSiteSettings) settings.GET("/about", c.Settings.GetAbout) + settings.GET("/login-logs", c.Settings.GetLoginLogs) } } } diff --git a/internal/services/config_service.go b/internal/services/config_service.go index 0fa4511..313f8b0 100644 --- a/internal/services/config_service.go +++ b/internal/services/config_service.go @@ -2,62 +2,66 @@ package services import ( "baihu/internal/constant" - "encoding/json" - "os" + + "gopkg.in/ini.v1" ) type ServerConfig struct { - Port int `json:"port"` - Host string `json:"host"` - SiteName string `json:"site_name"` + Port int `ini:"port"` + Host string `ini:"host"` } type DatabaseConfig struct { - Type string `json:"type"` - Host string `json:"host"` - Port int `json:"port"` - User string `json:"user"` - Password string `json:"password"` - DBName string `json:"dbname"` - Path string `json:"path"` - TablePrefix string `json:"table_prefix"` + Type string `ini:"type"` + Host string `ini:"host"` + Port int `ini:"port"` + User string `ini:"user"` + Password string `ini:"password"` + DBName string `ini:"dbname"` + Path string `ini:"path"` + TablePrefix string `ini:"table_prefix"` } type SecurityConfig struct { - JWTSecret string `json:"jwt_secret"` - PasswordSalt string `json:"password_salt"` + Secret string `ini:"secret"` } type TaskConfig struct { - DefaultTimeout int `json:"default_timeout"` - LogRetentionDays int `json:"log_retention_days"` + DefaultTimeout int `ini:"default_timeout"` + LogRetentionDays int `ini:"log_retention_days"` } type AppConfig struct { - Server ServerConfig `json:"server"` - Database DatabaseConfig `json:"database"` - Security SecurityConfig `json:"security"` - Task TaskConfig `json:"task"` + Server ServerConfig `ini:"server"` + Database DatabaseConfig `ini:"database"` + Security SecurityConfig `ini:"security"` + Task TaskConfig `ini:"task"` } var Config *AppConfig func LoadConfig(path string) (*AppConfig, error) { - data, err := os.ReadFile(path) + cfg, err := ini.Load(path) if err != nil { return nil, err } Config = &AppConfig{} - if err := json.Unmarshal(data, Config); err != nil { + if err := cfg.MapTo(Config); err != nil { return nil, err } - // 设置表前缀到 constant 包 - if Config.Database.TablePrefix != "" { - constant.TablePrefix = Config.Database.TablePrefix + // 设置默认数据库路径 + if Config.Database.Path == "" { + Config.Database.Path = constant.DefaultDBPath } + // 设置表前缀到 constant 包 + constant.TablePrefix = Config.Database.TablePrefix + + // 设置 Secret 到 constant 包 + constant.Secret = Config.Security.Secret + return Config, nil } diff --git a/internal/services/init_service.go b/internal/services/init_service.go index e890a36..8fb8c95 100644 --- a/internal/services/init_service.go +++ b/internal/services/init_service.go @@ -1,10 +1,6 @@ package services import ( - "crypto/rand" - "encoding/hex" - - "baihu/internal/constant" "baihu/internal/logger" ) @@ -27,11 +23,9 @@ func (s *InitService) Initialize() *UserService { logger.Warnf("初始化设置失败: %v", err) } - // 初始化 JWT Secret(也用作密码 salt,必须在创建 UserService 之前) - s.initJWTSecret() + // 创建 UserService + userService := NewUserService() - // 创建 UserService(依赖 settingsService 获取 salt) - userService := NewUserService(s.settingsService) // 创建管理员账号 s.initializeAdmin(userService) @@ -49,30 +43,3 @@ func (s *InitService) initializeAdmin(userService *UserService) { 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/login_log_service.go b/internal/services/login_log_service.go new file mode 100644 index 0000000..6251c6e --- /dev/null +++ b/internal/services/login_log_service.go @@ -0,0 +1,52 @@ +package services + +import ( + "baihu/internal/database" + "baihu/internal/models" +) + +type LoginLogService struct{} + +func NewLoginLogService() *LoginLogService { + return &LoginLogService{} +} + +// Create 创建登录日志 +func (s *LoginLogService) Create(username, ip, userAgent, status, message string) error { + log := &models.LoginLog{ + Username: username, + IP: ip, + UserAgent: userAgent, + Status: status, + Message: message, + } + return database.DB.Create(log).Error +} + +// List 获取登录日志列表 +func (s *LoginLogService) List(page, pageSize int, username string) ([]models.LoginLog, int64, error) { + var logs []models.LoginLog + var total int64 + + query := database.DB.Model(&models.LoginLog{}) + if username != "" { + query = query.Where("username LIKE ?", "%"+username+"%") + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + offset := (page - 1) * pageSize + if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&logs).Error; err != nil { + return nil, 0, err + } + + return logs, total, nil +} + +// CleanOldLogs 清理指定天数前的日志 +func (s *LoginLogService) CleanOldLogs(days int) (int64, error) { + result := database.DB.Exec("DELETE FROM "+models.LoginLog{}.TableName()+" WHERE created_at < datetime('now', ?)", "-"+string(rune(days))+" days") + return result.RowsAffected, result.Error +} diff --git a/internal/services/settings_service.go b/internal/services/settings_service.go index 6f4fa0c..3dbc74f 100644 --- a/internal/services/settings_service.go +++ b/internal/services/settings_service.go @@ -1,6 +1,7 @@ package services import ( + "baihu/internal/cache" "baihu/internal/constant" "baihu/internal/database" "baihu/internal/models" @@ -17,26 +18,23 @@ func (s *SettingsService) InitSettings() error { for section, keys := range constant.DefaultSettings { for key, value := range keys { var count int64 - database.DB.Model(&models.Setting{}). - Where("section = ? AND key = ?", section, key). - Count(&count) + database.DB.Model(&models.Setting{}).Where("section = ? AND key = ?", section, key).Count(&count) if count == 0 { - setting := &models.Setting{ - Section: section, - Key: key, - Value: value, - } - if err := database.DB.Create(setting).Error; err != nil { + if err := database.DB.Create(&models.Setting{Section: section, Key: key, Value: value}).Error; err != nil { return err } } } } + cache.LoadSiteCache() return nil } // Get 获取单个设置 func (s *SettingsService) Get(section, key string) string { + if section == constant.SectionSite { + return cache.GetSiteCache(key) + } var setting models.Setting if err := database.DB.Where("section = ? AND key = ?", section, key).First(&setting).Error; err != nil { if def, ok := constant.DefaultSettings[section][key]; ok { @@ -50,29 +48,27 @@ func (s *SettingsService) Get(section, key string) string { // Set 设置单个值 func (s *SettingsService) Set(section, key, value string) error { var setting models.Setting - result := database.DB.Where("section = ? AND key = ?", section, key).First(&setting) - if result.Error != nil { - setting = models.Setting{Section: section, Key: key, Value: value} - return database.DB.Create(&setting).Error + if database.DB.Where("section = ? AND key = ?", section, key).First(&setting).Error != nil { + return database.DB.Create(&models.Setting{Section: section, Key: key, Value: value}).Error } return database.DB.Model(&setting).Update("value", value).Error } // GetSection 获取整个 section 的设置 func (s *SettingsService) GetSection(section string) map[string]string { - var settings []models.Setting - database.DB.Where("section = ?", section).Find(&settings) - + if section == constant.SectionSite { + return cache.GetSiteCacheAll() + } result := make(map[string]string) - // 先填充默认值 if defaults, ok := constant.DefaultSettings[section]; ok { for k, v := range defaults { result[k] = v } } - // 覆盖数据库值 - for _, s := range settings { - result[s.Key] = s.Value + var settings []models.Setting + database.DB.Where("section = ?", section).Find(&settings) + for _, setting := range settings { + result[setting.Key] = setting.Value } return result } @@ -84,5 +80,8 @@ func (s *SettingsService) SetSection(section string, values map[string]string) e return err } } + if section == constant.SectionSite { + cache.SetSiteCacheBatch(values) + } return nil } diff --git a/internal/services/user_service.go b/internal/services/user_service.go index 296de28..c132faa 100644 --- a/internal/services/user_service.go +++ b/internal/services/user_service.go @@ -9,18 +9,14 @@ import ( "baihu/internal/models" ) -type UserService struct { - settingsService *SettingsService -} +type UserService struct{} -func NewUserService(settingsService *SettingsService) *UserService { - return &UserService{settingsService: settingsService} +func NewUserService() *UserService { + return &UserService{} } func (us *UserService) hashPassword(password string) string { - // 使用 JWT Secret 作为密码 salt - salt := us.settingsService.Get(constant.SectionSystem, constant.KeyJWTSecret) - hash := sha256.Sum256([]byte(password + salt)) + hash := sha256.Sum256([]byte(password + constant.Secret)) return hex.EncodeToString(hash[:]) } diff --git a/internal/utils/pagination.go b/internal/utils/pagination.go index 0df0995..188de3d 100644 --- a/internal/utils/pagination.go +++ b/internal/utils/pagination.go @@ -3,6 +3,7 @@ package utils import ( "strconv" + "baihu/internal/cache" "baihu/internal/constant" "github.com/gin-gonic/gin" @@ -14,16 +15,27 @@ type Pagination struct { PageSize int } +// getDefaultPageSize 从缓存获取默认分页大小 +func getDefaultPageSize() int { + pageSizeStr := cache.GetSiteCache(constant.KeyPageSize) + pageSize, err := strconv.Atoi(pageSizeStr) + if err != nil || pageSize < 1 { + return 10 + } + return pageSize +} + // ParsePagination 从请求中解析分页参数 func ParsePagination(c *gin.Context) Pagination { + defaultPageSize := getDefaultPageSize() page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) - pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", strconv.Itoa(constant.DefaultPageSize))) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", strconv.Itoa(defaultPageSize))) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 100 { - pageSize = constant.DefaultPageSize + pageSize = defaultPageSize } return Pagination{Page: page, PageSize: pageSize} diff --git a/web/src/api/index.ts b/web/src/api/index.ts index f78d8ee..05e4f41 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -113,7 +113,14 @@ export const api = { getPublicSite: () => request<{ title: string; subtitle: string; icon: string }>('/settings/public'), updateSite: (data: SiteSettings) => request('/settings/site', { method: 'PUT', body: JSON.stringify(data) }), - getAbout: () => request('/settings/about') + getAbout: () => request('/settings/about'), + getLoginLogs: (params?: { page?: number; page_size?: number; username?: string }) => { + const query = new URLSearchParams() + if (params?.page) query.set('page', String(params.page)) + if (params?.page_size) query.set('page_size', String(params.page_size)) + if (params?.username) query.set('username', params.username) + return request(`/settings/login-logs?${query}`) + } }, files: { tree: () => request('/files/tree'), @@ -264,3 +271,21 @@ export interface SiteSettings { page_size: string cookie_days: string } + + +export interface LoginLog { + id: number + username: string + ip: string + user_agent: string + status: string + message: string + created_at: string +} + +export interface LoginLogListResponse { + data: LoginLog[] + total: number + page: number + page_size: number +} diff --git a/web/src/layouts/MainLayout.vue b/web/src/layouts/MainLayout.vue index f13345b..516c560 100644 --- a/web/src/layouts/MainLayout.vue +++ b/web/src/layouts/MainLayout.vue @@ -1,7 +1,7 @@ + + + + + + 登录日志 + 查看系统登录记录 + + + + + + + + + + + + + + + + 用户名 + IP 地址 + 浏览器 + 状态 + 消息 + 时间 + + + + + 暂无登录日志 + + + {{ log.username }} + {{ log.ip }} + {{ getBrowserInfo(log.user_agent) }} + + + {{ log.status === 'success' ? '成功' : '失败' }} + + + {{ log.message }} + {{ log.created_at }} + + + + + + +
查看系统登录记录
{{ log.ip }}