New administrator authentication method

New configuration generation scheme
This commit is contained in:
2025-10-26 09:35:07 +08:00
parent c93ee377fe
commit 270c5a8ffd
18 changed files with 520 additions and 566 deletions

4
.gitignore vendored
View File

@@ -1,8 +1,8 @@
/config.json
/database.db
/recharge.db
logs
模板
.DS_Store
networkDev
config.json
node.txt
networkDev

View File

@@ -60,11 +60,7 @@ func runServer(cmd *cobra.Command, args []string) {
if err := database.AutoMigrate(); err != nil {
logrus.WithError(err).Fatal("数据库自动迁移失败")
}
// 初始化默认管理员账号admin/admin123
if err := database.SeedDefaultAdmin(); err != nil {
logrus.WithError(err).Fatal("默认管理员初始化失败")
}
// 初始化默认系统设置
// 初始化默认系统设置(包含管理员账号)
if err := database.SeedDefaultSettings(); err != nil {
logrus.WithError(err).Fatal("默认系统设置初始化失败")
}

View File

@@ -2,7 +2,7 @@ package config
import (
"bytes"
_ "embed"
"encoding/json"
"errors"
"io/fs"
"os"
@@ -11,8 +11,156 @@ import (
"github.com/spf13/viper"
)
//go:embed config.json
var DefaultConfig string
// ServerConfig 服务器配置结构体
// 包含服务器运行相关的配置信息
type ServerConfig struct {
Host string `json:"host" mapstructure:"host"` // 服务器监听地址
Port int `json:"port" mapstructure:"port"` // 服务器监听端口
Mode string `json:"mode" mapstructure:"mode"` // 运行模式debug/release
Dist string `json:"dist" mapstructure:"dist"` // 静态文件目录
}
// DatabaseConfig 数据库配置结构体
// 包含数据库连接相关的配置信息
type DatabaseConfig struct {
Type string `json:"type" mapstructure:"type"` // 数据库类型mysql/sqlite
MySQL MySQLConfig `json:"mysql" mapstructure:"mysql"` // MySQL配置
SQLite SQLiteConfig `json:"sqlite" mapstructure:"sqlite"` // SQLite配置
}
// MySQLConfig MySQL数据库配置结构体
// 包含MySQL数据库连接的详细配置信息
type MySQLConfig struct {
Host string `json:"host" mapstructure:"host"` // 数据库主机地址
Port int `json:"port" mapstructure:"port"` // 数据库端口
Username string `json:"username" mapstructure:"username"` // 数据库用户名
Password string `json:"password" mapstructure:"password"` // 数据库密码
Database string `json:"database" mapstructure:"database"` // 数据库名称
Charset string `json:"charset" mapstructure:"charset"` // 字符集
MaxIdleConns int `json:"max_idle_conns" mapstructure:"max_idle_conns"` // 最大空闲连接数
MaxOpenConns int `json:"max_open_conns" mapstructure:"max_open_conns"` // 最大打开连接数
}
// SQLiteConfig SQLite数据库配置结构体
// 包含SQLite数据库文件路径配置
type SQLiteConfig struct {
Path string `json:"path" mapstructure:"path"` // 数据库文件路径
}
// RedisConfig Redis配置结构体
// 包含Redis连接相关的配置信息
type RedisConfig struct {
Host string `json:"host" mapstructure:"host"` // Redis服务器地址
Port int `json:"port" mapstructure:"port"` // Redis服务器端口
Password string `json:"password" mapstructure:"password"` // Redis密码
DB int `json:"db" mapstructure:"db"` // Redis数据库编号
}
// LogConfig 日志配置结构体
// 包含日志记录相关的配置信息
type LogConfig struct {
Level string `json:"level" mapstructure:"level"` // 日志级别
File string `json:"file" mapstructure:"file"` // 日志文件路径
MaxSize int `json:"max_size" mapstructure:"max_size"` // 单个日志文件最大大小(MB)
MaxBackups int `json:"max_backups" mapstructure:"max_backups"` // 保留的旧日志文件数量
MaxAge int `json:"max_age" mapstructure:"max_age"` // 日志文件保留天数
}
// CookieConfig Cookie配置结构体
// 包含Cookie相关的安全配置信息
type CookieConfig struct {
Secure bool `json:"secure" mapstructure:"secure"` // 是否只在HTTPS下发送Cookie
SameSite string `json:"same_site" mapstructure:"same_site"` // SameSite属性Strict/Lax/None
Domain string `json:"domain" mapstructure:"domain"` // Cookie域名
MaxAge int `json:"max_age" mapstructure:"max_age"` // Cookie最大存活时间
}
// SecurityConfig 安全配置结构体
// 包含应用程序安全相关的配置信息
type SecurityConfig struct {
JWTSecret string `json:"jwt_secret" mapstructure:"jwt_secret"` // JWT签名密钥
EncryptionKey string `json:"encryption_key" mapstructure:"encryption_key"` // 数据加密密钥
JWTRefreshThresholdHours int `json:"jwt_refresh_threshold_hours" mapstructure:"jwt_refresh_threshold_hours"` // JWT令牌刷新阈值小时
Cookie CookieConfig `json:"cookie" mapstructure:"cookie"` // Cookie配置
}
// AppConfig 应用配置结构体
type AppConfig struct {
Server ServerConfig `json:"server" mapstructure:"server"`
Database DatabaseConfig `json:"database" mapstructure:"database"`
Redis RedisConfig `json:"redis" mapstructure:"redis"`
Log LogConfig `json:"log" mapstructure:"log"`
Security SecurityConfig `json:"security" mapstructure:"security"`
}
// GetDefaultAppConfig 获取默认应用配置
func GetDefaultAppConfig() *AppConfig {
return &AppConfig{
Server: ServerConfig{
Host: "0.0.0.0",
Port: 8080,
Mode: "debug",
Dist: "",
},
Database: DatabaseConfig{
Type: "sqlite",
MySQL: MySQLConfig{
Host: "localhost",
Port: 3306,
Username: "root",
Password: "password",
Database: "networkdev",
Charset: "utf8mb4",
MaxIdleConns: 10,
MaxOpenConns: 100,
},
SQLite: SQLiteConfig{
Path: "./database.db",
},
},
Redis: RedisConfig{
Host: "localhost",
Port: 6379,
Password: "",
DB: 0,
},
Log: LogConfig{
Level: "info",
File: "./logs/app.log",
MaxSize: 100,
MaxBackups: 5,
MaxAge: 30,
},
Security: SecurityConfig{
JWTSecret: "",
EncryptionKey: "",
JWTRefreshThresholdHours: 6,
Cookie: CookieConfig{
Secure: true,
SameSite: "Lax",
Domain: "",
MaxAge: 86400,
},
},
}
}
// GetSecureDefaultAppConfig 获取带有安全密钥的默认应用配置
func GetSecureDefaultAppConfig() (*AppConfig, error) {
config := GetDefaultAppConfig()
// 生成安全密钥
jwtSecret, encryptionKey, err := GenerateSecureKeys()
if err != nil {
return nil, err
}
// 设置安全密钥
config.Security.JWTSecret = jwtSecret
config.Security.EncryptionKey = encryptionKey
return config, nil
}
// Init 初始化配置文件
func Init(cfgFilePath string) {
@@ -24,7 +172,31 @@ func Init(cfgFilePath string) {
var pathError *fs.PathError
if errors.As(err, &pathError) {
log.Warn("未找到配置文件,使用默认配置")
err = os.WriteFile(cfgFilePath, []byte(DefaultConfig), 0o644)
// 生成带有安全密钥的默认配置
defaultConfig, configErr := GetSecureDefaultAppConfig()
if configErr != nil {
log.WithFields(
log.Fields{
"err": configErr,
},
).Error("生成安全配置失败,使用基础默认配置")
defaultConfig = GetDefaultAppConfig()
}
// 将配置结构体转换为JSON
configBytes, marshalErr := json.MarshalIndent(defaultConfig, "", " ")
if marshalErr != nil {
log.WithFields(
log.Fields{
"err": marshalErr,
},
).Fatal("序列化默认配置失败")
return
}
// 写入配置文件
err = os.WriteFile(cfgFilePath, configBytes, 0o644)
if err != nil {
log.WithFields(
log.Fields{
@@ -36,16 +208,17 @@ func Init(cfgFilePath string) {
log.Fields{
"file": cfgFilePath,
},
).Info("写入默认配置文件成功")
).Info("写入默认配置文件成功(已生成安全密钥)")
}
// 写完默认配置后再读一次
err = viper.ReadConfig(bytes.NewBuffer([]byte(DefaultConfig)))
// 将配置加载到viper中
err = viper.ReadConfig(bytes.NewBuffer(configBytes))
if err != nil {
log.WithFields(
log.Fields{
"err": err,
},
).Error("读取默认配置文件失败")
).Error("读取默认配置失败")
} else {
log.Info("已成功读取默认配置")
}
@@ -63,8 +236,8 @@ func Init(cfgFilePath string) {
},
).Info("使用配置文件")
// 验证配置并设置默认值
if _, err := ValidateAndSetDefaults(); err != nil {
// 验证配置
if _, err := ValidateConfig(); err != nil {
log.WithFields(
log.Fields{
"err": err,
@@ -75,5 +248,22 @@ func Init(cfgFilePath string) {
// CreateDefaultConfig 创建默认配置文件
func CreateDefaultConfig(filePath string) error {
return os.WriteFile(filePath, []byte(DefaultConfig), 0o644)
// 生成带有安全密钥的默认配置
defaultConfig, err := GetSecureDefaultAppConfig()
if err != nil {
log.WithFields(
log.Fields{
"err": err,
},
).Error("生成安全配置失败,使用基础默认配置")
defaultConfig = GetDefaultAppConfig()
}
// 将配置结构体转换为JSON
configBytes, err := json.MarshalIndent(defaultConfig, "", " ")
if err != nil {
return err
}
return os.WriteFile(filePath, configBytes, 0o644)
}

View File

@@ -1,48 +0,0 @@
{
"server": {
"host": "0.0.0.0",
"port": 8080,
"mode": "debug",
"dist": ""
},
"database": {
"type": "sqlite",
"mysql": {
"host": "localhost",
"port": 3306,
"username": "root",
"password": "password",
"database": "networkdev",
"charset": "utf8mb4",
"max_idle_conns": 10,
"max_open_conns": 100
},
"sqlite": {
"path": "./database.db"
}
},
"redis": {
"host": "localhost",
"port": 6379,
"password": "",
"db": 0
},
"log": {
"level": "info",
"file": "./logs/app.log",
"max_size": 100,
"max_backups": 5,
"max_age": 30
},
"security": {
"jwt_secret": "your-jwt-secret-key",
"encryption_key": "your-encryption-key",
"jwt_refresh_threshold_hours": 6,
"cookie": {
"secure": false,
"same_site": "Lax",
"domain": "",
"max_age": 86400
}
}
}

49
config/security.go Normal file
View File

@@ -0,0 +1,49 @@
package config
import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"fmt"
)
// GenerateSecureJWTSecret 生成安全的JWT密钥
// 生成64字节512位的随机密钥使用base64编码
func GenerateSecureJWTSecret() (string, error) {
// 生成64字节的随机数据
bytes := make([]byte, 64)
if _, err := rand.Read(bytes); err != nil {
return "", fmt.Errorf("生成JWT密钥失败: %w", err)
}
// 使用base64编码便于配置文件存储
return base64.StdEncoding.EncodeToString(bytes), nil
}
// GenerateSecureEncryptionKey 生成安全的加密密钥
// 生成32字节256位的随机密钥使用十六进制编码
func GenerateSecureEncryptionKey() (string, error) {
// 生成32字节的随机数据AES-256
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return "", fmt.Errorf("生成加密密钥失败: %w", err)
}
// 使用十六进制编码
return hex.EncodeToString(bytes), nil
}
// GenerateSecureKeys 生成所有安全密钥
func GenerateSecureKeys() (jwtSecret, encryptionKey string, err error) {
jwtSecret, err = GenerateSecureJWTSecret()
if err != nil {
return "", "", err
}
encryptionKey, err = GenerateSecureEncryptionKey()
if err != nil {
return "", "", err
}
return jwtSecret, encryptionKey, nil
}

View File

@@ -13,80 +13,8 @@ import (
"github.com/spf13/viper"
)
// ServerConfig 服务器配置结构体
// 包含HTTP服务器的基本配置信息
type ServerConfig struct {
Host string `json:"host" mapstructure:"host"` // 服务器监听地址
Port int `json:"port" mapstructure:"port"` // 服务器监听端口
Mode string `json:"mode" mapstructure:"mode"` // 运行模式debug/release
Dist string `json:"dist" mapstructure:"dist"` // 静态文件目录
}
// DatabaseConfig 数据库配置结构体
// 支持MySQL和SQLite两种数据库类型
type DatabaseConfig struct {
Type string `json:"type" mapstructure:"type"` // 数据库类型mysql/sqlite
MySQL MySQLConfig `json:"mysql" mapstructure:"mysql"` // MySQL配置
SQLite SQLiteConfig `json:"sqlite" mapstructure:"sqlite"` // SQLite配置
}
// MySQLConfig MySQL数据库配置结构体
// 包含MySQL数据库连接和连接池的配置信息
type MySQLConfig struct {
Host string `json:"host" mapstructure:"host"` // 数据库主机地址
Port int `json:"port" mapstructure:"port"` // 数据库端口
Username string `json:"username" mapstructure:"username"` // 数据库用户名
Password string `json:"password" mapstructure:"password"` // 数据库密码
Database string `json:"database" mapstructure:"database"` // 数据库名称
Charset string `json:"charset" mapstructure:"charset"` // 字符集
MaxIdleConns int `json:"max_idle_conns" mapstructure:"max_idle_conns"` // 最大空闲连接数
MaxOpenConns int `json:"max_open_conns" mapstructure:"max_open_conns"` // 最大打开连接数
}
// SQLiteConfig SQLite数据库配置结构体
// 包含SQLite数据库文件路径配置
type SQLiteConfig struct {
Path string `json:"path" mapstructure:"path"` // 数据库文件路径
}
// RedisConfig Redis配置结构体
// 包含Redis缓存服务器的连接配置
type RedisConfig struct {
Host string `json:"host" mapstructure:"host"` // Redis服务器地址
Port int `json:"port" mapstructure:"port"` // Redis服务器端口
Password string `json:"password" mapstructure:"password"` // Redis密码
DB int `json:"db" mapstructure:"db"` // Redis数据库编号
}
// LogConfig 日志配置结构体
// 包含日志记录的相关配置信息
type LogConfig struct {
Level string `json:"level" mapstructure:"level"` // 日志级别
File string `json:"file" mapstructure:"file"` // 日志文件路径
MaxSize int `json:"max_size" mapstructure:"max_size"` // 单个日志文件最大大小(MB)
MaxBackups int `json:"max_backups" mapstructure:"max_backups"` // 保留的旧日志文件数量
MaxAge int `json:"max_age" mapstructure:"max_age"` // 日志文件保留天数
}
// SecurityConfig 安全配置结构体
// 包含应用程序安全相关的配置信息
type SecurityConfig struct {
JWTSecret string `json:"jwt_secret" mapstructure:"jwt_secret"` // JWT签名密钥
EncryptionKey string `json:"encryption_key" mapstructure:"encryption_key"` // 数据加密密钥
JWTRefreshThresholdHours int `json:"jwt_refresh_threshold_hours" mapstructure:"jwt_refresh_threshold_hours"` // JWT令牌刷新阈值小时
}
// AppConfig 应用配置结构体
type AppConfig struct {
Server ServerConfig `json:"server" mapstructure:"server"`
Database DatabaseConfig `json:"database" mapstructure:"database"`
Redis RedisConfig `json:"redis" mapstructure:"redis"`
Log LogConfig `json:"log" mapstructure:"log"`
Security SecurityConfig `json:"security" mapstructure:"security"`
}
// ValidateAndSetDefaults 验证配置并设置默认值
func ValidateAndSetDefaults() (*AppConfig, error) {
// ValidateConfig 验证配置
func ValidateConfig() (*AppConfig, error) {
var config AppConfig
// 解析配置到结构体
@@ -94,9 +22,6 @@ func ValidateAndSetDefaults() (*AppConfig, error) {
return nil, fmt.Errorf("解析配置失败: %w", err)
}
// 设置默认值
setDefaults(&config)
// 验证配置
if err := validateConfig(&config); err != nil {
return nil, fmt.Errorf("配置验证失败: %w", err)
@@ -106,74 +31,6 @@ func ValidateAndSetDefaults() (*AppConfig, error) {
return &config, nil
}
// setDefaults 设置默认值
func setDefaults(config *AppConfig) {
// 服务器默认值
if config.Server.Host == "" {
config.Server.Host = "0.0.0.0"
}
if config.Server.Port == 0 {
config.Server.Port = 8080
}
if config.Server.Mode == "" {
config.Server.Mode = "debug"
}
// 数据库默认值
if config.Database.Type == "" {
config.Database.Type = "sqlite"
}
if config.Database.MySQL.Port == 0 {
config.Database.MySQL.Port = 3306
}
if config.Database.MySQL.Charset == "" {
config.Database.MySQL.Charset = "utf8mb4"
}
if config.Database.MySQL.MaxIdleConns == 0 {
config.Database.MySQL.MaxIdleConns = 10
}
if config.Database.MySQL.MaxOpenConns == 0 {
config.Database.MySQL.MaxOpenConns = 100
}
if config.Database.SQLite.Path == "" {
config.Database.SQLite.Path = "./database.db"
}
// Redis默认值
if config.Redis.Host == "" {
config.Redis.Host = "localhost"
}
if config.Redis.Port == 0 {
config.Redis.Port = 6379
}
// 日志默认值
if config.Log.Level == "" {
config.Log.Level = "info"
}
// 不为空的日志文件路径设置默认值,保持为空表示只输出到控制台
if config.Log.MaxSize == 0 {
config.Log.MaxSize = 100
}
if config.Log.MaxBackups == 0 {
config.Log.MaxBackups = 5
}
if config.Log.MaxAge == 0 {
config.Log.MaxAge = 30
}
// 安全配置默认值
if config.Security.JWTSecret == "" || config.Security.JWTSecret == "your-jwt-secret-key" {
config.Security.JWTSecret = "default-jwt-secret-change-in-production"
}
if config.Security.EncryptionKey == "" || config.Security.EncryptionKey == "your-encryption-key" {
config.Security.EncryptionKey = "default-encryption-key-change-in-production"
}
if config.Security.JWTRefreshThresholdHours == 0 {
config.Security.JWTRefreshThresholdHours = 6
}
}
// validateConfig 验证配置
func validateConfig(config *AppConfig) error {
// 验证服务器配置

View File

@@ -98,25 +98,56 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
return
}
var user models.User
dbErr := db.Where("username = ?", body.Username).First(&user).Error
if dbErr != nil {
// 通过前缀匹配一次性获取所有管理员相关设置
var adminSettings []models.Settings
if err = db.Where("name LIKE ?", "admin_%").Find(&adminSettings).Error; err != nil {
utils.JsonResponse(w, http.StatusUnauthorized, false, "用户不存在或密码错误", nil)
return
}
if user.Role != 0 {
utils.JsonResponse(w, http.StatusForbidden, false, "非管理员账号不可登录后台", nil)
// 将设置转换为map便于查找
settingsMap := make(map[string]string)
for _, setting := range adminSettings {
settingsMap[setting.Name] = setting.Value
}
// 检查必要的设置是否存在
adminUsername, hasUsername := settingsMap["admin_username"]
adminPassword, hasPassword := settingsMap["admin_password"]
adminPasswordSalt, hasSalt := settingsMap["admin_password_salt"]
if !hasUsername || !hasPassword || !hasSalt {
utils.JsonResponse(w, http.StatusUnauthorized, false, "用户不存在或密码错误", nil)
return
}
// 验证用户名
if body.Username != adminUsername {
utils.JsonResponse(w, http.StatusUnauthorized, false, "用户不存在或密码错误", nil)
return
}
// 验证密码为空的情况(首次登录需要初始化)
if adminPassword == "" || adminPasswordSalt == "" {
utils.JsonResponse(w, http.StatusInternalServerError, false, "管理员账号未初始化,请联系系统管理员", nil)
return
}
// 使用盐值验证密码
if !utils.VerifyPasswordWithSalt(body.Password, user.PasswordSalt, user.Password) {
if !utils.VerifyPasswordWithSalt(body.Password, adminPasswordSalt, adminPassword) {
utils.JsonResponse(w, http.StatusUnauthorized, false, "用户不存在或密码错误", nil)
return
}
// 创建虚拟用户对象用于生成JWT令牌
adminUser := models.User{
Username: adminUsername,
Password: adminPassword,
PasswordSalt: adminPasswordSalt,
}
// 生成JWT令牌
token, err := generateJWTToken(user)
token, err := generateJWTTokenForAdmin(adminUser)
if err != nil {
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成令牌失败", nil)
return
@@ -159,32 +190,30 @@ var jwtSecret = []byte(viper.GetString("security.jwt_secret"))
// JWTClaims JWT载荷结构
type JWTClaims struct {
UserUUID string `json:"user_uuid"`
Username string `json:"username"`
Role int `json:"role"`
IsAdmin bool `json:"is_admin"` // 是否为管理员
PasswordHash string `json:"password_hash"` // 密码哈希摘要,用于验证密码是否被修改
jwt.RegisteredClaims
}
// generateJWTToken 生成JWT令牌
// - 包含用户ID、用户名、角色信息
// generateJWTTokenForAdmin 生成管理员JWT令牌
// - 包含管理员UUID、用户名信息
// - 设置24小时过期时间
// - 使用HMAC-SHA256签名
func generateJWTToken(user models.User) (string, error) {
func generateJWTTokenForAdmin(adminUser models.User) (string, error) {
// 生成密码哈希摘要使用SHA256
passwordHashDigest := utils.GenerateSHA256Hash(user.Password)
passwordHashDigest := utils.GenerateSHA256Hash(adminUser.Password)
claims := JWTClaims{
UserUUID: user.UUID,
Username: user.Username,
Role: user.Role,
Username: adminUser.Username,
IsAdmin: true, // 管理员
PasswordHash: passwordHashDigest, // 包含密码哈希摘要
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "凌动技术",
Subject: user.UUID,
Subject: adminUser.Username,
},
}
@@ -230,39 +259,16 @@ func IsAdminAuthenticated(r *http.Request) bool {
return false
}
// 验证用户角色(只允许管理员角色=0
if claims.Role != 0 {
// 验证用户角色(只允许管理员)
if !claims.IsAdmin {
return false
}
// 验证用户是否仍然存在于数据库
db, err := database.GetDB()
if err != nil {
return false
}
var user models.User
if dbErr := db.Where("uuid = ? AND role = 0", claims.UserUUID).First(&user).Error; dbErr != nil {
// 记录安全事件用户不存在但持有有效JWT令牌
fmt.Printf("[SECURITY WARNING] Invalid JWT token detected - User not found: UUID=%s, Username=%s, IP=%s\n",
claims.UserUUID, claims.Username, r.RemoteAddr)
return false
}
// 验证用户名是否匹配(防止用户名被修改后仍使用旧令牌)
if user.Username != claims.Username {
// 记录安全事件:用户名不匹配
fmt.Printf("[SECURITY WARNING] Username mismatch detected - Token username=%s, DB username=%s, UUID=%s, IP=%s\n",
claims.Username, user.Username, claims.UserUUID, r.RemoteAddr)
return false
}
// 验证密码哈希是否匹配(防止密码被修改后仍使用旧令牌)
currentPasswordHash := utils.GenerateSHA256Hash(user.Password)
if claims.PasswordHash != currentPasswordHash {
// 记录安全事件:密码哈希不匹配,可能密码已被修改
fmt.Printf("[SECURITY WARNING] Password hash mismatch detected - Token may be invalid due to password change: UUID=%s, Username=%s, IP=%s\n",
claims.UserUUID, claims.Username, r.RemoteAddr)
// 对于管理员不需要验证数据库中的用户记录因为管理员信息存储在settings
// 只需要验证JWT中的信息即可
if !claims.IsAdmin {
fmt.Printf("[SECURITY WARNING] Invalid admin token detected - Username=%s, IP=%s\n",
claims.Username, r.RemoteAddr)
return false
}
@@ -286,42 +292,17 @@ func IsAdminAuthenticatedWithCleanup(w http.ResponseWriter, r *http.Request) boo
return false
}
// 验证用户角色(只允许管理员角色=0
if claims.Role != 0 {
// 验证用户角色(只允许管理员)
if !claims.IsAdmin {
clearInvalidJWTCookie(w)
return false
}
// 验证用户是否仍然存在于数据库
db, err := database.GetDB()
if err != nil {
return false
}
var user models.User
if dbErr := db.Where("uuid = ? AND role = 0", claims.UserUUID).First(&user).Error; dbErr != nil {
// 记录安全事件并清理失效Cookie
fmt.Printf("[SECURITY WARNING] Invalid JWT token detected - User not found: UUID=%s, Username=%s, IP=%s\n",
claims.UserUUID, claims.Username, r.RemoteAddr)
clearInvalidJWTCookie(w)
return false
}
// 验证用户名是否匹配(防止用户名被修改后仍使用旧令牌)
if user.Username != claims.Username {
// 记录安全事件并清理失效Cookie
fmt.Printf("[SECURITY WARNING] Username mismatch detected - Token username=%s, DB username=%s, UUID=%s, IP=%s\n",
claims.Username, user.Username, claims.UserUUID, r.RemoteAddr)
clearInvalidJWTCookie(w)
return false
}
// 验证密码哈希是否匹配(防止密码被修改后仍使用旧令牌)
currentPasswordHash := utils.GenerateSHA256Hash(user.Password)
if claims.PasswordHash != currentPasswordHash {
// 记录安全事件并清理失效Cookie
fmt.Printf("[SECURITY WARNING] Password hash mismatch detected - Token may be invalid due to password change: UUID=%s, Username=%s, IP=%s\n",
claims.UserUUID, claims.Username, r.RemoteAddr)
// 对于管理员不需要验证数据库中的用户记录因为管理员信息存储在settings
// 只需要验证JWT中的信息即可
if !claims.IsAdmin {
fmt.Printf("[SECURITY WARNING] Invalid admin token detected - Username=%s, IP=%s\n",
claims.Username, r.RemoteAddr)
clearInvalidJWTCookie(w)
return false
}
@@ -344,39 +325,14 @@ func GetCurrentAdminUser(r *http.Request) (*JWTClaims, error) {
return nil, fmt.Errorf("无效的会话信息")
}
if claims.Role != 0 {
if !claims.IsAdmin {
return nil, fmt.Errorf("权限不足")
}
// 验证用户是否仍然存在于数据库
db, err := database.GetDB()
if err != nil {
return nil, fmt.Errorf("数据库连接失败")
}
var user models.User
if dbErr := db.Where("uuid = ? AND role = 0", claims.UserUUID).First(&user).Error; dbErr != nil {
// 记录安全事件用户不存在但持有有效JWT令牌
fmt.Printf("[SECURITY WARNING] Invalid JWT token detected in GetCurrentAdminUser - User not found: UUID=%s, Username=%s, IP=%s\n",
claims.UserUUID, claims.Username, r.RemoteAddr)
return nil, fmt.Errorf("用户不存在或权限已变更")
}
// 验证用户名是否匹配(防止用户名被修改后仍使用旧令牌)
if user.Username != claims.Username {
// 记录安全事件:用户名不匹配
fmt.Printf("[SECURITY WARNING] Username mismatch detected in GetCurrentAdminUser - Token username=%s, DB username=%s, UUID=%s, IP=%s\n",
claims.Username, user.Username, claims.UserUUID, r.RemoteAddr)
return nil, fmt.Errorf("用户信息已变更,请重新登录")
}
// 验证密码哈希是否匹配(防止密码被修改后仍使用旧令牌)
currentPasswordHash := utils.GenerateSHA256Hash(user.Password)
if claims.PasswordHash != currentPasswordHash {
// 记录安全事件:密码哈希不匹配,可能密码已被修改
fmt.Printf("[SECURITY WARNING] Password hash mismatch detected in GetCurrentAdminUser - Token may be invalid due to password change: UUID=%s, Username=%s, IP=%s\n",
claims.UserUUID, claims.Username, r.RemoteAddr)
return nil, fmt.Errorf("密码已变更,请重新登录")
// 对于管理员不需要验证数据库中的用户记录因为管理员信息存储在settings
// 只需要验证JWT中的信息即可
if !claims.IsAdmin {
return nil, fmt.Errorf("无效的管理员令牌")
}
return claims, nil
@@ -397,52 +353,25 @@ func GetCurrentAdminUserWithRefresh(w http.ResponseWriter, r *http.Request) (*JW
return nil, false, fmt.Errorf("无效的会话信息")
}
if claims.Role != 0 {
if !claims.IsAdmin {
return nil, false, fmt.Errorf("权限不足")
}
// 验证用户是否仍然存在于数据库
db, err := database.GetDB()
if err != nil {
return nil, false, fmt.Errorf("数据库连接失败")
}
var user models.User
if dbErr := db.Where("uuid = ? AND role = 0", claims.UserUUID).First(&user).Error; dbErr != nil {
// 记录安全事件用户不存在但持有有效JWT令牌
fmt.Printf("[SECURITY WARNING] Invalid JWT token detected in GetCurrentAdminUserWithRefresh - User not found: UUID=%s, Username=%s, IP=%s\n",
claims.UserUUID, claims.Username, r.RemoteAddr)
return nil, false, fmt.Errorf("用户不存在或权限已变更")
}
// 验证用户名是否匹配(防止用户名被修改后仍使用旧令牌)
if user.Username != claims.Username {
// 记录安全事件:用户名不匹配
fmt.Printf("[SECURITY WARNING] Username mismatch detected in GetCurrentAdminUserWithRefresh - Token username=%s, DB username=%s, UUID=%s, IP=%s\n",
claims.Username, user.Username, claims.UserUUID, r.RemoteAddr)
return nil, false, fmt.Errorf("用户信息已变更,请重新登录")
}
// 验证密码哈希是否匹配(防止密码被修改后仍使用旧令牌)
currentPasswordHash := utils.GenerateSHA256Hash(user.Password)
if claims.PasswordHash != currentPasswordHash {
// 记录安全事件:密码哈希不匹配,可能密码已被修改
fmt.Printf("[SECURITY WARNING] Password hash mismatch detected in GetCurrentAdminUserWithRefresh - Token may be invalid due to password change: UUID=%s, Username=%s, IP=%s\n",
claims.UserUUID, claims.Username, r.RemoteAddr)
return nil, false, fmt.Errorf("密码已变更,请重新登录")
// 对于管理员不需要验证数据库中的用户记录因为管理员信息存储在settings
// 只需要验证JWT中的信息即可
if !claims.IsAdmin {
return nil, false, fmt.Errorf("无效的管理员令牌")
}
// 检查是否需要刷新令牌(根据配置的阈值)
refreshed := false
refreshThreshold := time.Duration(viper.GetInt("security.jwt_refresh_threshold_hours")) * time.Hour
if time.Until(claims.ExpiresAt.Time) < refreshThreshold {
// 生成新的JWT令牌
user := models.User{
UUID: claims.UserUUID,
// 为管理员生成新的JWT令牌
adminUser := models.User{
Username: claims.Username,
Role: claims.Role,
}
newToken, err := generateJWTToken(user)
newToken, err := generateJWTTokenForAdmin(adminUser)
if err == nil {
// 更新Cookie使用安全配置
newCookie := utils.CreateSecureCookie("admin_session", newToken, utils.GetDefaultCookieMaxAge())

View File

@@ -2,7 +2,6 @@ package admin
import (
"encoding/json"
"fmt"
"net/http"
"networkDev/database"
"networkDev/models"
@@ -16,9 +15,9 @@ func UserFragmentHandler(w http.ResponseWriter, r *http.Request) {
utils.RenderTemplate(w, "user.html", map[string]interface{}{})
}
// UserProfileQueryHandler 查询当前登录管理员的基本信息
// - 返回 uuid/username/role/created_at 四个字段
// - 自动刷新接近过期的JWT令牌
// UserProfileQueryHandler 获取当前登录管理员的用户名
// - 返回 JSON: {username}
// - 直接从JWT获取用户名信息
func UserProfileQueryHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
@@ -31,24 +30,8 @@ func UserProfileQueryHandler(w http.ResponseWriter, r *http.Request) {
return
}
// 查询用户完整信息以获取创建时间
db, err := database.GetDB()
if err != nil {
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
return
}
var user models.User
if dbErr := db.Where("uuid = ?", claims.UserUUID).First(&user).Error; dbErr != nil {
utils.JsonResponse(w, http.StatusNotFound, false, "用户不存在", nil)
return
}
utils.JsonResponse(w, http.StatusOK, true, "ok", map[string]interface{}{
"uuid": user.UUID,
"username": user.Username,
"role": user.Role,
"created_at": user.CreatedAt,
"username": claims.Username,
})
}
@@ -98,21 +81,42 @@ func UserPasswordUpdateHandler(w http.ResponseWriter, r *http.Request) {
return
}
// 确认是管理员
if !claims.IsAdmin {
utils.JsonResponse(w, http.StatusForbidden, false, "权限不足", nil)
return
}
// 获取数据库连接
db, err := database.GetDB()
if err != nil {
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
return
}
// 查询当前用户
var user models.User
if dbErr := db.Where("uuid = ?", claims.UserUUID).First(&user).Error; dbErr != nil {
utils.JsonResponse(w, http.StatusNotFound, false, "用户不存在", nil)
// 通过前缀匹配一次性获取所有管理员相关设置
var adminSettings []models.Settings
if err = db.Where("name LIKE ?", "admin_%").Find(&adminSettings).Error; err != nil {
utils.JsonResponse(w, http.StatusInternalServerError, false, "获取管理员设置失败", nil)
return
}
// 校验旧密码(使用盐值验证)
if !utils.VerifyPasswordWithSalt(body.OldPassword, user.PasswordSalt, user.Password) {
// 将设置转换为map便于查找
settingsMap := make(map[string]string)
for _, setting := range adminSettings {
settingsMap[setting.Name] = setting.Value
}
// 检查必要的设置是否存在
adminPassword, hasPassword := settingsMap["admin_password"]
adminPasswordSalt, hasSalt := settingsMap["admin_password_salt"]
if !hasPassword || !hasSalt {
utils.JsonResponse(w, http.StatusInternalServerError, false, "管理员密码设置不完整", nil)
return
}
// 校验旧密码
if !utils.VerifyPasswordWithSalt(body.OldPassword, adminPasswordSalt, adminPassword) {
utils.JsonResponse(w, http.StatusUnauthorized, false, "旧密码不正确", nil)
return
}
@@ -120,41 +124,34 @@ func UserPasswordUpdateHandler(w http.ResponseWriter, r *http.Request) {
// 生成新的密码盐值
newSalt, err := utils.GenerateRandomSalt()
if err != nil {
// 添加详细错误日志
fmt.Printf("生成密码盐失败: %v\n", err)
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成密码盐失败", nil)
return
}
fmt.Printf("成功生成新盐值,长度: %d\n", len(newSalt))
// 使用新盐值生成密码哈希
hash, err := utils.HashPasswordWithSalt(body.NewPassword, newSalt)
// 生成密码哈希
newPasswordHash, err := utils.HashPasswordWithSalt(body.NewPassword, newSalt)
if err != nil {
// 添加详细错误日志
fmt.Printf("生成密码哈希失败: %v, 密码长度: %d, 盐值长度: %d\n", err, len(body.NewPassword), len(newSalt))
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成密码哈希失败", nil)
return
}
fmt.Printf("成功生成密码哈希,长度: %d\n", len(hash))
// 更新密码和盐值
if dbErr := db.Model(&models.User{}).Where("uuid = ?", claims.UserUUID).Updates(map[string]interface{}{
"password": hash,
"password_salt": newSalt,
}).Error; dbErr != nil {
// 更新settings中的管理员密码和盐值
if err = db.Model(&models.Settings{}).Where("name = ?", "admin_password").Update("value", newPasswordHash).Error; err != nil {
utils.JsonResponse(w, http.StatusInternalServerError, false, "更新密码失败", nil)
return
}
// 重新查询用户信息(包含新密码)
var updatedUser models.User
if dbErr := db.Where("uuid = ?", claims.UserUUID).First(&updatedUser).Error; dbErr != nil {
utils.JsonResponse(w, http.StatusInternalServerError, false, "查询用户信息失败", nil)
if err = db.Model(&models.Settings{}).Where("name = ?", "admin_password_salt").Update("value", newSalt).Error; err != nil {
utils.JsonResponse(w, http.StatusInternalServerError, false, "更新密码盐值失败", nil)
return
}
// 重新生成JWT令牌包含新的密码哈希摘要
newToken, err := generateJWTToken(updatedUser)
adminUser := models.User{
Username: claims.Username,
Password: newPasswordHash,
PasswordSalt: newSalt,
}
newToken, err := generateJWTTokenForAdmin(adminUser)
if err != nil {
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成新令牌失败", nil)
return
@@ -210,19 +207,45 @@ func UserProfileUpdateHandler(w http.ResponseWriter, r *http.Request) {
return
}
// 检查唯一性排除当前用户UUID
var cnt int64
if dbErr := db.Model(&models.User{}).Where("username = ? AND uuid <> ?", username, claims.UserUUID).Count(&cnt).Error; dbErr != nil {
utils.JsonResponse(w, http.StatusInternalServerError, false, "检查用户名唯一性失败", nil)
return
}
if cnt > 0 {
utils.JsonResponse(w, http.StatusBadRequest, false, "用户名已存在,请更换", nil)
// 确认当前用户是管理员
if !claims.IsAdmin {
utils.JsonResponse(w, http.StatusForbidden, false, "权限不足", nil)
return
}
// 如果未变化则直接返回成功(无需校验旧密码)
if strings.EqualFold(username, claims.Username) {
// 获取所有管理员相关设置
var adminSettings []models.Settings
if dbErr := db.Where("name LIKE ?", "admin_%").Find(&adminSettings).Error; dbErr != nil {
utils.JsonResponse(w, http.StatusInternalServerError, false, "获取管理员设置失败", nil)
return
}
// 转换为map便于查找
settingsMap := make(map[string]string)
for _, setting := range adminSettings {
settingsMap[setting.Name] = setting.Value
}
adminUsername, exists := settingsMap["admin_username"]
if !exists {
utils.JsonResponse(w, http.StatusInternalServerError, false, "管理员用户名设置不存在", nil)
return
}
adminPassword, exists := settingsMap["admin_password"]
if !exists {
utils.JsonResponse(w, http.StatusInternalServerError, false, "管理员密码设置不存在", nil)
return
}
adminPasswordSalt, exists := settingsMap["admin_password_salt"]
if !exists {
utils.JsonResponse(w, http.StatusInternalServerError, false, "管理员密码盐值设置不存在", nil)
return
}
// 如果用户名未变化则直接返回成功(无需校验旧密码)
if strings.EqualFold(username, adminUsername) {
utils.JsonResponse(w, http.StatusOK, true, "保存成功", map[string]interface{}{
"username": username,
})
@@ -234,28 +257,27 @@ func UserProfileUpdateHandler(w http.ResponseWriter, r *http.Request) {
utils.JsonResponse(w, http.StatusBadRequest, false, "修改用户名需要提供当前密码", nil)
return
}
// 查询当前用户并校验旧密码
var user models.User
if dbErr := db.Where("uuid = ?", claims.UserUUID).First(&user).Error; dbErr != nil {
utils.JsonResponse(w, http.StatusNotFound, false, "用户不存在", nil)
return
}
// 使用盐值验证当前密码
if !utils.VerifyPasswordWithSalt(body.OldPassword, user.PasswordSalt, user.Password) {
if !utils.VerifyPasswordWithSalt(body.OldPassword, adminPasswordSalt, adminPassword) {
utils.JsonResponse(w, http.StatusUnauthorized, false, "当前密码不正确", nil)
return
}
// 执行更新
if dbErr := db.Model(&models.User{}).Where("uuid = ?", claims.UserUUID).Update("username", username).Error; dbErr != nil {
utils.JsonResponse(w, http.StatusInternalServerError, false, "更新用户名失败", nil)
// 更新管理员用户名设置
if dbErr := db.Model(&models.Settings{}).Where("name = ?", "admin_username").Update("value", username).Error; dbErr != nil {
utils.JsonResponse(w, http.StatusInternalServerError, false, "更新管理员用户名失败", nil)
return
}
// 重新签发JWT并写入Cookie
// 使用完整的用户信息(包含密码)来生成JWT令牌
user.Username = username // 更新用户名
token, err := generateJWTToken(user)
// 创建虚拟用户对象用于生成JWT令牌
adminUser := models.User{
Username: username, // 使用新的用户名
Password: adminPassword,
PasswordSalt: adminPasswordSalt,
}
token, err := generateJWTTokenForAdmin(adminUser)
if err != nil {
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成新令牌失败", nil)
return

View File

@@ -1,5 +0,0 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
#HttpOnly_localhost FALSE / TRUE 1761422606 csrf_token QLYaH1VddKCyAFgijZ80OYxzDht7zVLPbXH-rprEXvM=

View File

@@ -1,54 +0,0 @@
package database
import (
"networkDev/models"
"networkDev/utils"
"github.com/sirupsen/logrus"
)
// SeedDefaultAdmin 初始化默认管理员账号
// - 如果已存在任何管理员用户role=0则跳过
// - 如不存在,则创建用户名为 admin、密码为 admin123以 bcrypt 哈希存储)、角色 Role=0 的管理员
// - 根据需求:默认 admin 用户的 ID 固定为 10000
func SeedDefaultAdmin() error {
db, err := GetDB()
if err != nil {
return err
}
// 检查是否存在任何管理员用户role=0
var count int64
if dbErr := db.Model(&models.User{}).Where("role = ?", 0).Count(&count).Error; dbErr != nil {
return dbErr
}
if count > 0 {
logrus.Info("已存在管理员用户,跳过默认管理员创建")
return nil
}
// 生成密码盐值
salt, err := utils.GenerateRandomSalt()
if err != nil {
return err
}
// 使用盐值生成密码哈希(不存明文)
hash, err := utils.HashPasswordWithSalt("admin123", salt)
if err != nil {
return err
}
// 创建默认管理员ID和UUID将自动生成
admin := models.User{
Username: "admin",
Password: hash,
PasswordSalt: salt,
Role: 0, // 0=管理员
}
if err := db.Create(&admin).Error; err != nil {
return err
}
logrus.WithField("username", "admin").WithField("uuid", admin.UUID).Info("默认管理员创建成功")
return nil
}

View File

@@ -2,8 +2,10 @@ package database
import (
"networkDev/models"
"networkDev/utils"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
// SeedDefaultSettings 初始化默认系统设置
@@ -60,7 +62,23 @@ func SeedDefaultSettings() error {
{
Name: "maintenance_mode",
Value: "0",
Description: "系统开关0=开启系统1=关闭系统",
Description: "维护模式0=关闭维护模式1=开启维护模式",
},
// ===== 管理员账号相关默认项 =====
{
Name: "admin_username",
Value: "admin",
Description: "管理员用户名",
},
{
Name: "admin_password",
Value: "",
Description: "管理员密码哈希值",
},
{
Name: "admin_password_salt",
Value: "",
Description: "管理员密码加密盐值",
},
// ===== 页脚与备案相关默认项 =====
{
@@ -106,6 +124,55 @@ func SeedDefaultSettings() error {
}
}
// 初始化默认管理员账号(如果密码为空)
if err := initDefaultAdmin(db); err != nil {
return err
}
logrus.Info("默认系统设置初始化完成")
return nil
}
// initDefaultAdmin 初始化默认管理员账号
// 如果admin_password为空则生成默认密码admin123的哈希值
func initDefaultAdmin(db *gorm.DB) error {
var passwordSetting models.Settings
if err := db.Where("name = ?", "admin_password").First(&passwordSetting).Error; err != nil {
logrus.WithError(err).Error("获取管理员密码设置失败")
return err
}
// 如果密码已设置,跳过初始化
if passwordSetting.Value != "" {
logrus.Info("管理员密码已设置,跳过默认密码初始化")
return nil
}
// 生成密码盐值
salt, err := utils.GenerateRandomSalt()
if err != nil {
logrus.WithError(err).Error("生成密码盐值失败")
return err
}
// 使用盐值生成密码哈希默认密码admin123
hash, err := utils.HashPasswordWithSalt("admin123", salt)
if err != nil {
logrus.WithError(err).Error("生成密码哈希失败")
return err
}
// 更新密码和盐值
if err := db.Model(&models.Settings{}).Where("name = ?", "admin_password").Update("value", hash).Error; err != nil {
logrus.WithError(err).Error("更新管理员密码失败")
return err
}
if err := db.Model(&models.Settings{}).Where("name = ?", "admin_password_salt").Update("value", salt).Error; err != nil {
logrus.WithError(err).Error("更新管理员密码盐值失败")
return err
}
logrus.Info("默认管理员账号初始化完成,用户名: admin, 密码: admin123")
return nil
}

View File

@@ -10,13 +10,13 @@ import (
// User 用户表模型
// 说明PasswordSalt 使用 32 字节随机盐(以 16 进制存储为 64 个字符),因此列长度设置为 64
// 注意此表只存储普通用户管理员账号存储在settings表中
type User struct {
ID uint `gorm:"primaryKey;comment:用户ID自增主键"`
UUID string `gorm:"uniqueIndex;size:36;not null;comment:用户的唯一标识符" json:"uuid"`
Username string `gorm:"uniqueIndex;size:64;not null;comment:用户名,唯一索引"`
Password string `gorm:"size:255;not null;comment:密码哈希值"`
PasswordSalt string `gorm:"size:64;not null;comment:密码加密盐值"`
Role int `gorm:"not null;comment:用户角色0=管理员1=普通用户"`
CreatedAt time.Time `gorm:"comment:创建时间"`
UpdatedAt time.Time `gorm:"comment:更新时间"`
}

View File

@@ -93,13 +93,12 @@ func (s *SettingsService) RefreshCache() {
s.loadAllSettings()
}
// GetSessionTimeout 获取会话超时时间(秒)
func (s *SettingsService) GetSessionTimeout() int {
return s.GetInt("session_timeout", 3600) // 默认1小时
}
// IsMaintenanceMode 检查系统是否关闭
// IsMaintenanceMode 检查是否开启维护模式
func (s *SettingsService) IsMaintenanceMode() bool {
return s.GetBool("maintenance_mode", false)
}

View File

@@ -65,10 +65,8 @@ func GetTemplateDataWithCSRF(r *http.Request, additionalData map[string]interfac
data["CSRFToken"] = GetCSRFTokenForTemplate(r)
// 合并额外数据
if additionalData != nil {
for key, value := range additionalData {
data[key] = value
}
for key, value := range additionalData {
data[key] = value
}
return data

View File

@@ -297,7 +297,7 @@ loadScript(layuijs, function () {
'site-description': '站点描述网站的简要描述用于SEO和搜索引擎结果展示',
'site-logo': '站点Logo网站的标志图片路径建议使用SVG格式',
// 系统配置 (settings.html)
'maintenance-mode': '系统关闭:开启后网站将进入维护模式,普通用户无法访问',
'maintenance-mode': '维护模式:开启后网站将进入维护模式,普通用户无法访问',
'default-user-role': '默认角色新注册用户的默认权限级别0为管理员1为普通成员',
'session-timeout': '会话超时:用户登录会话的有效时间,单位为秒,超时后需要重新登录',
// 页脚与备案信息 (settings.html)

View File

@@ -221,8 +221,8 @@
cols: [[
{ field: 'id', title: 'ID', width: 80, sort: true },
{ field: 'app_name', title: '应用名称', minWidth: 150 },
{ field: 'uuid', title: 'UUID', minWidth: 335 },
{ field: 'api_type_name', title: '接口类型', minWidth: 120 },
{ field: 'uuid', title: 'UUID', minWidth: 335 },
{
field: 'status_name',
title: '状态',

View File

@@ -40,10 +40,10 @@
<div class="layui-card-body">
<form class="layui-form" id="systemForm">
<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>
<div class="layui-input-block">
<div style="display: flex; align-items: center; justify-content: flex-start; gap: 10px;">
<input type="checkbox" name="maintenance_mode" lay-skin="switch" lay-text="关闭系统|开启系统" title="关闭系统|开启系统">
<input type="checkbox" name="maintenance_mode" lay-skin="switch" lay-text="开启|关闭" title="开启|关闭">
</div>
</div>
</div>

View File

@@ -1,51 +1,14 @@
{{ define "user.html" }}
<section>
<h2>个人资料</h2>
<h2>账户管理</h2>
<div class="layui-tab layui-tab-brief" lay-filter="userTabs" style="margin-top: 16px;">
<ul class="layui-tab-title">
<li class="layui-this">个人资料</li>
<li>修改密码</li>
<li>修改用户名</li>d
<li class="layui-this">修改密码</li>
<li>修改用户名</li>
</ul>
<div class="layui-tab-content">
<!-- 个人资料模块 -->
<div class="layui-tab-item layui-show">
<div class="layui-card" style="margin-top: 16px;">
<div class="layui-card-header">个人资料</div>
<div class="layui-card-body">
<form class="layui-form" id="profileForm" lay-filter="profileForm">
<div class="layui-form-item">
<label class="layui-form-label">UUID</label>
<div class="layui-input-block">
<input type="text" name="uuid" disabled readonly class="layui-input readonly-field"
style="font-family: monospace; font-size: 12px;" />
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">用户组</label>
<div class="layui-input-block">
<input type="text" name="role" disabled readonly class="layui-input readonly-field" />
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">用户名</label>
<div class="layui-input-block">
<input type="text" name="username" disabled readonly class="layui-input readonly-field" />
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">创建时间</label>
<div class="layui-input-block">
<input type="text" name="created_at" disabled readonly class="layui-input readonly-field" />
</div>
</div>
</form>
</div>
</div>
</div>
<!-- 修改密码模块 -->
<div class="layui-tab-item">
<div class="layui-tab-item layui-show">
<div class="layui-card" style="margin-top: 16px;">
<div class="layui-card-header">修改密码</div>
<div class="layui-card-body">
@@ -58,7 +21,7 @@
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">新密码</label>
<label class="layui-form-label">密码</label>
<div class="layui-input-block">
<input type="password" name="new_password" placeholder="请输入新密码至少6位" autocomplete="off"
class="layui-input" lay-verify="required" />
@@ -166,31 +129,22 @@
const element = layui.element
// 全局变量
let userProfile = null
let currentUsername = null
// 加载个人资料
const loadProfile = async () => {
// 获取当前用户名
const getCurrentUsername = async () => {
try {
const res = await fetch('/admin/api/user/profile')
const data = await res.json()
const ok = (data.success === true) || (data.code === 0)
if (!ok) throw new Error(data.message || data.msg || '加载失败')
userProfile = data.data || {}
// 填充个人资料表单
const profileData = {
...userProfile,
role: roleToText(userProfile.role),
created_at: formatTime(userProfile.created_at)
}
form.val('profileForm', profileData)
if (!ok) throw new Error(data.message || data.msg || '获取用户信息失败')
currentUsername = data.data.username
// 填充用户名修改表单的当前用户名
form.val('usernameForm', { current_username: userProfile.username })
form.val('usernameForm', { current_username: currentUsername })
} catch (e) {
layer.msg(e.message || '加载个人资料失败', { icon: 2 })
layer.msg(e.message || '获取用户信息失败', { icon: 2 })
}
}
@@ -273,7 +227,7 @@
return { ok: false, msg: '请填写新用户名和当前密码' }
}
if (new_username === userProfile?.username) {
if (new_username === currentUsername) {
return { ok: false, msg: '新用户名不能与当前用户名相同' }
}
@@ -307,14 +261,14 @@
layer.msg('用户名修改成功', { icon: 1 })
// 重新加载个人资料
await loadProfile()
// 重新获取当前用户名
await getCurrentUsername()
// 清空表单(不显示重置提示)
form.val('usernameForm', {
new_username: '',
password: '',
current_username: userProfile?.username || ''
current_username: currentUsername || ''
})
} catch (e) {
@@ -328,7 +282,7 @@
form.val('usernameForm', {
new_username: '',
password: '',
current_username: userProfile?.username || ''
current_username: currentUsername || ''
})
layer.msg('表单已重置', { icon: 1 })
}
@@ -348,7 +302,7 @@
document.getElementById('resetUsernameBtn')?.addEventListener('click', UsernameModule.reset)
// 初始化加载
loadProfile()
getCurrentUsername()
})
})
})()