mirror of
https://github.com/skyle1995/NetworkAuth.git
synced 2026-05-25 02:24:05 +08:00
更新底层架构
This commit is contained in:
@@ -3,9 +3,9 @@ package admin
|
||||
import (
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"networkDev/controllers"
|
||||
"networkDev/models"
|
||||
"networkDev/utils/encrypt"
|
||||
"NetworkAuth/controllers"
|
||||
"NetworkAuth/models"
|
||||
"NetworkAuth/utils/encrypt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"NetworkAuth/controllers"
|
||||
"NetworkAuth/models"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"networkDev/controllers"
|
||||
"networkDev/models"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -247,7 +247,7 @@ func AppResetSecretHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
logrus.WithField("app_uuid", app.UUID).Info("Successfully reset app secret")
|
||||
logrus.WithField("app_uuid", app.UUID).Debug("Successfully reset app secret")
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
@@ -295,7 +295,7 @@ func AppCreateHandler(c *gin.Context) {
|
||||
"download_type": req.DownloadType,
|
||||
"download_url": req.DownloadURL,
|
||||
"force_update": req.ForceUpdate,
|
||||
}).Info("Received app create request")
|
||||
}).Debug("Received app create request")
|
||||
|
||||
// 创建应用
|
||||
app := models.App{
|
||||
@@ -344,8 +344,9 @@ func AppCreateHandler(c *gin.Context) {
|
||||
tx.Rollback()
|
||||
logrus.WithError(err).Error("Failed to create app")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 1,
|
||||
"msg": "创建应用失败",
|
||||
"code": 1,
|
||||
"msg": "创建应用失败: " + err.Error(),
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -384,7 +385,7 @@ func AppCreateHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
logrus.WithField("app_uuid", app.UUID).Info("Successfully created app with default APIs")
|
||||
logrus.WithField("app_uuid", app.UUID).Debug("Successfully created app with default APIs")
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
@@ -460,7 +461,7 @@ func AppUpdateHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
logrus.WithField("app_id", app.ID).Info("Successfully updated app")
|
||||
logrus.WithField("app_id", app.ID).Debug("Successfully updated app")
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
@@ -557,7 +558,7 @@ func AppDeleteHandler(c *gin.Context) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"app_id": app.ID,
|
||||
"app_uuid": app.UUID,
|
||||
}).Info("Successfully deleted app and related APIs")
|
||||
}).Debug("Successfully deleted app and related APIs")
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
@@ -629,7 +630,7 @@ func AppUpdateAppDataHandler(c *gin.Context) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"app_uuid": req.UUID,
|
||||
"app_name": app.Name,
|
||||
}).Info("App data updated successfully")
|
||||
}).Debug("App data updated successfully")
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
@@ -701,7 +702,7 @@ func AppUpdateAnnouncementHandler(c *gin.Context) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"app_uuid": req.UUID,
|
||||
"app_name": app.Name,
|
||||
}).Info("App announcement updated successfully")
|
||||
}).Debug("App announcement updated successfully")
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
@@ -870,7 +871,7 @@ func AppUpdateMultiConfigHandler(c *gin.Context) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"app_uuid": req.UUID,
|
||||
"app_name": app.Name,
|
||||
}).Info("App multi config updated successfully")
|
||||
}).Debug("App multi config updated successfully")
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
@@ -1023,7 +1024,7 @@ func AppUpdateBindConfigHandler(c *gin.Context) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"app_uuid": req.UUID,
|
||||
"app_name": app.Name,
|
||||
}).Info("App bind config updated successfully")
|
||||
}).Debug("App bind config updated successfully")
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
@@ -1161,7 +1162,7 @@ func AppUpdateRegisterConfigHandler(c *gin.Context) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"app_uuid": req.UUID,
|
||||
"app_name": app.Name,
|
||||
}).Info("App register config updated successfully")
|
||||
}).Debug("App register config updated successfully")
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
@@ -1265,7 +1266,7 @@ func AppsBatchDeleteHandler(c *gin.Context) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"app_ids": req.IDs,
|
||||
"app_uuids": appUUIDs,
|
||||
}).Info("Successfully batch deleted apps and related APIs")
|
||||
}).Debug("Successfully batch deleted apps and related APIs")
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"NetworkAuth/controllers"
|
||||
"NetworkAuth/database"
|
||||
"NetworkAuth/models"
|
||||
"NetworkAuth/services"
|
||||
"NetworkAuth/utils"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"networkDev/controllers"
|
||||
"networkDev/database"
|
||||
"networkDev/models"
|
||||
"networkDev/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/spf13/viper"
|
||||
@@ -40,9 +40,9 @@ func LoginPageHandler(c *gin.Context) {
|
||||
|
||||
// 获取或生成CSRF令牌
|
||||
var token string
|
||||
if existingToken := utils.GetCSRFTokenFromCookie(c); existingToken != "" {
|
||||
// 重用现有的Cookie令牌
|
||||
token = existingToken
|
||||
// 尝试从Cookie获取
|
||||
if cookie, err := c.Cookie(CSRFCookieName); err == nil && cookie != "" {
|
||||
token = cookie
|
||||
} else {
|
||||
// 生成新的CSRF令牌并设置到Cookie
|
||||
newToken, err := utils.GenerateCSRFToken()
|
||||
@@ -53,21 +53,14 @@ func LoginPageHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
token = newToken
|
||||
utils.SetCSRFToken(c, token)
|
||||
setCSRFToken(c, token)
|
||||
}
|
||||
|
||||
// 准备模板数据
|
||||
extraData := gin.H{
|
||||
"Title": "管理员登录",
|
||||
}
|
||||
data := authBaseController.GetDefaultTemplateData()
|
||||
data["Title"] = "管理员登录"
|
||||
data["CSRFToken"] = token
|
||||
|
||||
// 合并额外数据
|
||||
for key, value := range extraData {
|
||||
data[key] = value
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "login.html", data)
|
||||
}
|
||||
|
||||
@@ -76,21 +69,30 @@ func LoginPageHandler(c *gin.Context) {
|
||||
// ============================================================================
|
||||
|
||||
// LoginHandler 管理员登录接口
|
||||
// - 接收JSON: {username, password}
|
||||
// - 接收JSON: {username, password, captcha, csrf_token}
|
||||
// - 验证CSRF令牌
|
||||
// - 验证验证码
|
||||
// - 验证用户存在与密码正确性
|
||||
// - 仅允许 Role=0 的管理员登录
|
||||
// - 成功后设置简单的会话Cookie(后续可切换为JWT或更完善的Session)
|
||||
// - 仅允许管理员登录
|
||||
// - 成功后设置JWT Cookie
|
||||
func LoginHandler(c *gin.Context) {
|
||||
var body struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Captcha string `json:"captcha"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Captcha string `json:"captcha"`
|
||||
CSRFToken string `json:"csrf_token"`
|
||||
}
|
||||
|
||||
if !authBaseController.BindJSON(c, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 验证CSRF令牌 (Gin 方式)
|
||||
if !validateCSRFToken(c, body.CSRFToken) {
|
||||
authBaseController.HandleValidationError(c, "CSRF令牌验证失败")
|
||||
return
|
||||
}
|
||||
|
||||
if !authBaseController.ValidateRequired(c, map[string]interface{}{
|
||||
"用户名": body.Username,
|
||||
"密码": body.Password,
|
||||
@@ -101,95 +103,133 @@ func LoginHandler(c *gin.Context) {
|
||||
|
||||
// 验证验证码
|
||||
if !VerifyCaptcha(c, body.Captcha) {
|
||||
recordLoginLog(c, body.Username, 0, "验证码错误")
|
||||
authBaseController.HandleValidationError(c, "验证码错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取数据库连接
|
||||
db, ok := authBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// 获取系统设置服务
|
||||
settingsService := services.GetSettingsService()
|
||||
adminUsername := settingsService.GetString("admin_username", "admin")
|
||||
adminPasswordHash := settingsService.GetString("admin_password", "")
|
||||
adminPasswordSalt := settingsService.GetString("admin_password_salt", "")
|
||||
|
||||
// 通过前缀匹配一次性获取所有管理员相关设置
|
||||
var adminSettings []models.Settings
|
||||
if err := db.Where("name LIKE ?", "admin_%").Find(&adminSettings).Error; err != nil {
|
||||
authBaseController.HandleValidationError(c, "用户不存在或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 将设置转换为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 {
|
||||
authBaseController.HandleValidationError(c, "用户不存在或密码错误")
|
||||
// 验证密码为空的情况(首次登录需要初始化)
|
||||
if adminPasswordHash == "" || adminPasswordSalt == "" {
|
||||
recordLoginLog(c, body.Username, 0, "管理员账号未初始化")
|
||||
authBaseController.HandleInternalError(c, "管理员账号未初始化,请联系系统管理员", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证用户名
|
||||
if body.Username != adminUsername {
|
||||
recordLoginLog(c, body.Username, 0, "用户名错误")
|
||||
authBaseController.HandleValidationError(c, "用户不存在或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证密码为空的情况(首次登录需要初始化)
|
||||
if adminPassword == "" || adminPasswordSalt == "" {
|
||||
authBaseController.HandleInternalError(c, "管理员账号未初始化,请联系系统管理员", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 使用盐值验证密码
|
||||
if !utils.VerifyPasswordWithSalt(body.Password, adminPasswordSalt, adminPassword) {
|
||||
// 验证密码(使用盐值校验)
|
||||
if !utils.VerifyPasswordWithSalt(body.Password, adminPasswordSalt, adminPasswordHash) {
|
||||
recordLoginLog(c, body.Username, 0, "密码错误")
|
||||
authBaseController.HandleValidationError(c, "用户不存在或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 创建虚拟用户对象用于生成JWT令牌
|
||||
adminUser := models.User{
|
||||
Username: adminUsername,
|
||||
Password: adminPassword,
|
||||
PasswordSalt: adminPasswordSalt,
|
||||
}
|
||||
|
||||
// 生成JWT令牌
|
||||
token, err := generateJWTTokenForAdmin(adminUser)
|
||||
token, err := generateJWTTokenForAdmin(body.Username, adminPasswordHash)
|
||||
if err != nil {
|
||||
recordLoginLog(c, body.Username, 0, "生成令牌失败")
|
||||
authBaseController.HandleInternalError(c, "生成令牌失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置JWT Cookie(使用安全配置)
|
||||
cookie := utils.CreateSecureCookie("admin_session", token, utils.GetDefaultCookieMaxAge())
|
||||
// 设置JWT Cookie(HttpOnly,安全)
|
||||
// 使用系统配置的Cookie参数
|
||||
secure, sameSite, domain, maxAge := settingsService.GetCookieConfig()
|
||||
cookie := utils.CreateSecureCookie("admin_session", token, maxAge, domain, secure, sameSite)
|
||||
c.SetCookie(cookie.Name, cookie.Value, cookie.MaxAge, cookie.Path, cookie.Domain, cookie.Secure, cookie.HttpOnly)
|
||||
|
||||
recordLoginLog(c, body.Username, 1, "登录成功")
|
||||
authBaseController.HandleSuccess(c, "登录成功", gin.H{
|
||||
"redirect": "/admin",
|
||||
})
|
||||
}
|
||||
|
||||
// recordLoginLog 记录登录日志
|
||||
// status: 1-成功, 0-失败
|
||||
func recordLoginLog(c *gin.Context, username string, status int, message string) {
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
// 记录日志失败不应影响主流程,但可以记录到系统日志
|
||||
fmt.Printf("Failed to connect to database for login log: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
log := models.LoginLog{
|
||||
Type: "admin",
|
||||
Username: username,
|
||||
IP: c.ClientIP(),
|
||||
Status: status,
|
||||
Message: message,
|
||||
UserAgent: c.Request.UserAgent(),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := db.Create(&log).Error; err != nil {
|
||||
fmt.Printf("Failed to create login log: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// LogoutHandler 管理员登出
|
||||
// - 清理JWT Cookie
|
||||
// - 清理JWT Cookie会话
|
||||
// - 确保令牌完全失效
|
||||
func LogoutHandler(c *gin.Context) {
|
||||
// 清理JWT Cookie
|
||||
clearInvalidJWTCookie(c)
|
||||
|
||||
// 可选:将JWT令牌加入黑名单(需要Redis或数据库支持)
|
||||
// 这里可以实现JWT黑名单机制
|
||||
|
||||
authBaseController.HandleSuccess(c, "已退出登录", gin.H{
|
||||
"redirect": "/admin/login",
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CSRF 相关辅助函数
|
||||
// ============================================================================
|
||||
|
||||
const (
|
||||
CSRFCookieName = "csrf_token"
|
||||
CSRFHeaderName = "X-CSRF-Token"
|
||||
CSRFFormField = "csrf_token"
|
||||
)
|
||||
|
||||
// setCSRFToken 设置CSRF令牌到Cookie (Gin适配)
|
||||
func setCSRFToken(c *gin.Context, token string) {
|
||||
c.SetCookie(CSRFCookieName, token, 3600*24, "/", "", false, false)
|
||||
c.Header(CSRFHeaderName, token)
|
||||
}
|
||||
|
||||
// validateCSRFToken 验证CSRF令牌 (Gin适配)
|
||||
func validateCSRFToken(c *gin.Context, requestToken string) bool {
|
||||
// 获取Cookie中的令牌
|
||||
cookie, err := c.Cookie(CSRFCookieName)
|
||||
if err != nil || cookie == "" {
|
||||
return false
|
||||
}
|
||||
cookieToken := cookie
|
||||
|
||||
// 如果请求体中没有提供token,尝试从Header获取
|
||||
if requestToken == "" {
|
||||
requestToken = c.GetHeader(CSRFHeaderName)
|
||||
}
|
||||
|
||||
if requestToken == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// 使用常量时间比较
|
||||
return strings.Compare(cookieToken, requestToken) == 0
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 辅助函数
|
||||
// ============================================================================
|
||||
@@ -198,14 +238,27 @@ func LogoutHandler(c *gin.Context) {
|
||||
// - 统一的Cookie清理函数,确保一致性
|
||||
// - 在JWT校验失败时自动调用,提升安全性和用户体验
|
||||
func clearInvalidJWTCookie(c *gin.Context) {
|
||||
cookie := utils.CreateExpiredCookie("admin_session")
|
||||
_, _, domain, _ := services.GetSettingsService().GetCookieConfig()
|
||||
cookie := utils.CreateExpiredCookie("admin_session", domain)
|
||||
c.SetCookie(cookie.Name, cookie.Value, cookie.MaxAge, cookie.Path, cookie.Domain, cookie.Secure, cookie.HttpOnly)
|
||||
}
|
||||
|
||||
// getJWTSecret 动态获取当前的JWT密钥
|
||||
// 修复安全漏洞:确保每次都从最新配置中获取密钥,而不是使用启动时的全局变量
|
||||
func getJWTSecret() []byte {
|
||||
return []byte(viper.GetString("security.jwt_secret"))
|
||||
// 1. 尝试从数据库设置获取
|
||||
settingsService := services.GetSettingsService()
|
||||
if secret := settingsService.GetJWTSecret(); secret != "" {
|
||||
return []byte(secret)
|
||||
}
|
||||
|
||||
// 2. 尝试从配置文件获取(兼容旧配置)
|
||||
if secret := viper.GetString("security.jwt_secret"); secret != "" {
|
||||
return []byte(secret)
|
||||
}
|
||||
|
||||
// 3. 使用默认不安全密钥(仅开发环境)
|
||||
return []byte("default-insecure-jwt-secret")
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -215,27 +268,40 @@ func getJWTSecret() []byte {
|
||||
// JWTClaims JWT载荷结构体
|
||||
type JWTClaims struct {
|
||||
Username string `json:"username"`
|
||||
UUID string `json:"uuid"` // 添加虚拟角色UUID
|
||||
Role int `json:"role"` // 添加虚拟角色
|
||||
PasswordHash string `json:"password_hash"` // 密码哈希摘要,用于验证密码是否被修改
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// generateJWTTokenForAdmin 生成管理员JWT令牌
|
||||
// - 包含管理员UUID、用户名信息
|
||||
// - 设置24小时过期时间
|
||||
// - 包含管理员用户名信息和密码哈希
|
||||
// - 设置过期时间
|
||||
// - 使用HMAC-SHA256签名
|
||||
func generateJWTTokenForAdmin(adminUser models.User) (string, error) {
|
||||
func generateJWTTokenForAdmin(username, passwordHash string) (string, error) {
|
||||
// 生成密码哈希摘要(使用SHA256)
|
||||
passwordHashDigest := utils.GenerateSHA256Hash(adminUser.Password)
|
||||
// 注意:传入的 passwordHash 已经是数据库存的 Hash,这里我们再次 Hash 还是直接用?
|
||||
// atomicLibrary 的实现是: utils.GenerateSHA256Hash(adminUser.Password)
|
||||
// 这里我们直接用数据库里的 Hash 值作为 Token 的一部分即可,或者对它再 Hash 一次。
|
||||
// 为了与 validateAdminPasswordHash 对应,我们需要知道验证时怎么比对。
|
||||
// validateAdminPasswordHash: currentPasswordHash := utils.GenerateSHA256Hash(adminPassword.Value)
|
||||
// 所以这里也应该对数据库里的值进行 Hash。
|
||||
passwordHashDigest := utils.GenerateSHA256Hash(passwordHash)
|
||||
|
||||
// 获取虚拟管理员UUID (NetworkAuth 项目默认为 admin-uuid-001)
|
||||
adminUUID := services.GetSettingsService().GetString("admin_uuid", "admin-uuid-001")
|
||||
|
||||
claims := JWTClaims{
|
||||
Username: adminUser.Username,
|
||||
Username: username,
|
||||
UUID: adminUUID,
|
||||
Role: 0, // 0表示超级管理员
|
||||
PasswordHash: passwordHashDigest, // 包含密码哈希摘要
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(services.GetSettingsService().GetJWTExpire()) * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: "凌动技术",
|
||||
Subject: adminUser.Username,
|
||||
Issuer: "NetworkAuth",
|
||||
Subject: username,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -303,7 +369,7 @@ func validateAdminPasswordHash(claims *JWTClaims, c *gin.Context) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsAdminAuthenticated 判断管理员是否已认证(导出)
|
||||
// IsAdminAuthenticated 判断管理员是否已认证(Gin版本)
|
||||
// - 检查admin_session Cookie中的JWT令牌
|
||||
// - 验证令牌签名、过期时间和用户角色
|
||||
func IsAdminAuthenticated(c *gin.Context) bool {
|
||||
@@ -318,12 +384,48 @@ func IsAdminAuthenticated(c *gin.Context) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// 注释:由于这是管理员专用认证函数,不需要额外的角色验证
|
||||
|
||||
// 验证密码哈希
|
||||
return validateAdminPasswordHash(claims, c)
|
||||
}
|
||||
|
||||
// IsAdminAuthenticatedHttp 判断管理员是否已认证(HTTP兼容版本)
|
||||
// 保留此方法以兼容未迁移的 Handler
|
||||
func IsAdminAuthenticatedHttp(r *http.Request) bool {
|
||||
cookie, err := r.Cookie("admin_session")
|
||||
if err != nil || cookie.Value == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// 解析并验证JWT令牌
|
||||
claims, err := parseJWTToken(cookie.Value)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 注意:HTTP 版本无法方便地获取 ClientIP 用于日志,且无法使用 Gin Context 的功能
|
||||
// 这里仅做基本的 Token 验证。如果 Token 包含了 PasswordHash,这里也会解析出来。
|
||||
// 但验证 PasswordHash 需要 DB 访问。
|
||||
// 为了完整性,我们应该也验证 PasswordHash。
|
||||
// 这里的 ClientIP 只能从 r.RemoteAddr 获取。
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var adminPassword models.Settings
|
||||
if err := db.Where("name = ?", "admin_password").First(&adminPassword).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
currentPasswordHash := utils.GenerateSHA256Hash(adminPassword.Value)
|
||||
if claims.PasswordHash != currentPasswordHash {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// IsAdminAuthenticatedWithCleanup 带自动清理功能的JWT校验函数
|
||||
// - 当JWT校验失败时,自动清理失效的Cookie
|
||||
// - 适用于API接口等需要清理失效令牌的场景
|
||||
@@ -341,8 +443,6 @@ func IsAdminAuthenticatedWithCleanup(c *gin.Context) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// 注释:由于这是管理员专用认证函数,不需要额外的角色验证
|
||||
|
||||
// 验证密码哈希
|
||||
if !validateAdminPasswordHash(claims, c) {
|
||||
clearInvalidJWTCookie(c)
|
||||
@@ -352,23 +452,18 @@ func IsAdminAuthenticatedWithCleanup(c *gin.Context) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GetCurrentAdminUser 获取当前登录的管理员用户信息
|
||||
// - 从JWT令牌中提取用户信息
|
||||
// - 自动刷新接近过期的令牌(剩余时间少于6小时时刷新)
|
||||
// - 返回用户ID、用户名和角色
|
||||
func GetCurrentAdminUser(c *gin.Context) (*JWTClaims, error) {
|
||||
cookie, err := getJWTCookie(c)
|
||||
// GetCurrentAdminUser 获取当前登录的管理员用户信息 (HTTP 兼容版)
|
||||
func GetCurrentAdminUser(r *http.Request) (*JWTClaims, error) {
|
||||
cookie, err := r.Cookie("admin_session")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("未找到会话信息")
|
||||
}
|
||||
|
||||
claims, err := parseJWTToken(cookie)
|
||||
claims, err := parseJWTToken(cookie.Value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无效的会话信息")
|
||||
}
|
||||
|
||||
// 注释:由于这是管理员专用函数,不需要额外的角色验证
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
@@ -394,25 +489,56 @@ func GetCurrentAdminUserWithRefresh(c *gin.Context) (*JWTClaims, bool, error) {
|
||||
|
||||
// 检查是否需要刷新令牌
|
||||
refreshed := false
|
||||
refreshThreshold := time.Duration(viper.GetInt("security.jwt_refresh")) * time.Hour
|
||||
|
||||
// 动态获取刷新阈值:默认剩余时间少于6小时刷新
|
||||
refreshThresholdHours := services.GetSettingsService().GetJWTRefresh()
|
||||
if refreshThresholdHours <= 0 {
|
||||
refreshThresholdHours = 6 // 默认值
|
||||
}
|
||||
refreshThreshold := time.Duration(refreshThresholdHours) * time.Hour
|
||||
|
||||
// 动态获取JWT总有效期
|
||||
expireHours := services.GetSettingsService().GetJWTExpire()
|
||||
if expireHours <= 0 {
|
||||
expireHours = 24 // 默认值
|
||||
}
|
||||
|
||||
// 动态获取Cookie配置(用于更新Cookie过期时间)
|
||||
secure, sameSite, domain, maxAge := services.GetSettingsService().GetCookieConfig()
|
||||
|
||||
// 1. 默认情况下,每次请求都更新Cookie的过期时间(滑动过期)
|
||||
tokenToSet := cookie
|
||||
shouldUpdateCookie := true
|
||||
|
||||
// 2. 检查是否需要刷新JWT令牌(生成新的Token)
|
||||
if time.Until(claims.ExpiresAt.Time) < refreshThreshold {
|
||||
adminUser := models.User{
|
||||
Username: claims.Username,
|
||||
}
|
||||
newToken, err := generateJWTTokenForAdmin(adminUser)
|
||||
// 获取当前的 PasswordHash
|
||||
db, _ := database.GetDB()
|
||||
var adminPassword models.Settings
|
||||
db.Where("name = ?", "admin_password").First(&adminPassword)
|
||||
|
||||
// 使用新的有效期生成令牌
|
||||
newToken, err := generateJWTTokenForAdmin(claims.Username, adminPassword.Value)
|
||||
if err == nil {
|
||||
c.SetCookie("admin_session", newToken, utils.GetDefaultCookieMaxAge(), "/", "", false, true)
|
||||
tokenToSet = newToken
|
||||
refreshed = true
|
||||
|
||||
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(24 * time.Hour))
|
||||
// 更新当前claims的过期时间
|
||||
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Duration(expireHours) * time.Hour))
|
||||
claims.IssuedAt = jwt.NewNumericDate(time.Now())
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 执行Cookie更新
|
||||
if shouldUpdateCookie {
|
||||
cookieObj := utils.CreateSecureCookie("admin_session", tokenToSet, maxAge, domain, secure, sameSite)
|
||||
c.SetCookie(cookieObj.Name, cookieObj.Value, cookieObj.MaxAge, cookieObj.Path, cookieObj.Domain, cookieObj.Secure, cookieObj.HttpOnly)
|
||||
}
|
||||
|
||||
return claims, refreshed, nil
|
||||
}
|
||||
|
||||
// AdminAuthRequired 管理员认证拦截中间件
|
||||
// AdminAuthRequired 管理员认证拦截中间件 (Gin Middleware)
|
||||
// - 未登录:重定向到 /admin/login
|
||||
// - 已登录:自动刷新接近过期的令牌,然后放行到后续处理器
|
||||
func AdminAuthRequired() gin.HandlerFunc {
|
||||
@@ -424,8 +550,6 @@ func AdminAuthRequired() gin.HandlerFunc {
|
||||
clearInvalidJWTCookie(c)
|
||||
|
||||
// 中文注释:区分普通页面请求与AJAX/JSON请求
|
||||
// - 对 AJAX/JSON:直接返回 401 JSON,便于前端处理(如提示重新登录)
|
||||
// - 对普通页面:保持原有重定向到登录页
|
||||
accept := c.GetHeader("Accept")
|
||||
xrw := strings.ToLower(strings.TrimSpace(c.GetHeader("X-Requested-With")))
|
||||
if strings.Contains(accept, "application/json") || xrw == "xmlhttprequest" {
|
||||
@@ -448,6 +572,11 @@ func AdminAuthRequired() gin.HandlerFunc {
|
||||
_ = claims // 避免未使用变量警告
|
||||
}
|
||||
|
||||
// 将解析出的用户信息存入上下文,供后续处理使用
|
||||
c.Set("admin_uuid", claims.UUID)
|
||||
c.Set("admin_username", claims.Username)
|
||||
c.Set("admin_role", claims.Role)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,9 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"networkDev/controllers"
|
||||
"networkDev/middleware"
|
||||
"networkDev/utils"
|
||||
"NetworkAuth/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mojocn/base64Captcha"
|
||||
)
|
||||
|
||||
@@ -19,9 +17,6 @@ import (
|
||||
// 全局变量
|
||||
// ============================================================================
|
||||
|
||||
// 创建基础控制器实例
|
||||
var captchaBaseController = controllers.NewBaseController()
|
||||
|
||||
// 全局验证码存储器
|
||||
var store = base64Captcha.DefaultMemStore
|
||||
|
||||
@@ -49,7 +44,7 @@ func CaptchaHandler(c *gin.Context) {
|
||||
// 使用crypto/rand生成安全的随机数
|
||||
randomNum, err := secureRandomInt(3)
|
||||
if err != nil {
|
||||
captchaBaseController.HandleInternalError(c, "生成随机数失败", err)
|
||||
c.String(http.StatusInternalServerError, "生成随机数失败")
|
||||
return
|
||||
}
|
||||
captchaLength := 4 + randomNum // 4-6位随机长度
|
||||
@@ -62,37 +57,31 @@ func CaptchaHandler(c *gin.Context) {
|
||||
ShowLineOptions: 2 | 4,
|
||||
Length: captchaLength,
|
||||
Source: "ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789", // 混合大小写字母和数字,去除易混淆字符
|
||||
Fonts: []string{"wqy-microhei.ttc"},
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
captcha := base64Captcha.NewCaptcha(&driver, store)
|
||||
id, b64s, _, err := captcha.Generate()
|
||||
if err != nil {
|
||||
captchaBaseController.HandleInternalError(c, "生成验证码失败", err)
|
||||
c.String(http.StatusInternalServerError, "生成验证码失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 将验证码ID存储到session中(这里简化处理,实际项目中应该使用更安全的方式)
|
||||
// 设置cookie来存储验证码ID
|
||||
cookie := utils.CreateSecureCookie("captcha_id", id, 300) // 5分钟过期
|
||||
c.SetCookie(cookie.Name, cookie.Value, cookie.MaxAge, cookie.Path, cookie.Domain, cookie.Secure, cookie.HttpOnly)
|
||||
// 将验证码ID存储到Cookie中
|
||||
c.SetCookie("captcha_id", id, 300, "/", "", false, true)
|
||||
|
||||
// 解码base64图片数据并返回
|
||||
// 设置响应头
|
||||
c.Header("Content-Type", "image/png")
|
||||
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
c.Header("Pragma", "no-cache")
|
||||
c.Header("Expires", "0")
|
||||
|
||||
// 直接返回base64编码的图片数据,让浏览器解析
|
||||
// 但是我们需要返回实际的图片数据,所以需要解码base64
|
||||
|
||||
// 去掉data:image/png;base64,前缀
|
||||
b64s = strings.TrimPrefix(b64s, "data:image/png;base64,")
|
||||
|
||||
imgData, err := base64.StdEncoding.DecodeString(b64s)
|
||||
if err != nil {
|
||||
captchaBaseController.HandleInternalError(c, "解码验证码图片失败", err)
|
||||
c.String(http.StatusInternalServerError, "解码验证码图片失败")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -110,11 +99,7 @@ func VerifyCaptcha(c *gin.Context, captchaValue string) bool {
|
||||
|
||||
// 从cookie中获取验证码ID
|
||||
captchaId, err := c.Cookie("captcha_id")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if captchaId == "" {
|
||||
if err != nil || captchaId == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -125,7 +110,7 @@ func VerifyCaptcha(c *gin.Context, captchaValue string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// 如果原始值验证失败,尝试小写验证(因为显示的是大小写混合,但允许用户输入小写)
|
||||
// 如果原始值验证失败,尝试小写验证
|
||||
if store.Verify(captchaId, strings.ToLower(captchaValue), false) {
|
||||
// 验证成功后删除验证码
|
||||
store.Verify(captchaId, strings.ToLower(captchaValue), true)
|
||||
@@ -139,22 +124,3 @@ func VerifyCaptcha(c *gin.Context, captchaValue string) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// CaptchaAPIHandler 验证码API接口(可选,用于AJAX验证)
|
||||
// POST /admin/api/captcha/verify - 验证验证码
|
||||
func CaptchaAPIHandler(c *gin.Context) {
|
||||
var body struct {
|
||||
Captcha string `json:"captcha"`
|
||||
}
|
||||
if !captchaBaseController.BindJSON(c, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
isValid := VerifyCaptcha(c, body.Captcha)
|
||||
|
||||
if isValid {
|
||||
captchaBaseController.HandleSuccess(c, "验证码正确", nil)
|
||||
} else {
|
||||
captchaBaseController.HandleValidationError(c, "验证码错误")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"NetworkAuth/controllers"
|
||||
"NetworkAuth/models"
|
||||
"net/http"
|
||||
"networkDev/controllers"
|
||||
"networkDev/models"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -299,7 +299,7 @@ func FunctionDeleteHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
logrus.WithField("function_id", req.ID).Info("Successfully deleted function")
|
||||
logrus.WithField("function_id", req.ID).Debug("Successfully deleted function")
|
||||
|
||||
functionBaseController.HandleSuccess(c, "删除成功", nil)
|
||||
}
|
||||
@@ -331,7 +331,7 @@ func FunctionsBatchDeleteHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
logrus.WithField("function_ids", req.IDs).Info("Successfully batch deleted functions")
|
||||
logrus.WithField("function_ids", req.IDs).Debug("Successfully batch deleted functions")
|
||||
|
||||
functionBaseController.HandleSuccess(c, "批量删除成功", nil)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"NetworkAuth/constants"
|
||||
"NetworkAuth/controllers"
|
||||
"NetworkAuth/middleware"
|
||||
"NetworkAuth/models"
|
||||
"NetworkAuth/services"
|
||||
"NetworkAuth/utils"
|
||||
"NetworkAuth/utils/timeutil"
|
||||
"net/http"
|
||||
"networkDev/constants"
|
||||
"networkDev/controllers"
|
||||
"networkDev/middleware"
|
||||
"networkDev/models"
|
||||
"networkDev/services"
|
||||
"networkDev/utils"
|
||||
"networkDev/utils/timeutil"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/spf13/viper"
|
||||
@@ -83,11 +84,8 @@ func AdminLayoutHandler(c *gin.Context) {
|
||||
data["CSRFToken"] = token
|
||||
|
||||
// 从数据库读取站点标题,如果失败则使用默认值
|
||||
if db, ok := handlersBaseController.GetDB(c); ok {
|
||||
if siteTitle, err := services.FindSettingByName("site_title", db); err == nil && siteTitle != nil {
|
||||
data["Title"] = siteTitle.Value
|
||||
}
|
||||
}
|
||||
settingsSvc := services.GetSettingsService()
|
||||
data["Title"] = settingsSvc.GetString("site_title", "后台管理")
|
||||
|
||||
// 合并其他数据(如果有的话)
|
||||
extraData := gin.H{}
|
||||
@@ -192,3 +190,45 @@ func DashboardStatsHandler(c *gin.Context) {
|
||||
|
||||
handlersBaseController.HandleSuccess(c, "ok", data)
|
||||
}
|
||||
|
||||
// DashboardLoginLogsHandler 获取管理员最近登录日志
|
||||
func DashboardLoginLogsHandler(c *gin.Context) {
|
||||
db, ok := handlersBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取分页参数
|
||||
pageStr := c.DefaultQuery("page", "1")
|
||||
limitStr := c.DefaultQuery("limit", "10")
|
||||
page, _ := strconv.Atoi(pageStr)
|
||||
limit, _ := strconv.Atoi(limitStr)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if limit < 1 || limit > 100 {
|
||||
limit = 10
|
||||
}
|
||||
offset := (page - 1) * limit
|
||||
|
||||
var total int64
|
||||
// 当前模型的 LoginLog 本身就是专用于 admin 的登录日志模型(没有 type 字段),所以直接查询全部即可
|
||||
query := db.Model(&models.LoginLog{})
|
||||
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
handlersBaseController.HandleInternalError(c, "获取登录日志总数失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
var logs []models.LoginLog
|
||||
if err := query.Order("created_at desc").Offset(offset).Limit(limit).Find(&logs).Error; err != nil {
|
||||
handlersBaseController.HandleInternalError(c, "获取登录日志列表失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
data := gin.H{
|
||||
"total": total,
|
||||
"list": logs,
|
||||
}
|
||||
handlersBaseController.HandleSuccess(c, "获取登录日志成功", data)
|
||||
}
|
||||
|
||||
174
controllers/admin/login_log.go
Normal file
174
controllers/admin/login_log.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"NetworkAuth/controllers"
|
||||
"NetworkAuth/models"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// 全局变量
|
||||
// ============================================================================
|
||||
|
||||
var loginLogBaseController = controllers.NewBaseController()
|
||||
|
||||
// ============================================================================
|
||||
// 辅助函数
|
||||
// ============================================================================
|
||||
|
||||
// RecordLoginLog 记录登录日志
|
||||
func RecordLoginLog(c *gin.Context, username string, status int, message string) {
|
||||
db, ok := loginLogBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
log := models.LoginLog{
|
||||
Type: "admin",
|
||||
Username: username,
|
||||
IP: c.ClientIP(),
|
||||
Status: status,
|
||||
Message: message,
|
||||
UserAgent: c.Request.UserAgent(),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := db.Create(&log).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to create login log")
|
||||
}
|
||||
}
|
||||
|
||||
// LoginLogsFragmentHandler 登录日志页面片段处理器
|
||||
func LoginLogsFragmentHandler(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "login_logs.html", gin.H{
|
||||
"Title": "登录日志",
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API处理器
|
||||
// ============================================================================
|
||||
|
||||
// LoginLogsListHandler 登录日志列表API处理器
|
||||
func LoginLogsListHandler(c *gin.Context) {
|
||||
// 获取分页参数
|
||||
page, _ := strconv.Atoi(c.Query("page"))
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
limit, _ := strconv.Atoi(c.Query("limit"))
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
// 构建查询
|
||||
db, ok := loginLogBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var logs []models.LoginLog
|
||||
var total int64
|
||||
|
||||
// 兼容旧数据(Type为空)和新数据(Type=admin)
|
||||
query := db.Model(&models.LoginLog{}).Where("type = ? OR type = ? OR type IS NULL", "admin", "")
|
||||
|
||||
// 筛选条件:用户名
|
||||
if username := strings.TrimSpace(c.Query("username")); username != "" {
|
||||
query = query.Where("username = ?", username)
|
||||
}
|
||||
|
||||
// 筛选条件:IP
|
||||
if ip := strings.TrimSpace(c.Query("ip")); ip != "" {
|
||||
query = query.Where("ip = ?", ip)
|
||||
}
|
||||
|
||||
// 筛选条件:状态
|
||||
if statusStr := strings.TrimSpace(c.Query("status")); statusStr != "" {
|
||||
if status, err := strconv.Atoi(statusStr); err == nil {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
}
|
||||
|
||||
// 筛选条件:时间范围
|
||||
startTime := strings.TrimSpace(c.Query("start_time"))
|
||||
endTime := strings.TrimSpace(c.Query("end_time"))
|
||||
if startTime != "" && endTime != "" {
|
||||
query = query.Where("created_at BETWEEN ? AND ?", startTime, endTime)
|
||||
}
|
||||
|
||||
// 统计总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
loginLogBaseController.HandleInternalError(c, "获取日志总数失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 查询数据(时间倒序,从新到旧)
|
||||
offset := (page - 1) * limit
|
||||
if err := query.Order("created_at DESC").Limit(limit).Offset(offset).Find(&logs).Error; err != nil {
|
||||
loginLogBaseController.HandleInternalError(c, "获取日志列表失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换数据格式
|
||||
var list []map[string]interface{}
|
||||
for _, log := range logs {
|
||||
list = append(list, map[string]interface{}{
|
||||
"id": log.ID,
|
||||
"username": log.Username,
|
||||
"ip": log.IP,
|
||||
"status": log.Status,
|
||||
"message": log.Message,
|
||||
"user_agent": log.UserAgent,
|
||||
"created_at": log.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
loginLogBaseController.HandleSuccess(c, "ok", gin.H{
|
||||
"list": list,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
|
||||
// LoginLogsClearHandler 清空登录日志API处理器
|
||||
func LoginLogsClearHandler(c *gin.Context) {
|
||||
db, ok := loginLogBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 物理删除所有登录日志
|
||||
if err := db.Where("type = ?", "admin").Delete(&models.LoginLog{}).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to clear login logs")
|
||||
loginLogBaseController.HandleInternalError(c, "清空登录日志失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
// 由于 NetworkAuth 中没有 SystemAdminUser 全局变量,这里暂时使用 "admin"
|
||||
operator := "admin"
|
||||
// 尝试从上下文获取用户名(如果中间件设置了的话)
|
||||
// if user, exists := c.Get("username"); exists {
|
||||
// operator = user.(string)
|
||||
// }
|
||||
|
||||
log := models.OperationLog{
|
||||
OperationType: "清空登录日志",
|
||||
Operator: operator,
|
||||
OperatorUUID: "", // NetworkAuth 中暂时无法获取 UUID
|
||||
AppName: "-",
|
||||
ProductName: "-",
|
||||
TransactionID: "-",
|
||||
Details: "管理员清空了所有登录日志",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
db.Create(&log)
|
||||
|
||||
loginLogBaseController.HandleSuccess(c, "登录日志已清空", nil)
|
||||
}
|
||||
152
controllers/admin/operation_log.go
Normal file
152
controllers/admin/operation_log.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"NetworkAuth/controllers"
|
||||
"NetworkAuth/models"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// 全局变量
|
||||
// ============================================================================
|
||||
|
||||
var logBaseController = controllers.NewBaseController()
|
||||
|
||||
// ============================================================================
|
||||
// 页面处理器
|
||||
// ============================================================================
|
||||
|
||||
// LogsFragmentHandler 日志操作页面片段处理器
|
||||
func LogsFragmentHandler(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "operation_logs.html", gin.H{
|
||||
"Title": "操作日志",
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API处理器
|
||||
// ============================================================================
|
||||
|
||||
// LogsListHandler 日志列表API处理器
|
||||
func LogsListHandler(c *gin.Context) {
|
||||
// 获取分页参数
|
||||
page, _ := strconv.Atoi(c.Query("page"))
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
limit, _ := strconv.Atoi(c.Query("limit"))
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
// 获取搜索参数
|
||||
startTimeStr := strings.TrimSpace(c.Query("start_time"))
|
||||
endTimeStr := strings.TrimSpace(c.Query("end_time"))
|
||||
operationType := strings.TrimSpace(c.Query("operation_type"))
|
||||
operator := strings.TrimSpace(c.Query("operator"))
|
||||
transactionID := strings.TrimSpace(c.Query("transaction_id"))
|
||||
|
||||
// 构建查询
|
||||
db, ok := logBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var logs []models.OperationLog
|
||||
var total int64
|
||||
|
||||
query := db.Model(&models.OperationLog{})
|
||||
|
||||
// 筛选条件
|
||||
if operationType != "" {
|
||||
query = query.Where("operation_type = ?", operationType)
|
||||
}
|
||||
if operator != "" {
|
||||
// 支持按 UUID 或 用户名 筛选
|
||||
query = query.Where("operator_uuid = ? OR operator = ?", operator, operator)
|
||||
}
|
||||
if transactionID != "" {
|
||||
// 优化:使用精确匹配提升查询性能
|
||||
query = query.Where("transaction_id = ?", transactionID)
|
||||
}
|
||||
if startTimeStr != "" {
|
||||
if t, err := time.ParseInLocation("2006-01-02", startTimeStr, time.Local); err == nil {
|
||||
query = query.Where("created_at >= ?", t)
|
||||
} else if t, err := time.ParseInLocation("2006-01-02 15:04:05", startTimeStr, time.Local); err == nil {
|
||||
query = query.Where("created_at >= ?", t)
|
||||
} else {
|
||||
query = query.Where("created_at >= ?", startTimeStr)
|
||||
}
|
||||
}
|
||||
if endTimeStr != "" {
|
||||
if t, err := time.ParseInLocation("2006-01-02", endTimeStr, time.Local); err == nil {
|
||||
t = t.Add(24*time.Hour - time.Nanosecond)
|
||||
query = query.Where("created_at <= ?", t)
|
||||
} else if t, err := time.ParseInLocation("2006-01-02 15:04:05", endTimeStr, time.Local); err == nil {
|
||||
query = query.Where("created_at <= ?", t)
|
||||
} else {
|
||||
if len(endTimeStr) == 10 { // yyyy-MM-dd
|
||||
endTimeStr += " 23:59:59"
|
||||
}
|
||||
query = query.Where("created_at <= ?", endTimeStr)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
logrus.WithError(err).Error("获取日志总数失败")
|
||||
logBaseController.HandleInternalError(c, "获取日志总数失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 分页查询(时间倒序,从新到旧)
|
||||
offset := (page - 1) * limit
|
||||
if err := query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&logs).Error; err != nil {
|
||||
logrus.WithError(err).Error("查询日志列表失败")
|
||||
logBaseController.HandleInternalError(c, "查询日志列表失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
logBaseController.HandleSuccess(c, "获取日志列表成功", gin.H{
|
||||
"list": logs,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
|
||||
// LogsClearHandler 清空日志API处理器
|
||||
func LogsClearHandler(c *gin.Context) {
|
||||
db, ok := logBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 开启事务进行清空
|
||||
if err := db.Session(&gorm.Session{AllowGlobalUpdate: true}).Unscoped().Delete(&models.OperationLog{}).Error; err != nil {
|
||||
logrus.WithError(err).Error("清空操作日志失败")
|
||||
logBaseController.HandleInternalError(c, "清空操作日志失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 记录操作日志 (因为刚刚清空了,这条将是第一条)
|
||||
operator := "admin"
|
||||
log := models.OperationLog{
|
||||
OperationType: "清空日志",
|
||||
Operator: operator,
|
||||
OperatorUUID: "",
|
||||
AppName: "-",
|
||||
ProductName: "-",
|
||||
TransactionID: "-",
|
||||
Details: "管理员清空了所有操作日志",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
db.Create(&log)
|
||||
|
||||
logBaseController.HandleSuccess(c, "日志已清空", nil)
|
||||
}
|
||||
262
controllers/admin/profile.go
Normal file
262
controllers/admin/profile.go
Normal file
@@ -0,0 +1,262 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"NetworkAuth/database"
|
||||
"NetworkAuth/models"
|
||||
"NetworkAuth/services"
|
||||
"NetworkAuth/utils"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ProfileFragmentHandler 个人资料片段渲染
|
||||
// - 渲染个人资料与修改密码表单
|
||||
func ProfileFragmentHandler(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "profile.html", map[string]interface{}{})
|
||||
}
|
||||
|
||||
// ProfileInfoHandler 查询当前登录管理员的基本信息
|
||||
// - 返回 username 字段
|
||||
func ProfileInfoHandler(c *gin.Context) {
|
||||
_, _, err := GetCurrentAdminUserWithRefresh(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 1,
|
||||
"msg": "未登录或会话已过期",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取最新设置
|
||||
settingsService := services.GetSettingsService()
|
||||
username := settingsService.GetString("admin_username", "admin")
|
||||
|
||||
authBaseController.HandleSuccess(c, "ok", map[string]interface{}{
|
||||
"username": username,
|
||||
})
|
||||
}
|
||||
|
||||
// ProfilePasswordUpdateHandler 修改当前登录管理员的密码
|
||||
// - 接收 JSON: {old_password, new_password, confirm_password}
|
||||
// - 校验旧密码正确性、新密码与确认一致性
|
||||
// - 成功后更新密码哈希
|
||||
func ProfilePasswordUpdateHandler(c *gin.Context) {
|
||||
_, _, err := GetCurrentAdminUserWithRefresh(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 1,
|
||||
"msg": "未登录或会话已过期",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
OldPassword string `json:"old_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
ConfirmPassword string `json:"confirm_password"`
|
||||
}
|
||||
if !authBaseController.BindJSON(c, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
// 基础校验
|
||||
if body.OldPassword == "" || body.NewPassword == "" || body.ConfirmPassword == "" {
|
||||
authBaseController.HandleValidationError(c, "旧密码/新密码/确认密码均不能为空")
|
||||
return
|
||||
}
|
||||
if len(body.NewPassword) < 6 {
|
||||
authBaseController.HandleValidationError(c, "新密码长度不能少于6位")
|
||||
return
|
||||
}
|
||||
if body.NewPassword != body.ConfirmPassword {
|
||||
authBaseController.HandleValidationError(c, "两次输入的新密码不一致")
|
||||
return
|
||||
}
|
||||
if body.NewPassword == body.OldPassword {
|
||||
authBaseController.HandleValidationError(c, "新密码不能与旧密码相同")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前密码设置
|
||||
settingsService := services.GetSettingsService()
|
||||
currentHash := settingsService.GetString("admin_password", "")
|
||||
currentSalt := settingsService.GetString("admin_password_salt", "")
|
||||
|
||||
// 校验旧密码
|
||||
if !utils.VerifyPasswordWithSalt(body.OldPassword, currentSalt, currentHash) {
|
||||
authBaseController.HandleValidationError(c, "旧密码不正确")
|
||||
return
|
||||
}
|
||||
|
||||
// 生成新盐值和哈希
|
||||
newSalt, err := utils.GenerateRandomSalt()
|
||||
if err != nil {
|
||||
authBaseController.HandleInternalError(c, "生成盐值失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
newHash, err := utils.HashPasswordWithSalt(body.NewPassword, newSalt)
|
||||
if err != nil {
|
||||
authBaseController.HandleInternalError(c, "生成密码哈希失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新数据库
|
||||
db, ok := authBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 更新 admin_password
|
||||
if err := updateSetting(db, "admin_password", newHash); err != nil {
|
||||
authBaseController.HandleInternalError(c, "更新密码失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新 admin_password_salt
|
||||
if err := updateSetting(db, "admin_password_salt", newSalt); err != nil {
|
||||
authBaseController.HandleInternalError(c, "更新盐值失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 刷新缓存
|
||||
settingsService.RefreshCache()
|
||||
|
||||
// 清除相关缓存键
|
||||
_ = utils.RedisDel(c.Request.Context(), "setting:admin_password", "setting:admin_password_salt")
|
||||
|
||||
// 获取当前用户名
|
||||
currentUsername := settingsService.GetString("admin_username", "admin")
|
||||
|
||||
// 重新签发JWT并写入Cookie
|
||||
token, err := generateJWTTokenForAdmin(currentUsername, newHash)
|
||||
if err != nil {
|
||||
authBaseController.HandleInternalError(c, "生成新令牌失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
secure, sameSite, domain, maxAge := settingsService.GetCookieConfig()
|
||||
cookie := utils.CreateSecureCookie("admin_session", token, maxAge, domain, secure, sameSite)
|
||||
c.SetCookie(cookie.Name, cookie.Value, cookie.MaxAge, cookie.Path, cookie.Domain, cookie.Secure, cookie.HttpOnly)
|
||||
|
||||
authBaseController.HandleSuccess(c, "密码修改成功", nil)
|
||||
}
|
||||
|
||||
// ProfileUpdateHandler 修改当前登录管理员的用户名
|
||||
// - 接收 JSON: {username}
|
||||
// - 校验用户名非空、长度
|
||||
// - 更新数据库后重新签发JWT并写入 Cookie,保持前端展示的一致性
|
||||
func ProfileUpdateHandler(c *gin.Context) {
|
||||
_, _, err := GetCurrentAdminUserWithRefresh(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 1,
|
||||
"msg": "未登录或会话已过期",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Username string `json:"username"`
|
||||
OldPassword string `json:"old_password"`
|
||||
}
|
||||
if !authBaseController.BindJSON(c, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
username := strings.TrimSpace(body.Username)
|
||||
if username == "" {
|
||||
authBaseController.HandleValidationError(c, "用户名不能为空")
|
||||
return
|
||||
}
|
||||
if len(username) > 64 {
|
||||
authBaseController.HandleValidationError(c, "用户名长度不能超过64字符")
|
||||
return
|
||||
}
|
||||
|
||||
settingsService := services.GetSettingsService()
|
||||
currentUsername := settingsService.GetString("admin_username", "admin")
|
||||
|
||||
// 如果未变化则直接返回成功
|
||||
if strings.EqualFold(username, currentUsername) {
|
||||
authBaseController.HandleSuccess(c, "保存成功", map[string]interface{}{
|
||||
"username": username,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 修改用户名需要进行当前密码校验
|
||||
if strings.TrimSpace(body.OldPassword) == "" {
|
||||
authBaseController.HandleValidationError(c, "修改用户名需要提供当前密码")
|
||||
return
|
||||
}
|
||||
|
||||
currentHash := settingsService.GetString("admin_password", "")
|
||||
currentSalt := settingsService.GetString("admin_password_salt", "")
|
||||
|
||||
// 校验旧密码
|
||||
if !utils.VerifyPasswordWithSalt(body.OldPassword, currentSalt, currentHash) {
|
||||
authBaseController.HandleValidationError(c, "当前密码不正确")
|
||||
return
|
||||
}
|
||||
|
||||
// 更新数据库
|
||||
db, ok := authBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if err := updateSetting(db, "admin_username", username); err != nil {
|
||||
authBaseController.HandleInternalError(c, "更新用户名失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 重新签发JWT并写入Cookie
|
||||
token, err := generateJWTTokenForAdmin(username, currentHash)
|
||||
if err != nil {
|
||||
authBaseController.HandleInternalError(c, "生成新令牌失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
secure, sameSite, domain, maxAge := settingsService.GetCookieConfig()
|
||||
cookie := utils.CreateSecureCookie("admin_session", token, maxAge, domain, secure, sameSite)
|
||||
c.SetCookie(cookie.Name, cookie.Value, cookie.MaxAge, cookie.Path, cookie.Domain, cookie.Secure, cookie.HttpOnly)
|
||||
|
||||
// 刷新缓存
|
||||
settingsService.RefreshCache()
|
||||
_ = utils.RedisDel(c.Request.Context(), "setting:admin_username")
|
||||
|
||||
authBaseController.HandleSuccess(c, "用户名修改成功", map[string]interface{}{
|
||||
"username": username,
|
||||
})
|
||||
}
|
||||
|
||||
// 辅助函数:更新设置项
|
||||
func updateSetting(db interface{}, name, value string) error {
|
||||
// 类型断言
|
||||
gormDB, ok := db.(*gorm.DB)
|
||||
if !ok {
|
||||
// 如果断言失败,尝试重新获取连接
|
||||
var err error
|
||||
gormDB, err = database.GetDB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var setting models.Settings
|
||||
if err := gormDB.Where("name = ?", name).First(&setting).Error; err != nil {
|
||||
// 如果不存在则创建
|
||||
setting = models.Settings{Name: name, Value: value}
|
||||
return gormDB.Create(&setting).Error
|
||||
}
|
||||
|
||||
// 存在则更新
|
||||
return gormDB.Model(&setting).Update("value", value).Error
|
||||
}
|
||||
@@ -1,55 +1,52 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"NetworkAuth/config"
|
||||
"NetworkAuth/models"
|
||||
"NetworkAuth/services"
|
||||
"NetworkAuth/utils"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"networkDev/controllers"
|
||||
"networkDev/models"
|
||||
"networkDev/services"
|
||||
"networkDev/utils"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// 全局变量
|
||||
// ============================================================================
|
||||
|
||||
// 创建基础控制器实例
|
||||
var settingsBaseController = controllers.NewBaseController()
|
||||
|
||||
// ============================================================================
|
||||
// 页面处理器
|
||||
// ============================================================================
|
||||
|
||||
// SettingsFragmentHandler 设置片段渲染
|
||||
// - 渲染设置表单(通过前端JS调用API加载/保存)
|
||||
func SettingsFragmentHandler(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "settings.html", gin.H{})
|
||||
c.HTML(http.StatusOK, "settings.html", map[string]interface{}{})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API处理器
|
||||
// ============================================================================
|
||||
// SubAccountSimpleListHandler 子账号简单列表API处理器 (Mock)
|
||||
func SubAccountSimpleListHandler(c *gin.Context) {
|
||||
// Mock implementation for NetworkAuth which has no subaccounts
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": []interface{}{},
|
||||
})
|
||||
}
|
||||
|
||||
// SettingsQueryHandler 设置查询API
|
||||
// - 返回所有设置项的 name:value 映射
|
||||
func SettingsQueryHandler(c *gin.Context) {
|
||||
db, ok := settingsBaseController.GetDB(c)
|
||||
db, ok := authBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var list []models.Settings
|
||||
if err := db.Find(&list).Error; err != nil {
|
||||
settingsBaseController.HandleInternalError(c, "查询失败", err)
|
||||
authBaseController.HandleInternalError(c, "查询失败", err)
|
||||
return
|
||||
}
|
||||
res := map[string]string{}
|
||||
for _, s := range list {
|
||||
res[s.Name] = s.Value
|
||||
}
|
||||
settingsBaseController.HandleSuccess(c, "ok", res)
|
||||
authBaseController.HandleSuccess(c, "ok", res)
|
||||
}
|
||||
|
||||
// SettingsUpdateHandler 更新系统设置处理器
|
||||
@@ -65,7 +62,8 @@ func SettingsQueryHandler(c *gin.Context) {
|
||||
func SettingsUpdateHandler(c *gin.Context) {
|
||||
// 先尝试解析为直接字段格式
|
||||
var directBody map[string]interface{}
|
||||
if !settingsBaseController.BindJSON(c, &directBody) {
|
||||
if err := c.ShouldBindJSON(&directBody); err != nil {
|
||||
authBaseController.HandleValidationError(c, "请求体错误")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -82,7 +80,7 @@ func SettingsUpdateHandler(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
settingsBaseController.HandleValidationError(c, "settings字段格式错误")
|
||||
authBaseController.HandleValidationError(c, "settings字段格式错误")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
@@ -99,11 +97,19 @@ func SettingsUpdateHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
if len(settingsData) == 0 {
|
||||
settingsBaseController.HandleValidationError(c, "无设置项")
|
||||
authBaseController.HandleValidationError(c, "无设置项")
|
||||
return
|
||||
}
|
||||
|
||||
db, ok := settingsBaseController.GetDB(c)
|
||||
// 验证设置项值
|
||||
for k, v := range settingsData {
|
||||
if err := validateSettingValue(k, v); err != nil {
|
||||
authBaseController.HandleValidationError(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
db, ok := authBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -113,13 +119,66 @@ func SettingsUpdateHandler(c *gin.Context) {
|
||||
|
||||
// 批量处理设置项
|
||||
for k, v := range settingsData {
|
||||
// 特殊处理 admin_password
|
||||
if k == "admin_password" {
|
||||
// 如果密码为空,跳过更新(保留原密码)
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
// 由于 NetworkAuth 中没有 SystemAdminUser 全局变量,这里暂时使用 "admin"
|
||||
// operator := "admin"
|
||||
// 尝试从上下文获取用户名(如果中间件设置了的话)
|
||||
// if user, exists := c.Get("username"); exists {
|
||||
// operator = user.(string)
|
||||
// }
|
||||
|
||||
// 生成随机盐值
|
||||
salt, err := utils.GenerateRandomSalt()
|
||||
if err != nil {
|
||||
authBaseController.HandleInternalError(c, "生成盐值失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 使用盐值哈希密码
|
||||
hash, err := utils.HashPasswordWithSalt(v, salt)
|
||||
if err != nil {
|
||||
authBaseController.HandleInternalError(c, "密码哈希失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新 salt 设置项(如果不存在则创建)
|
||||
var saltSetting models.Settings
|
||||
if err := db.Where("name = ?", "admin_password_salt").First(&saltSetting).Error; err != nil {
|
||||
saltSetting = models.Settings{Name: "admin_password_salt", Value: salt}
|
||||
if err := db.Create(&saltSetting).Error; err != nil {
|
||||
logrus.WithError(err).Error("创建admin_password_salt失败")
|
||||
authBaseController.HandleInternalError(c, "保存盐值失败", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := db.Model(&saltSetting).Update("value", salt).Error; err != nil {
|
||||
logrus.WithError(err).Error("更新admin_password_salt失败")
|
||||
authBaseController.HandleInternalError(c, "更新盐值失败", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 将盐值相关的缓存键加入清理列表
|
||||
keysToDel = append(keysToDel, "setting:admin_password_salt")
|
||||
|
||||
// 将当前处理的值替换为哈希后的密码
|
||||
v = hash
|
||||
}
|
||||
|
||||
var s models.Settings
|
||||
if err := db.Where("name = ?", k).First(&s).Error; err != nil {
|
||||
// 不存在则创建
|
||||
s = models.Settings{Name: k, Value: v}
|
||||
if err := db.Create(&s).Error; err != nil {
|
||||
logrus.WithError(err).WithField("setting_name", k).Error("创建设置失败")
|
||||
settingsBaseController.HandleInternalError(c, fmt.Sprintf("保存设置 %s 失败", k), err)
|
||||
authBaseController.HandleInternalError(c, fmt.Sprintf("保存设置 %s 失败", k), err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -127,7 +186,7 @@ func SettingsUpdateHandler(c *gin.Context) {
|
||||
// 存在则更新
|
||||
if err := db.Model(&models.Settings{}).Where("id = ?", s.ID).Update("value", v).Error; err != nil {
|
||||
logrus.WithError(err).WithField("setting_name", k).Error("更新设置失败")
|
||||
settingsBaseController.HandleInternalError(c, fmt.Sprintf("更新设置 %s 失败", k), err)
|
||||
authBaseController.HandleInternalError(c, fmt.Sprintf("更新设置 %s 失败", k), err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -137,10 +196,60 @@ func SettingsUpdateHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 删除Redis缓存键(如果Redis不可用则静默跳过)
|
||||
_ = utils.RedisDel(context.Background(), keysToDel...)
|
||||
_ = utils.RedisDel(c.Request.Context(), keysToDel...)
|
||||
|
||||
// 刷新内存中的设置缓存,保证后续读取一致
|
||||
services.GetSettingsService().RefreshCache()
|
||||
|
||||
settingsBaseController.HandleSuccess(c, "保存成功", nil)
|
||||
authBaseController.HandleSuccess(c, "保存成功", nil)
|
||||
}
|
||||
|
||||
// validateSettingValue 验证设置项值的合法性
|
||||
func validateSettingValue(key, value string) error {
|
||||
switch key {
|
||||
case "jwt_refresh":
|
||||
// 验证JWT刷新时间:至少1小时
|
||||
hours, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("JWT刷新阈值必须是整数")
|
||||
}
|
||||
if hours < 1 {
|
||||
return fmt.Errorf("JWT刷新阈值必须至少为1小时")
|
||||
}
|
||||
case "jwt_expire":
|
||||
// 验证JWT有效期:至少1小时
|
||||
hours, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("JWT有效期必须是整数")
|
||||
}
|
||||
if hours < 1 {
|
||||
return fmt.Errorf("JWT有效期必须至少为1小时")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SettingsGenerateKeyHandler 生成安全密钥API
|
||||
// - type: "jwt" 或 "encryption"
|
||||
func SettingsGenerateKeyHandler(c *gin.Context) {
|
||||
keyType := c.Query("type")
|
||||
var key string
|
||||
var err error
|
||||
|
||||
switch keyType {
|
||||
case "jwt":
|
||||
key, err = config.GenerateSecureJWTSecret()
|
||||
case "encryption":
|
||||
key, err = config.GenerateSecureEncryptionKey()
|
||||
default:
|
||||
authBaseController.HandleValidationError(c, "无效的密钥类型")
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
authBaseController.HandleInternalError(c, "生成密钥失败: "+err.Error(), err)
|
||||
return
|
||||
}
|
||||
|
||||
authBaseController.HandleSuccess(c, "生成成功", map[string]string{"key": key})
|
||||
}
|
||||
|
||||
@@ -1,282 +0,0 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"networkDev/controllers"
|
||||
"networkDev/models"
|
||||
"networkDev/utils"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// 全局变量
|
||||
// ============================================================================
|
||||
|
||||
// 创建基础控制器实例
|
||||
var baseController = controllers.NewBaseController()
|
||||
|
||||
// ============================================================================
|
||||
// 页面处理器
|
||||
// ============================================================================
|
||||
|
||||
// UserFragmentHandler 个人资料片段渲染
|
||||
// - 渲染个人资料与修改密码表单
|
||||
func UserFragmentHandler(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "user.html", gin.H{})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API处理器
|
||||
// ============================================================================
|
||||
|
||||
// UserProfileQueryHandler 获取当前登录管理员的用户名
|
||||
// - 返回 JSON: {username}
|
||||
// - 直接从JWT获取用户名信息
|
||||
func UserProfileQueryHandler(c *gin.Context) {
|
||||
claims, _, err := GetCurrentAdminUserWithRefresh(c)
|
||||
if err != nil {
|
||||
baseController.HandleValidationError(c, "未登录或会话已过期")
|
||||
return
|
||||
}
|
||||
|
||||
baseController.HandleSuccess(c, "ok", gin.H{
|
||||
"username": claims.Username,
|
||||
})
|
||||
}
|
||||
|
||||
// UserPasswordUpdateHandler 修改当前登录管理员的密码
|
||||
// - 接收 JSON: {old_password, new_password, confirm_password}
|
||||
// - 校验旧密码正确性、新密码与确认一致性
|
||||
// - 成功后更新密码哈希
|
||||
// - 自动刷新接近过期的JWT令牌
|
||||
func UserPasswordUpdateHandler(c *gin.Context) {
|
||||
claims, _, err := GetCurrentAdminUserWithRefresh(c)
|
||||
if err != nil {
|
||||
baseController.HandleValidationError(c, "未登录或会话已过期")
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
OldPassword string `json:"old_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
ConfirmPassword string `json:"confirm_password"`
|
||||
}
|
||||
|
||||
if !baseController.BindJSON(c, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
// 基础校验
|
||||
if !baseController.ValidateRequired(c, map[string]interface{}{
|
||||
"旧密码": body.OldPassword,
|
||||
"新密码": body.NewPassword,
|
||||
"确认密码": body.ConfirmPassword,
|
||||
}) {
|
||||
return
|
||||
}
|
||||
|
||||
if len(body.NewPassword) < 6 {
|
||||
baseController.HandleValidationError(c, "新密码长度不能少于6位")
|
||||
return
|
||||
}
|
||||
if body.NewPassword != body.ConfirmPassword {
|
||||
baseController.HandleValidationError(c, "两次输入的新密码不一致")
|
||||
return
|
||||
}
|
||||
if body.NewPassword == body.OldPassword {
|
||||
baseController.HandleValidationError(c, "新密码不能与旧密码相同")
|
||||
return
|
||||
}
|
||||
|
||||
// 注释:由于使用了AdminAuthRequired中间件,已确保是管理员用户
|
||||
|
||||
// 获取数据库连接
|
||||
db, ok := baseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 通过前缀匹配一次性获取所有管理员相关设置
|
||||
var adminSettings []models.Settings
|
||||
if err = db.Where("name LIKE ?", "admin_%").Find(&adminSettings).Error; err != nil {
|
||||
baseController.HandleInternalError(c, "获取管理员设置失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 将设置转换为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 {
|
||||
baseController.HandleInternalError(c, "管理员密码设置不完整", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 校验旧密码
|
||||
if !utils.VerifyPasswordWithSalt(body.OldPassword, adminPasswordSalt, adminPassword) {
|
||||
baseController.HandleValidationError(c, "旧密码不正确")
|
||||
return
|
||||
}
|
||||
|
||||
// 生成新的密码盐值
|
||||
newSalt, err := utils.GenerateRandomSalt()
|
||||
if err != nil {
|
||||
baseController.HandleInternalError(c, "生成密码盐失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 生成新密码哈希
|
||||
newPasswordHash, err := utils.HashPasswordWithSalt(body.NewPassword, newSalt)
|
||||
if err != nil {
|
||||
baseController.HandleInternalError(c, "生成密码哈希失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新settings中的管理员密码和盐值
|
||||
if err = db.Model(&models.Settings{}).Where("name = ?", "admin_password").Update("value", newPasswordHash).Error; err != nil {
|
||||
baseController.HandleInternalError(c, "更新密码失败", err)
|
||||
return
|
||||
}
|
||||
if err = db.Model(&models.Settings{}).Where("name = ?", "admin_password_salt").Update("value", newSalt).Error; err != nil {
|
||||
baseController.HandleInternalError(c, "更新密码盐值失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 重新生成JWT令牌(包含新的密码哈希摘要)
|
||||
adminUser := models.User{
|
||||
Username: claims.Username,
|
||||
Password: newPasswordHash,
|
||||
PasswordSalt: newSalt,
|
||||
}
|
||||
newToken, err := generateJWTTokenForAdmin(adminUser)
|
||||
if err != nil {
|
||||
baseController.HandleInternalError(c, "生成新令牌失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新Cookie(使用安全配置)
|
||||
c.SetCookie("admin_session", newToken, utils.GetDefaultCookieMaxAge(), "/", "", false, true)
|
||||
|
||||
// 密码修改成功,已重新生成JWT令牌
|
||||
baseController.HandleSuccess(c, "密码修改成功", nil)
|
||||
}
|
||||
|
||||
// UserProfileUpdateHandler 修改当前登录管理员的用户名
|
||||
// - 接收 JSON: {username}
|
||||
// - 校验用户名非空、长度与唯一性
|
||||
// - 更新数据库后重新签发JWT并写入 Cookie,保持前端展示的一致性
|
||||
// - 自动刷新接近过期的JWT令牌
|
||||
func UserProfileUpdateHandler(c *gin.Context) {
|
||||
_, _, err := GetCurrentAdminUserWithRefresh(c)
|
||||
if err != nil {
|
||||
baseController.HandleValidationError(c, "未登录或会话已过期")
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Username string `json:"username"`
|
||||
OldPassword string `json:"old_password"`
|
||||
}
|
||||
if !baseController.BindJSON(c, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
username := strings.TrimSpace(body.Username)
|
||||
if username == "" {
|
||||
baseController.HandleValidationError(c, "用户名不能为空")
|
||||
return
|
||||
}
|
||||
if len(username) > 64 {
|
||||
baseController.HandleValidationError(c, "用户名长度不能超过64字符")
|
||||
return
|
||||
}
|
||||
|
||||
db, ok := baseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 注释:由于使用了AdminAuthRequired中间件,已确保是管理员用户
|
||||
|
||||
// 获取所有管理员相关设置
|
||||
var adminSettings []models.Settings
|
||||
if dbErr := db.Where("name LIKE ?", "admin_%").Find(&adminSettings).Error; dbErr != nil {
|
||||
baseController.HandleInternalError(c, "获取管理员设置失败", dbErr)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为map便于查找
|
||||
settingsMap := make(map[string]string)
|
||||
for _, setting := range adminSettings {
|
||||
settingsMap[setting.Name] = setting.Value
|
||||
}
|
||||
|
||||
adminUsername, exists := settingsMap["admin_username"]
|
||||
if !exists {
|
||||
baseController.HandleInternalError(c, "管理员用户名设置不存在", nil)
|
||||
return
|
||||
}
|
||||
|
||||
adminPassword, exists := settingsMap["admin_password"]
|
||||
if !exists {
|
||||
baseController.HandleInternalError(c, "管理员密码设置不存在", nil)
|
||||
return
|
||||
}
|
||||
|
||||
adminPasswordSalt, exists := settingsMap["admin_password_salt"]
|
||||
if !exists {
|
||||
baseController.HandleInternalError(c, "管理员密码盐值设置不存在", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果用户名未变化则直接返回成功(无需校验旧密码)
|
||||
if strings.EqualFold(username, adminUsername) {
|
||||
baseController.HandleSuccess(c, "保存成功", gin.H{
|
||||
"username": username,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 修改用户名需要进行当前密码校验
|
||||
if strings.TrimSpace(body.OldPassword) == "" {
|
||||
baseController.HandleValidationError(c, "修改用户名需要提供当前密码")
|
||||
return
|
||||
}
|
||||
|
||||
// 使用盐值验证当前密码
|
||||
if !utils.VerifyPasswordWithSalt(body.OldPassword, adminPasswordSalt, adminPassword) {
|
||||
baseController.HandleValidationError(c, "当前密码不正确")
|
||||
return
|
||||
}
|
||||
|
||||
// 更新管理员用户名设置
|
||||
if dbErr := db.Model(&models.Settings{}).Where("name = ?", "admin_username").Update("value", username).Error; dbErr != nil {
|
||||
baseController.HandleInternalError(c, "更新管理员用户名失败", dbErr)
|
||||
return
|
||||
}
|
||||
|
||||
// 重新签发JWT并写入Cookie
|
||||
// 创建虚拟用户对象用于生成JWT令牌
|
||||
adminUser := models.User{
|
||||
Username: username, // 使用新的用户名
|
||||
Password: adminPassword,
|
||||
PasswordSalt: adminPasswordSalt,
|
||||
}
|
||||
token, err := generateJWTTokenForAdmin(adminUser)
|
||||
if err != nil {
|
||||
baseController.HandleInternalError(c, "生成新令牌失败", err)
|
||||
return
|
||||
}
|
||||
c.SetCookie("admin_session", token, utils.GetDefaultCookieMaxAge(), "/", "", false, true)
|
||||
|
||||
baseController.HandleSuccess(c, "保存成功", gin.H{
|
||||
"username": username,
|
||||
})
|
||||
}
|
||||
7
controllers/admin/utils.go
Normal file
7
controllers/admin/utils.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"NetworkAuth/controllers"
|
||||
)
|
||||
|
||||
var base = controllers.NewBaseController()
|
||||
@@ -1,9 +1,9 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"NetworkAuth/controllers"
|
||||
"NetworkAuth/models"
|
||||
"net/http"
|
||||
"networkDev/controllers"
|
||||
"networkDev/models"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -319,7 +319,7 @@ func VariableDeleteHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
logrus.WithField("variable_id", req.ID).Info("Successfully deleted variable")
|
||||
logrus.WithField("variable_id", req.ID).Debug("Successfully deleted variable")
|
||||
|
||||
variableBaseController.HandleSuccess(c, "删除成功", nil)
|
||||
}
|
||||
@@ -351,7 +351,7 @@ func VariablesBatchDeleteHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
logrus.WithField("variable_ids", req.IDs).Info("Successfully batch deleted variables")
|
||||
logrus.WithField("variable_ids", req.IDs).Debug("Successfully batch deleted variables")
|
||||
|
||||
variableBaseController.HandleSuccess(c, "批量删除成功", nil)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"networkDev/database"
|
||||
"NetworkAuth/database"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
// ============================================================================
|
||||
|
||||
// BaseController 基础控制器结构体
|
||||
// 提供通用的数据库访问和响应处理方法
|
||||
type BaseController struct{}
|
||||
|
||||
// ============================================================================
|
||||
@@ -86,11 +87,18 @@ func (bc *BaseController) HandleInternalError(c *gin.Context, message string, er
|
||||
|
||||
// HandleSuccess 统一处理成功响应
|
||||
func (bc *BaseController) HandleSuccess(c *gin.Context, message string, data interface{}) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
resp := gin.H{
|
||||
"code": 0,
|
||||
"msg": message,
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
|
||||
// 检查是否有刷新的Token
|
||||
if newToken, exists := c.Get("new_token"); exists {
|
||||
resp["token"] = newToken
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// HandleCreated 统一处理创建成功响应
|
||||
@@ -173,9 +181,9 @@ func (bc *BaseController) BindURI(c *gin.Context, obj interface{}) bool {
|
||||
// 返回包含系统基础信息的数据映射,包括站点标题、页脚文本、备案信息等
|
||||
func (bc *BaseController) GetDefaultTemplateData() gin.H {
|
||||
return gin.H{
|
||||
"Title": "凌动技术",
|
||||
"SystemName": "网络验证系统",
|
||||
"FooterText": "© 2025 凌动技术 保留所有权利",
|
||||
"Title": "NetworkAuth",
|
||||
"SystemName": "NetworkAuth",
|
||||
"FooterText": "© 2026 NetworkAuth 保留所有权利",
|
||||
"ICPRecord": "",
|
||||
"ICPRecordLink": "https://beian.miit.gov.cn",
|
||||
"PSBRecord": "",
|
||||
|
||||
32
controllers/default/handlers.go
Normal file
32
controllers/default/handlers.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package default_ctrl
|
||||
|
||||
import (
|
||||
"NetworkAuth/services"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RootHandler 根路径处理器
|
||||
// 使用模板渲染服务器信息页面
|
||||
func RootHandler(c *gin.Context) {
|
||||
// 获取设置服务
|
||||
settings := services.GetSettingsService()
|
||||
|
||||
// 传递模板数据
|
||||
data := map[string]interface{}{
|
||||
"Title": settings.GetString("site_title", "NetworkAuth Server"),
|
||||
"Keywords": settings.GetString("site_keywords", ""),
|
||||
"Description": settings.GetString("site_description", ""),
|
||||
"SystemName": "系统提醒", // 对应 H1
|
||||
"WarningText": "🚫 未授权,拒绝访问",
|
||||
"InfoText": "💬 如有问题,请联系网站管理员",
|
||||
"FooterText": settings.GetString("footer_text", "Copyright © 2026 NetworkAuth. All Rights Reserved."),
|
||||
"ICPRecord": settings.GetString("icp_record", ""),
|
||||
"ICPRecordLink": settings.GetString("icp_record_link", "https://beian.miit.gov.cn"),
|
||||
"CurrentYear": time.Now().Year(),
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "index.html", data)
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package home
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"networkDev/controllers"
|
||||
"networkDev/database"
|
||||
"networkDev/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// 全局变量
|
||||
// ============================================================================
|
||||
|
||||
var homeBaseController = controllers.NewBaseController()
|
||||
|
||||
// ============================================================================
|
||||
// 辅助函数
|
||||
// ============================================================================
|
||||
|
||||
// getSettingValue 获取配置值,优先从数据库获取,不存在时使用默认值
|
||||
func getSettingValue(settingName string, defaultValue string, db *gorm.DB) string {
|
||||
if setting, err := services.FindSettingByName(settingName, db); err == nil {
|
||||
return setting.Value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 页面处理器
|
||||
// ============================================================================
|
||||
|
||||
// RootHandler 主页处理器
|
||||
func RootHandler(c *gin.Context) {
|
||||
// 获取数据库连接
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
c.HTML(http.StatusInternalServerError, "error.html", gin.H{
|
||||
"error": "数据库连接失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取默认模板数据
|
||||
data := homeBaseController.GetDefaultTemplateData()
|
||||
|
||||
// 从数据库读取设置,优先使用数据库配置,不存在时使用默认值
|
||||
data["SystemName"] = getSettingValue("site_title", data["SystemName"].(string), db)
|
||||
data["FooterText"] = getSettingValue("footer_text", data["FooterText"].(string), db)
|
||||
data["ICPRecord"] = getSettingValue("icp_record", data["ICPRecord"].(string), db)
|
||||
data["ICPRecordLink"] = getSettingValue("icp_record_link", data["ICPRecordLink"].(string), db)
|
||||
data["PSBRecord"] = getSettingValue("psb_record", data["PSBRecord"].(string), db)
|
||||
data["PSBRecordLink"] = getSettingValue("psb_record_link", data["PSBRecordLink"].(string), db)
|
||||
data["title"] = "主页"
|
||||
|
||||
c.HTML(http.StatusOK, "index.html", data)
|
||||
}
|
||||
122
controllers/install/install.go
Normal file
122
controllers/install/install.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"NetworkAuth/config"
|
||||
"NetworkAuth/database"
|
||||
"NetworkAuth/models"
|
||||
"NetworkAuth/services"
|
||||
"NetworkAuth/utils"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// InstallPageHandler 渲染安装页面
|
||||
func InstallPageHandler(c *gin.Context) {
|
||||
// 由于前端是通过模板渲染的,我们返回一个安装页面
|
||||
c.HTML(http.StatusOK, "install.html", gin.H{
|
||||
"title": "NetworkAuth 系统初始化",
|
||||
})
|
||||
}
|
||||
|
||||
// InstallSubmitHandler 处理安装表单提交
|
||||
func InstallSubmitHandler(c *gin.Context) {
|
||||
var req struct {
|
||||
// 数据库配置
|
||||
DbType string `json:"db_type" binding:"required,oneof=sqlite mysql"`
|
||||
DbHost string `json:"db_host"`
|
||||
DbPort int `json:"db_port"`
|
||||
DbName string `json:"db_name"`
|
||||
DbUser string `json:"db_user"`
|
||||
DbPass string `json:"db_pass"`
|
||||
|
||||
// 站点和管理员配置
|
||||
SiteTitle string `json:"site_title" binding:"required"`
|
||||
AdminUsername string `json:"admin_username" binding:"required"`
|
||||
AdminPassword string `json:"admin_password" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 更新配置文件
|
||||
err := config.UpdateConfig(func(cfg *config.AppConfig) {
|
||||
cfg.Database.Type = req.DbType
|
||||
if req.DbType == "mysql" {
|
||||
cfg.Database.MySQL.Host = req.DbHost
|
||||
cfg.Database.MySQL.Port = req.DbPort
|
||||
cfg.Database.MySQL.Database = req.DbName
|
||||
cfg.Database.MySQL.Username = req.DbUser
|
||||
cfg.Database.MySQL.Password = req.DbPass
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "更新配置文件失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 重新初始化数据库连接并执行迁移
|
||||
db, err := database.ReInit()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "连接数据库失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if db == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "获取数据库实例失败"})
|
||||
return
|
||||
}
|
||||
|
||||
// 强制执行迁移确保表存在
|
||||
if err := database.AutoMigrate(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "初始化数据表失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 初始化系统默认设置
|
||||
database.SeedDefaultSettings()
|
||||
|
||||
// 3. 生成新的管理员密码哈希和盐值
|
||||
adminSalt, err := utils.GenerateRandomSalt()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "生成盐值失败"})
|
||||
return
|
||||
}
|
||||
adminPasswordHash, err := utils.HashPasswordWithSalt(req.AdminPassword, adminSalt)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "加密密码失败"})
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 更新设置表
|
||||
settingsToUpdate := map[string]string{
|
||||
"site_title": req.SiteTitle,
|
||||
"admin_username": strings.TrimSpace(req.AdminUsername),
|
||||
"admin_password": adminPasswordHash,
|
||||
"admin_password_salt": adminSalt,
|
||||
"is_installed": "1", // 标记为已安装
|
||||
}
|
||||
|
||||
// 开启事务进行更新
|
||||
tx := db.Begin()
|
||||
for name, value := range settingsToUpdate {
|
||||
// 先尝试更新,如果没有该记录,则忽略(因为 AutoMigrate 已经创建了默认记录)
|
||||
if err := tx.Model(&models.Settings{}).Where("name = ?", name).Update("value", value).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "保存设置失败: " + name})
|
||||
return
|
||||
}
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
// 5. 更新内存缓存
|
||||
settingsService := services.GetSettingsService()
|
||||
for name, value := range settingsToUpdate {
|
||||
settingsService.Set(name, value)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "安装成功"})
|
||||
}
|
||||
Reference in New Issue
Block a user