Fix the new authentication issue

This commit is contained in:
2025-10-26 11:57:31 +08:00
parent 270c5a8ffd
commit 9e0eb1497b
15 changed files with 285 additions and 160 deletions

View File

@@ -16,8 +16,8 @@ import (
type ServerConfig struct { type ServerConfig struct {
Host string `json:"host" mapstructure:"host"` // 服务器监听地址 Host string `json:"host" mapstructure:"host"` // 服务器监听地址
Port int `json:"port" mapstructure:"port"` // 服务器监听端口 Port int `json:"port" mapstructure:"port"` // 服务器监听端口
Mode string `json:"mode" mapstructure:"mode"` // 运行模式debug/release
Dist string `json:"dist" mapstructure:"dist"` // 静态文件目录 Dist string `json:"dist" mapstructure:"dist"` // 静态文件目录
DevMode bool `json:"dev_mode" mapstructure:"dev_mode"` // 开发模式(跳过验证码等)
} }
// DatabaseConfig 数据库配置结构体 // DatabaseConfig 数据库配置结构体
@@ -80,7 +80,7 @@ type CookieConfig struct {
type SecurityConfig struct { type SecurityConfig struct {
JWTSecret string `json:"jwt_secret" mapstructure:"jwt_secret"` // JWT签名密钥 JWTSecret string `json:"jwt_secret" mapstructure:"jwt_secret"` // JWT签名密钥
EncryptionKey string `json:"encryption_key" mapstructure:"encryption_key"` // 数据加密密钥 EncryptionKey string `json:"encryption_key" mapstructure:"encryption_key"` // 数据加密密钥
JWTRefreshThresholdHours int `json:"jwt_refresh_threshold_hours" mapstructure:"jwt_refresh_threshold_hours"` // JWT令牌刷新阈值小时 JWTRefresh int `json:"jwt_refresh" mapstructure:"jwt_refresh"` // JWT令牌刷新阈值小时
Cookie CookieConfig `json:"cookie" mapstructure:"cookie"` // Cookie配置 Cookie CookieConfig `json:"cookie" mapstructure:"cookie"` // Cookie配置
} }
@@ -99,8 +99,8 @@ func GetDefaultAppConfig() *AppConfig {
Server: ServerConfig{ Server: ServerConfig{
Host: "0.0.0.0", Host: "0.0.0.0",
Port: 8080, Port: 8080,
Mode: "debug",
Dist: "", Dist: "",
DevMode: false,
}, },
Database: DatabaseConfig{ Database: DatabaseConfig{
Type: "sqlite", Type: "sqlite",
@@ -134,7 +134,7 @@ func GetDefaultAppConfig() *AppConfig {
Security: SecurityConfig{ Security: SecurityConfig{
JWTSecret: "", JWTSecret: "",
EncryptionKey: "", EncryptionKey: "",
JWTRefreshThresholdHours: 6, JWTRefresh: 6,
Cookie: CookieConfig{ Cookie: CookieConfig{
Secure: true, Secure: true,
SameSite: "Lax", SameSite: "Lax",

View File

@@ -75,12 +75,6 @@ func validateServerConfig(config *ServerConfig) error {
return fmt.Errorf("无效的端口号: %d端口号必须在1-65535之间", config.Port) return fmt.Errorf("无效的端口号: %d端口号必须在1-65535之间", config.Port)
} }
// 验证运行模式
validModes := []string{"debug", "release", "test"}
if !contains(validModes, config.Mode) {
return fmt.Errorf("无效的运行模式: %s支持的模式: %s", config.Mode, strings.Join(validModes, ", "))
}
return nil return nil
} }
@@ -200,7 +194,7 @@ func validateSecurityConfig(config *SecurityConfig) error {
return errors.New("加密密钥长度不能少于16个字符") return errors.New("加密密钥长度不能少于16个字符")
} }
if config.JWTRefreshThresholdHours < 1 || config.JWTRefreshThresholdHours > 23 { if config.JWTRefresh < 1 || config.JWTRefresh > 23 {
return errors.New("JWT令牌刷新阈值必须在1-23小时之间") return errors.New("JWT令牌刷新阈值必须在1-23小时之间")
} }

View File

@@ -185,13 +185,15 @@ func clearInvalidJWTCookie(w http.ResponseWriter) {
http.SetCookie(w, cookie) http.SetCookie(w, cookie)
} }
// JWT密钥生产环境应从配置文件或环境变量读取 // getJWTSecret 动态获取当前的JWT密钥
var jwtSecret = []byte(viper.GetString("security.jwt_secret")) // 修复安全漏洞:确保每次都从最新配置中获取密钥,而不是使用启动时的全局变量
func getJWTSecret() []byte {
return []byte(viper.GetString("security.jwt_secret"))
}
// JWTClaims JWT载荷结构 // JWTClaims JWT载荷结构
type JWTClaims struct { type JWTClaims struct {
Username string `json:"username"` Username string `json:"username"`
IsAdmin bool `json:"is_admin"` // 是否为管理员
PasswordHash string `json:"password_hash"` // 密码哈希摘要,用于验证密码是否被修改 PasswordHash string `json:"password_hash"` // 密码哈希摘要,用于验证密码是否被修改
jwt.RegisteredClaims jwt.RegisteredClaims
} }
@@ -206,7 +208,6 @@ func generateJWTTokenForAdmin(adminUser models.User) (string, error) {
claims := JWTClaims{ claims := JWTClaims{
Username: adminUser.Username, Username: adminUser.Username,
IsAdmin: true, // 管理员
PasswordHash: passwordHashDigest, // 包含密码哈希摘要 PasswordHash: passwordHashDigest, // 包含密码哈希摘要
RegisteredClaims: jwt.RegisteredClaims{ RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
@@ -218,7 +219,7 @@ func generateJWTTokenForAdmin(adminUser models.User) (string, error) {
} }
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret) return token.SignedString(getJWTSecret())
} }
// parseJWTToken 解析并验证JWT令牌 // parseJWTToken 解析并验证JWT令牌
@@ -230,7 +231,7 @@ func parseJWTToken(tokenString string) (*JWTClaims, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
} }
return jwtSecret, nil return getJWTSecret(), nil
}) })
if err != nil { if err != nil {
@@ -244,11 +245,48 @@ func parseJWTToken(tokenString string) (*JWTClaims, error) {
return nil, fmt.Errorf("invalid token") return nil, fmt.Errorf("invalid token")
} }
// getJWTCookie 获取JWT cookie的通用函数
func getJWTCookie(r *http.Request) (*http.Cookie, error) {
return r.Cookie("admin_session")
}
// validateAdminPasswordHash 验证管理员密码哈希的通用函数
func validateAdminPasswordHash(claims *JWTClaims, r *http.Request) bool {
// 【安全修复】验证数据库中的当前密码哈希
// 这确保了密码修改后旧的JWT令牌会失效
db, err := database.GetDB()
if err != nil {
fmt.Printf("[SECURITY WARNING] Database connection failed during auth - Username=%s, IP=%s\n",
claims.Username, r.RemoteAddr)
return false
}
// 获取当前数据库中的管理员密码
var adminPassword models.Settings
if err := db.Where("name = ?", "admin_password").First(&adminPassword).Error; err != nil {
fmt.Printf("[SECURITY WARNING] Admin password not found in database - Username=%s, IP=%s\n",
claims.Username, r.RemoteAddr)
return false
}
// 生成当前数据库密码的哈希摘要
currentPasswordHash := utils.GenerateSHA256Hash(adminPassword.Value)
// 验证JWT中的密码哈希是否与当前数据库中的密码哈希一致
if claims.PasswordHash != currentPasswordHash {
fmt.Printf("[SECURITY WARNING] Password hash mismatch - JWT token invalidated - Username=%s, IP=%s\n",
claims.Username, r.RemoteAddr)
return false
}
return true
}
// IsAdminAuthenticated 判断管理员是否已认证(导出) // IsAdminAuthenticated 判断管理员是否已认证(导出)
// - 检查admin_session Cookie中的JWT令牌 // - 检查admin_session Cookie中的JWT令牌
// - 验证令牌签名、过期时间和用户角色 // - 验证令牌签名、过期时间和用户角色
func IsAdminAuthenticated(r *http.Request) bool { func IsAdminAuthenticated(r *http.Request) bool {
cookie, err := r.Cookie("admin_session") cookie, err := getJWTCookie(r)
if err != nil || cookie.Value == "" { if err != nil || cookie.Value == "" {
return false return false
} }
@@ -259,27 +297,17 @@ func IsAdminAuthenticated(r *http.Request) bool {
return false return false
} }
// 验证用户角色(只允许管理员) // 注释:由于这是管理员专用认证函数,不需要额外的角色验证
if !claims.IsAdmin {
return false
}
// 对于管理员不需要验证数据库中的用户记录因为管理员信息存储在settings中 // 验证密码哈希
// 只需要验证JWT中的信息即可 return validateAdminPasswordHash(claims, r)
if !claims.IsAdmin {
fmt.Printf("[SECURITY WARNING] Invalid admin token detected - Username=%s, IP=%s\n",
claims.Username, r.RemoteAddr)
return false
}
return true
} }
// IsAdminAuthenticatedWithCleanup 带自动清理功能的JWT校验函数 // IsAdminAuthenticatedWithCleanup 带自动清理功能的JWT校验函数
// - 当JWT校验失败时自动清理失效的Cookie // - 当JWT校验失败时自动清理失效的Cookie
// - 适用于API接口等需要清理失效令牌的场景 // - 适用于API接口等需要清理失效令牌的场景
func IsAdminAuthenticatedWithCleanup(w http.ResponseWriter, r *http.Request) bool { func IsAdminAuthenticatedWithCleanup(w http.ResponseWriter, r *http.Request) bool {
cookie, err := r.Cookie("admin_session") cookie, err := getJWTCookie(r)
if err != nil || cookie.Value == "" { if err != nil || cookie.Value == "" {
return false return false
} }
@@ -292,17 +320,10 @@ func IsAdminAuthenticatedWithCleanup(w http.ResponseWriter, r *http.Request) boo
return false return false
} }
// 验证用户角色(只允许管理员) // 注释:由于这是管理员专用认证函数,不需要额外的角色验证
if !claims.IsAdmin {
clearInvalidJWTCookie(w)
return false
}
// 对于管理员不需要验证数据库中的用户记录因为管理员信息存储在settings中 // 验证密码哈希
// 只需要验证JWT中的信息即可 if !validateAdminPasswordHash(claims, r) {
if !claims.IsAdmin {
fmt.Printf("[SECURITY WARNING] Invalid admin token detected - Username=%s, IP=%s\n",
claims.Username, r.RemoteAddr)
clearInvalidJWTCookie(w) clearInvalidJWTCookie(w)
return false return false
} }
@@ -315,7 +336,7 @@ func IsAdminAuthenticatedWithCleanup(w http.ResponseWriter, r *http.Request) boo
// - 自动刷新接近过期的令牌剩余时间少于6小时时刷新 // - 自动刷新接近过期的令牌剩余时间少于6小时时刷新
// - 返回用户ID、用户名和角色 // - 返回用户ID、用户名和角色
func GetCurrentAdminUser(r *http.Request) (*JWTClaims, error) { func GetCurrentAdminUser(r *http.Request) (*JWTClaims, error) {
cookie, err := r.Cookie("admin_session") cookie, err := getJWTCookie(r)
if err != nil { if err != nil {
return nil, fmt.Errorf("未找到会话信息") return nil, fmt.Errorf("未找到会话信息")
} }
@@ -325,15 +346,7 @@ func GetCurrentAdminUser(r *http.Request) (*JWTClaims, error) {
return nil, fmt.Errorf("无效的会话信息") return nil, fmt.Errorf("无效的会话信息")
} }
if !claims.IsAdmin { // 注释:由于这是管理员专用函数,不需要额外的角色验证
return nil, fmt.Errorf("权限不足")
}
// 对于管理员不需要验证数据库中的用户记录因为管理员信息存储在settings中
// 只需要验证JWT中的信息即可
if !claims.IsAdmin {
return nil, fmt.Errorf("无效的管理员令牌")
}
return claims, nil return claims, nil
} }
@@ -343,7 +356,7 @@ func GetCurrentAdminUser(r *http.Request) (*JWTClaims, error) {
// - 自动刷新接近过期的令牌剩余时间少于6小时时刷新 // - 自动刷新接近过期的令牌剩余时间少于6小时时刷新
// - 返回用户ID、用户名、角色和是否刷新了令牌 // - 返回用户ID、用户名、角色和是否刷新了令牌
func GetCurrentAdminUserWithRefresh(w http.ResponseWriter, r *http.Request) (*JWTClaims, bool, error) { func GetCurrentAdminUserWithRefresh(w http.ResponseWriter, r *http.Request) (*JWTClaims, bool, error) {
cookie, err := r.Cookie("admin_session") cookie, err := getJWTCookie(r)
if err != nil { if err != nil {
return nil, false, fmt.Errorf("未找到会话信息") return nil, false, fmt.Errorf("未找到会话信息")
} }
@@ -353,19 +366,16 @@ func GetCurrentAdminUserWithRefresh(w http.ResponseWriter, r *http.Request) (*JW
return nil, false, fmt.Errorf("无效的会话信息") return nil, false, fmt.Errorf("无效的会话信息")
} }
if !claims.IsAdmin { // 注释:由于这是管理员专用函数,不需要额外的角色验证
return nil, false, fmt.Errorf("权限不足")
}
// 对于管理员不需要验证数据库中的用户记录因为管理员信息存储在settings中 // 验证密码哈希
// 只需要验证JWT中的信息即可 if !validateAdminPasswordHash(claims, r) {
if !claims.IsAdmin { return nil, false, fmt.Errorf("会话已失效,请重新登录")
return nil, false, fmt.Errorf("无效的管理员令牌")
} }
// 检查是否需要刷新令牌(根据配置的阈值) // 检查是否需要刷新令牌(根据配置的阈值)
refreshed := false refreshed := false
refreshThreshold := time.Duration(viper.GetInt("security.jwt_refresh_threshold_hours")) * time.Hour refreshThreshold := time.Duration(viper.GetInt("security.jwt_refresh")) * time.Hour
if time.Until(claims.ExpiresAt.Time) < refreshThreshold { if time.Until(claims.ExpiresAt.Time) < refreshThreshold {
// 为管理员生成新的JWT令牌 // 为管理员生成新的JWT令牌
adminUser := models.User{ adminUser := models.User{

View File

@@ -11,6 +11,7 @@ import (
"networkDev/utils" "networkDev/utils"
"github.com/mojocn/base64Captcha" "github.com/mojocn/base64Captcha"
"github.com/spf13/viper"
) )
// 全局验证码存储器 // 全局验证码存储器
@@ -91,6 +92,11 @@ func CaptchaHandler(w http.ResponseWriter, r *http.Request) {
// 这个函数将在登录处理中被调用 // 这个函数将在登录处理中被调用
// 支持大小写不敏感匹配 // 支持大小写不敏感匹配
func VerifyCaptcha(r *http.Request, captchaValue string) bool { func VerifyCaptcha(r *http.Request, captchaValue string) bool {
// 检查是否为开发模式,如果是则跳过验证码验证
if viper.GetBool("server.dev_mode") {
return true
}
// 从cookie中获取验证码ID // 从cookie中获取验证码ID
cookie, err := r.Cookie("captcha_id") cookie, err := r.Cookie("captcha_id")
if err != nil { if err != nil {

View File

@@ -3,6 +3,7 @@ package admin
import ( import (
"net/http" "net/http"
"networkDev/database" "networkDev/database"
"networkDev/models"
"networkDev/services" "networkDev/services"
"networkDev/utils" "networkDev/utils"
"networkDev/utils/timeutil" "networkDev/utils/timeutil"
@@ -10,7 +11,24 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
// AdminIndexHandler /admin 与 /admin/ 根路径入口 // formatDBType 格式化数据库类型显示
// 将配置文件中的小写类型转换为友好的显示格式
func formatDBType(dbType string) string {
switch dbType {
case "mysql":
return "MySQL"
case "sqlite":
return "SQLite"
case "postgresql", "postgres":
return "PostgreSQL"
case "sqlserver":
return "SQL Server"
default:
return "SQLite" // 默认显示
}
}
// AdminIndexHandler 后台首页处理器/admin 与 /admin/ 根路径入口
// - 未登录:重定向到 /admin/login // - 未登录:重定向到 /admin/login
// - 已登录:渲染后台布局页(或重定向到 /admin/layout // - 已登录:渲染后台布局页(或重定向到 /admin/layout
// - 自动清理失效的JWT Cookie // - 自动清理失效的JWT Cookie
@@ -71,10 +89,10 @@ func AdminLayoutHandler(w http.ResponseWriter, r *http.Request) {
} }
// DashboardFragmentHandler 仪表盘片段渲染 // DashboardFragmentHandler 仪表盘片段渲染
// - 展示系统信息:版本、运行模式、数据库类型、启动时长 // - 展示系统信息:版本、开发模式、数据库类型、启动时长
func DashboardFragmentHandler(w http.ResponseWriter, r *http.Request) { func DashboardFragmentHandler(w http.ResponseWriter, r *http.Request) {
version := "1.0.0" version := "1.0.0"
mode := viper.GetString("server.mode") mode := viper.GetBool("server.dev_mode")
dbType := viper.GetString("database.type") dbType := viper.GetString("database.type")
if dbType == "" { if dbType == "" {
dbType = "sqlite" dbType = "sqlite"
@@ -84,7 +102,7 @@ func DashboardFragmentHandler(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{ data := map[string]interface{}{
"Version": version, "Version": version,
"Mode": mode, "Mode": mode,
"DBType": dbType, "DBType": formatDBType(dbType),
"Uptime": uptime, "Uptime": uptime,
} }
@@ -100,7 +118,7 @@ func SystemInfoHandler(w http.ResponseWriter, r *http.Request) {
} }
version := "1.0.0" version := "1.0.0"
mode := viper.GetString("server.mode") mode := viper.GetBool("server.dev_mode")
dbType := viper.GetString("database.type") dbType := viper.GetString("database.type")
if dbType == "" { if dbType == "" {
dbType = "sqlite" dbType = "sqlite"
@@ -110,9 +128,64 @@ func SystemInfoHandler(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{ data := map[string]interface{}{
"version": version, "version": version,
"mode": mode, "mode": mode,
"db_type": dbType, "db_type": formatDBType(dbType),
"uptime": uptime, "uptime": uptime,
} }
utils.JsonResponse(w, http.StatusOK, true, "ok", data) utils.JsonResponse(w, http.StatusOK, true, "ok", data)
} }
// DashboardStatsHandler 仪表盘统计数据API接口
// - 返回应用统计数据的JSON数据包括全部/启用/禁用/变量数量
func DashboardStatsHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
// 获取数据库连接
db, err := database.GetDB()
if err != nil {
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
return
}
// 统计应用数据
var totalApps int64
var enabledApps int64
var disabledApps int64
var totalVariables int64
// 统计全部应用数量
if err := db.Model(&models.App{}).Count(&totalApps).Error; err != nil {
utils.JsonResponse(w, http.StatusInternalServerError, false, "统计应用数量失败", nil)
return
}
// 统计启用应用数量
if err := db.Model(&models.App{}).Where("status = ?", 1).Count(&enabledApps).Error; err != nil {
utils.JsonResponse(w, http.StatusInternalServerError, false, "统计启用应用数量失败", nil)
return
}
// 统计禁用应用数量
if err := db.Model(&models.App{}).Where("status = ?", 0).Count(&disabledApps).Error; err != nil {
utils.JsonResponse(w, http.StatusInternalServerError, false, "统计禁用应用数量失败", nil)
return
}
// 统计变量数量
if err := db.Model(&models.Variable{}).Count(&totalVariables).Error; err != nil {
utils.JsonResponse(w, http.StatusInternalServerError, false, "统计变量数量失败", nil)
return
}
data := map[string]interface{}{
"total_apps": totalApps,
"enabled_apps": enabledApps,
"disabled_apps": disabledApps,
"total_variables": totalVariables,
}
utils.JsonResponse(w, http.StatusOK, true, "ok", data)
}

View File

@@ -81,11 +81,7 @@ func UserPasswordUpdateHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
// 确认是管理员 // 注释由于使用了AdminAuthRequired中间件已确保是管理员用户
if !claims.IsAdmin {
utils.JsonResponse(w, http.StatusForbidden, false, "权限不足", nil)
return
}
// 获取数据库连接 // 获取数据库连接
db, err := database.GetDB() db, err := database.GetDB()
@@ -176,7 +172,7 @@ func UserProfileUpdateHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
claims, _, err := GetCurrentAdminUserWithRefresh(w, r) _, _, err := GetCurrentAdminUserWithRefresh(w, r)
if err != nil { if err != nil {
utils.JsonResponse(w, http.StatusUnauthorized, false, "未登录或会话已过期", nil) utils.JsonResponse(w, http.StatusUnauthorized, false, "未登录或会话已过期", nil)
return return
@@ -207,11 +203,7 @@ func UserProfileUpdateHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
// 确认当前用户是管理员 // 注释由于使用了AdminAuthRequired中间件已确保是管理员用户
if !claims.IsAdmin {
utils.JsonResponse(w, http.StatusForbidden, false, "权限不足", nil)
return
}
// 获取所有管理员相关设置 // 获取所有管理员相关设置
var adminSettings []models.Settings var adminSettings []models.Settings

View File

@@ -144,7 +144,7 @@ func initDefaultAdmin(db *gorm.DB) error {
// 如果密码已设置,跳过初始化 // 如果密码已设置,跳过初始化
if passwordSetting.Value != "" { if passwordSetting.Value != "" {
logrus.Info("管理员密码已设置,跳过默认密码初始化") logrus.Debug("管理员密码已设置,跳过默认密码初始化")
return nil return nil
} }

View File

@@ -54,6 +54,9 @@ func RegisterAdminRoutes(mux *http.ServeMux) {
// 系统信息API用于仪表盘定时刷新 // 系统信息API用于仪表盘定时刷新
mux.HandleFunc("/admin/api/system/info", adminctl.AdminAuthRequired(adminctl.SystemInfoHandler)) mux.HandleFunc("/admin/api/system/info", adminctl.AdminAuthRequired(adminctl.SystemInfoHandler))
// 仪表盘统计数据API
mux.HandleFunc("/admin/api/dashboard/stats", adminctl.AdminAuthRequired(adminctl.DashboardStatsHandler))
// 个人资料API // 个人资料API
mux.HandleFunc("/admin/api/user/profile", adminctl.AdminAuthRequired(adminctl.UserProfileQueryHandler)) mux.HandleFunc("/admin/api/user/profile", adminctl.AdminAuthRequired(adminctl.UserProfileQueryHandler))
mux.HandleFunc("/admin/api/user/profile/update", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.UserProfileUpdateHandler))) mux.HandleFunc("/admin/api/user/profile/update", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.UserProfileUpdateHandler)))

View File

@@ -1,16 +1,16 @@
{{ define "apis.html" }} {{ define "apis.html" }}
<section> <section>
<h2>接口管理</h2>d <h2>接口管理</h2>
<div class="layui-card" style="margin-top:12px"> <div class="layui-panel" style="margin-top:12px">
<div class="layui-card-header">筛选</div>d <h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">筛选</h3>
<div class="layui-card-body"> <div style="padding: 20px;">
<form class="layui-form layui-form-pane" id="apiFilterForm" lay-filter="apiFilterForm"> <form class="layui-form layui-form-pane" id="apiFilterForm" lay-filter="apiFilterForm">
<div class="layui-form-item"> <div class="layui-form-item">
<div class="layui-inline"> <div class="layui-inline">
<label class="layui-form-label">应用</label> <label class="layui-form-label">应用</label>
<div class="layui-input-inline"> <div class="layui-input-inline">
<select name="app_uuid" lay-filter="appSelect" lay-search=""> <select name="app_uuid" lay-filter="appSelect" lay-search="">
<option value="">请选择应用</option>e <option value="">请选择应用</option>
</select> </select>
</div> </div>
</div> </div>
@@ -30,9 +30,9 @@
</div> </div>
</div> </div>
<div class="layui-card" style="margin-top:12px"> <div class="layui-panel" style="margin-top:12px">
<div class="layui-card-header">接口列表</div> <h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">接口列表</h3>
<div class="layui-card-body"> <div style="padding: 20px;">
<table id="apisTable" lay-filter="apisTableFilter"></table> <table id="apisTable" lay-filter="apisTableFilter"></table>
<script type="text/html" id="tpl-apis-ops"> <script type="text/html" id="tpl-apis-ops">
<div style="white-space: nowrap;"> <div style="white-space: nowrap;">

View File

@@ -11,9 +11,9 @@
批量禁用</button> 批量禁用</button>
</div> </div>
<div class="layui-card" style="margin-top:12px"> <div class="layui-panel" style="margin-top:12px">
<div class="layui-card-header">筛选</div> <h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">筛选</h3>
<div class="layui-card-body"> <div style="padding: 20px;">
<form class="layui-form layui-form-pane" id="appFilterForm" lay-filter="appFilterForm"> <form class="layui-form layui-form-pane" id="appFilterForm" lay-filter="appFilterForm">
<div class="layui-form-item"> <div class="layui-form-item">
<div class="layui-inline"> <div class="layui-inline">
@@ -31,9 +31,9 @@
</div> </div>
</div> </div>
<div class="layui-card" style="margin-top:12px"> <div class="layui-panel" style="margin-top:12px">
<div class="layui-card-header">应用列表</div> <h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">应用列表</h3>
<div class="layui-card-body"> <div style="padding: 20px;">
<table id="appsTable" lay-filter="appsTableFilter"></table> <table id="appsTable" lay-filter="appsTableFilter"></table>
<script type="text/html" id="tpl-apps-ops"> <script type="text/html" id="tpl-apps-ops">
<div style="white-space: nowrap;"> <div style="white-space: nowrap;">

View File

@@ -2,37 +2,66 @@
<section> <section>
<h2>系统信息</h2> <h2>系统信息</h2>
<div class="layui-row layui-col-space15" style="margin-top:12px"> <div class="layui-row layui-col-space15" style="margin-top:12px">
<div class="layui-col-md6"> <!-- 系统信息面板 -->
<div class="layui-card"> <div class="layui-col-md8">
<div class="layui-card-header">基本信息</div> <div class="layui-panel">
<div class="layui-card-body"> <div style="padding: 20px;">
<div class="system-info-grid"> <h3 style="margin-top: 0; margin-bottom: 15px; font-weight: bold; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px;">系统信息</h3>
<div class="system-info-item"> <table class="layui-table" lay-skin="nob">
<div class="system-info-label">版本</div> <tbody>
<div class="system-info-value">{{ .Version }}</div> <tr>
</div> <td style="width: 120px; font-weight: bold;">程序版本</td>
<div class="system-info-item"> <td><span style="font-size: 18px; font-weight: bold; color: var(--lay-color-normal);">{{ .Version }}</span></td>
<div class="system-info-label">运行模式</div> </tr>
<div class="system-info-value">{{ .Mode }}</div> <tr>
<td style="font-weight: bold;">存储方案</td>
<td><span style="font-size: 18px; font-weight: bold; color: var(--lay-color-info);">{{ .DBType }}</span></td>
</tr>
<tr>
<td style="font-weight: bold;">开发模式</td>
<td>
{{ if .Mode }}
<span style="font-size: 18px; font-weight: bold; color: var(--lay-color-danger);">开启</span>
{{ else }}
<span style="font-size: 18px; font-weight: bold; color: var(--lay-color-success);">关闭</span>
{{ end }}
</td>
</tr>
<tr>
<td style="font-weight: bold;">运行时长</td>
<td><span id="uptime-display" style="font-size: 18px; font-weight: bold; color: var(--lay-color-normal);">{{ .Uptime }}</span></td>
</tr>
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>
</div>
</div> <!-- 应用统计面板 -->
<div class="layui-col-md6"> <div class="layui-col-md4">
<div class="layui-card"> <div class="layui-panel">
<div class="layui-card-header">运行状态</div> <div style="padding: 20px;">
<div class="layui-card-body"> <h3 style="margin-top: 0; margin-bottom: 15px; font-weight: bold; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px;">应用统计</h3>
<div class="system-info-grid"> <table class="layui-table" lay-skin="nob">
<div class="system-info-item"> <tbody>
<div class="system-info-label">数据库</div> <tr>
<div class="system-info-value">{{ .DBType }}</div> <td style="width: 120px; font-weight: bold;">全部应用</td>
</div> <td><span id="total-apps" style="font-size: 18px; font-weight: bold;">0</span></td>
<div class="system-info-item"> </tr>
<div class="system-info-label">运行时长</div> <tr>
<div class="system-info-value">{{ .Uptime }}</div> <td style="font-weight: bold;">启用应用</td>
</div> <td><span id="enabled-apps" style="font-size: 18px; font-weight: bold; color: var(--lay-color-success);">0</span></td>
</div> </tr>
<tr>
<td style="font-weight: bold;">禁用应用</td>
<td><span id="disabled-apps" style="font-size: 18px; font-weight: bold; color: var(--lay-color-danger);">0</span></td>
</tr>
<tr>
<td style="font-weight: bold;">变量数量</td>
<td><span id="total-variables" style="font-size: 18px; font-weight: bold; color: var(--lay-color-info);">0</span></td>
</tr>
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>
@@ -71,12 +100,7 @@
const data = res.data; const data = res.data;
// 更新运行时长 // 更新运行时长
if (data.uptime) { if (data.uptime) {
$('.system-info-item').each(function () { $('#uptime-display').text(data.uptime);
const label = $(this).find('.system-info-label').text();
if (label === '运行时长') {
$(this).find('.system-info-value').text(data.uptime);
}
});
} }
} }
}).fail(() => { }).fail(() => {
@@ -84,8 +108,30 @@
}); });
}; };
// 立即刷新一次系统信息 // 函数:刷新应用统计数据
// 说明:请求后台获取应用统计信息并更新页面显示
const refreshAppStats = () => {
$.get('/admin/api/dashboard/stats', (res) => {
if (res && res.code === 0 && res.data) {
const data = res.data;
$('#total-apps').text(data.total_apps || 0);
$('#enabled-apps').text(data.enabled_apps || 0);
$('#disabled-apps').text(data.disabled_apps || 0);
$('#total-variables').text(data.total_variables || 0);
}
}).fail(() => {
console.log('获取应用统计失败');
// 显示默认值
$('#total-apps').text('0');
$('#enabled-apps').text('0');
$('#disabled-apps').text('0');
$('#total-variables').text('0');
});
};
// 立即刷新一次系统信息和应用统计
refreshSystemInfo(); refreshSystemInfo();
refreshAppStats();
}); });
</script> </script>
{{ end }} {{ end }}

View File

@@ -2,9 +2,9 @@
<section> <section>
<h2>系统设置</h2> <h2>系统设置</h2>
<!-- 基本信息设置 --> <!-- 基本信息设置 -->
<div class="layui-card" style="margin-top: 16px;"> <div class="layui-panel" style="margin-top: 16px;">
<div class="layui-card-header">基本信息设置</div> <h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">基本信息设置</h3>
<div class="layui-card-body"> <div style="padding: 20px;">
<form class="layui-form" id="basicForm"> <form class="layui-form" id="basicForm">
<div class="layui-form-item"> <div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="site-title">站点标题</label> <label class="layui-form-label" style="cursor: pointer;" data-tips="site-title">站点标题</label>
@@ -35,9 +35,9 @@
</div> </div>
<!-- 系统配置设置 --> <!-- 系统配置设置 -->
<div class="layui-card" style="margin-top: 16px;"> <div class="layui-panel" style="margin-top: 16px;">
<div class="layui-card-header">系统配置</div> <h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">系统配置</h3>
<div class="layui-card-body"> <div style="padding: 20px;">
<form class="layui-form" id="systemForm"> <form class="layui-form" id="systemForm">
<div class="layui-form-item"> <div class="layui-form-item">
<label class="layui-form-label" style="cursor: pointer;" data-tips="maintenance-mode">维护模式</label> <label class="layui-form-label" style="cursor: pointer;" data-tips="maintenance-mode">维护模式</label>
@@ -72,9 +72,9 @@
</div> </div>
<!-- 页脚与备案信息 --> <!-- 页脚与备案信息 -->
<div class="layui-card" style="margin-top: 16px;"> <div class="layui-panel" style="margin-top: 16px;">
<div class="layui-card-header">页脚与备案</div> <h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">页脚与备案</h3>
<div class="layui-card-body"> <div style="padding: 20px;">
<form class="layui-form" id="footerForm"> <form class="layui-form" id="footerForm">
<div class="layui-form-item layui-form-text"> <div class="layui-form-item layui-form-text">
<label class="layui-form-label" style="cursor: pointer;" data-tips="footer-text">页脚文本</label> <label class="layui-form-label" style="cursor: pointer;" data-tips="footer-text">页脚文本</label>

View File

@@ -9,9 +9,9 @@
<div class="layui-tab-content"> <div class="layui-tab-content">
<!-- 修改密码模块 --> <!-- 修改密码模块 -->
<div class="layui-tab-item layui-show"> <div class="layui-tab-item layui-show">
<div class="layui-card" style="margin-top: 16px;"> <div class="layui-panel" style="margin-top: 16px;">
<div class="layui-card-header">修改密码</div> <h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">修改密码</h3>
<div class="layui-card-body"> <div style="padding: 20px;">
<form class="layui-form" id="passwordForm" lay-filter="passwordForm" onsubmit="return false"> <form class="layui-form" id="passwordForm" lay-filter="passwordForm" onsubmit="return false">
<div class="layui-form-item"> <div class="layui-form-item">
<label class="layui-form-label">当前密码</label> <label class="layui-form-label">当前密码</label>
@@ -51,9 +51,9 @@
<!-- 修改用户名模块 --> <!-- 修改用户名模块 -->
<div class="layui-tab-item"> <div class="layui-tab-item">
<div class="layui-card" style="margin-top: 16px;"> <div class="layui-panel" style="margin-top: 16px;">
<div class="layui-card-header">修改用户名</div> <h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">修改用户名</h3>
<div class="layui-card-body"> <div style="padding: 20px;">
<form class="layui-form" id="usernameForm" lay-filter="usernameForm" onsubmit="return false"> <form class="layui-form" id="usernameForm" lay-filter="usernameForm" onsubmit="return false">
<div class="layui-form-item"> <div class="layui-form-item">
<label class="layui-form-label">当前用户名</label> <label class="layui-form-label">当前用户名</label>

View File

@@ -7,9 +7,9 @@
批量删除</button> 批量删除</button>
</div> </div>
<div class="layui-card" style="margin-top:12px"> <div class="layui-panel" style="margin-top:12px">
<div class="layui-card-header">筛选</div> <h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">筛选</h3>
<div class="layui-card-body"> <div style="padding: 20px;">
<form class="layui-form layui-form-pane" id="variableFilterForm" lay-filter="variableFilterForm"> <form class="layui-form layui-form-pane" id="variableFilterForm" lay-filter="variableFilterForm">
<div class="layui-form-item"> <div class="layui-form-item">
<div class="layui-inline"> <div class="layui-inline">
@@ -35,9 +35,9 @@
</div> </div>
</div> </div>
<div class="layui-card" style="margin-top:12px"> <div class="layui-panel" style="margin-top:12px">
<div class="layui-card-header">变量列表</div> <h3 style="margin: 0; padding: 15px 20px; border-bottom: 1px solid var(--lay-color-border-2); padding-bottom: 10px; margin-bottom: 15px;">变量列表</h3>
<div class="layui-card-body"> <div style="padding: 20px;">
<table id="variablesTable" lay-filter="variablesTableFilter"></table> <table id="variablesTable" lay-filter="variablesTableFilter"></table>
</div> </div>
</div> </div>

Submodule web/template/layui-theme-dark added at a89e6787f4