mirror of
https://github.com/skyle1995/NetworkAuth.git
synced 2026-05-25 02:24:05 +08:00
修改项目为前后端分离方案
This commit is contained in:
@@ -21,17 +21,6 @@ import (
|
||||
// 创建基础控制器实例
|
||||
var apiBaseController = controllers.NewBaseController()
|
||||
|
||||
// ============================================================================
|
||||
// 页面处理器
|
||||
// ============================================================================
|
||||
|
||||
// APIFragmentHandler 接口列表页面片段处理器
|
||||
func APIFragmentHandler(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "apis.html", gin.H{
|
||||
"Title": "接口设置",
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API处理器
|
||||
// ============================================================================
|
||||
@@ -122,18 +111,12 @@ func APIListHandler(c *gin.Context) {
|
||||
responseAPIs = append(responseAPIs, responseAPI)
|
||||
}
|
||||
|
||||
// 计算分页信息
|
||||
totalPages := (total + int64(limit) - 1) / int64(limit)
|
||||
|
||||
// 返回结果
|
||||
response := gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{
|
||||
"apis": responseAPIs,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total_pages": totalPages,
|
||||
},
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"count": total,
|
||||
"data": responseAPIs,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
|
||||
@@ -21,17 +21,6 @@ import (
|
||||
|
||||
var appBaseController = controllers.NewBaseController()
|
||||
|
||||
// ============================================================================
|
||||
// 页面处理器
|
||||
// ============================================================================
|
||||
|
||||
// AppsFragmentHandler 应用列表页面片段处理器
|
||||
func AppsFragmentHandler(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "apps.html", gin.H{
|
||||
"Title": "应用程序",
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API处理器
|
||||
// ============================================================================
|
||||
@@ -359,6 +348,7 @@ func AppCreateHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to commit transaction")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
@@ -531,6 +521,28 @@ func AppDeleteHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 删除相关的变量记录
|
||||
if err := tx.Where("app_uuid = ?", app.UUID).Delete(&models.Variable{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
logrus.WithError(err).Error("Failed to delete related variables")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 1,
|
||||
"msg": "删除相关变量失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 删除相关的函数记录
|
||||
if err := tx.Where("app_uuid = ?", app.UUID).Delete(&models.Function{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
logrus.WithError(err).Error("Failed to delete related functions")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 1,
|
||||
"msg": "删除相关函数失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 删除应用
|
||||
if err := tx.Delete(&app).Error; err != nil {
|
||||
tx.Rollback()
|
||||
@@ -569,7 +581,7 @@ func AppDeleteHandler(c *gin.Context) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"app_id": app.ID,
|
||||
"app_uuid": app.UUID,
|
||||
}).Debug("Successfully deleted app and related APIs")
|
||||
}).Debug("Successfully deleted app and related APIs, Variables and Functions")
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
|
||||
@@ -24,54 +24,30 @@ import (
|
||||
var authBaseController = controllers.NewBaseController()
|
||||
|
||||
// ============================================================================
|
||||
// 页面处理器
|
||||
// API处理器
|
||||
// ============================================================================
|
||||
|
||||
// LoginPageHandler 管理员登录页渲染处理器
|
||||
// - 如果已登录则重定向到 /admin
|
||||
// - 否则渲染 web/template/admin/login.html 模板
|
||||
// - 自动清理失效的JWT Cookie,避免刷新时的问题
|
||||
func LoginPageHandler(c *gin.Context) {
|
||||
// 使用带清理功能的JWT校验,避免失效Cookie在登录页面造成问题
|
||||
if IsAdminAuthenticatedWithCleanup(c) {
|
||||
c.Redirect(http.StatusFound, "/admin")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取或生成CSRF令牌
|
||||
var token string
|
||||
// CSRFTokenHandler 获取CSRF令牌接口
|
||||
func CSRFTokenHandler(c *gin.Context) {
|
||||
// 尝试从Cookie获取
|
||||
var token string
|
||||
if cookie, err := c.Cookie(CSRFCookieName); err == nil && cookie != "" {
|
||||
token = cookie
|
||||
} else {
|
||||
// 生成新的CSRF令牌并设置到Cookie
|
||||
newToken, err := utils.GenerateCSRFToken()
|
||||
if err != nil {
|
||||
c.HTML(http.StatusInternalServerError, "error.html", gin.H{
|
||||
"Error": "生成CSRF令牌失败",
|
||||
})
|
||||
authBaseController.HandleInternalError(c, "生成CSRF令牌失败", err)
|
||||
return
|
||||
}
|
||||
token = newToken
|
||||
setCSRFToken(c, token)
|
||||
}
|
||||
|
||||
// 准备模板数据
|
||||
data := authBaseController.GetDefaultTemplateData()
|
||||
if sysName, ok := data["SystemName"].(string); ok && sysName != "" {
|
||||
data["Title"] = sysName + " - 管理员登录"
|
||||
} else {
|
||||
data["Title"] = "管理员登录"
|
||||
}
|
||||
data["CSRFToken"] = token
|
||||
|
||||
c.HTML(http.StatusOK, "login.html", data)
|
||||
authBaseController.HandleSuccess(c, "success", gin.H{
|
||||
"csrf_token": token,
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API处理器
|
||||
// ============================================================================
|
||||
|
||||
// LoginHandler 管理员登录接口
|
||||
// - 接收JSON: {username, password, captcha, csrf_token}
|
||||
// - 验证CSRF令牌
|
||||
@@ -112,35 +88,30 @@ func LoginHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取系统设置服务
|
||||
settingsService := services.GetSettingsService()
|
||||
adminUsername := settingsService.GetString("admin_username", "admin")
|
||||
adminPasswordHash := settingsService.GetString("admin_password", "")
|
||||
adminPasswordSalt := settingsService.GetString("admin_password_salt", "")
|
||||
|
||||
// 验证密码为空的情况(首次登录需要初始化)
|
||||
if adminPasswordHash == "" || adminPasswordSalt == "" {
|
||||
recordLoginLog(c, body.Username, 0, "管理员账号未初始化")
|
||||
authBaseController.HandleInternalError(c, "管理员账号未初始化,请联系系统管理员", nil)
|
||||
// 从数据库中查找对应的用户
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
recordLoginLog(c, body.Username, 0, "数据库连接失败")
|
||||
authBaseController.HandleInternalError(c, "数据库连接失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证用户名
|
||||
if body.Username != adminUsername {
|
||||
recordLoginLog(c, body.Username, 0, "用户名错误")
|
||||
var user models.User
|
||||
if err := db.Where("username = ? AND role = ?", body.Username, 0).First(&user).Error; err != nil {
|
||||
recordLoginLog(c, body.Username, 0, "用户不存在或非管理员")
|
||||
authBaseController.HandleValidationError(c, "用户不存在或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证密码(使用盐值校验)
|
||||
if !utils.VerifyPasswordWithSalt(body.Password, adminPasswordSalt, adminPasswordHash) {
|
||||
if !utils.VerifyPasswordWithSalt(body.Password, user.PasswordSalt, user.Password) {
|
||||
recordLoginLog(c, body.Username, 0, "密码错误")
|
||||
authBaseController.HandleValidationError(c, "用户不存在或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 生成JWT令牌
|
||||
token, err := generateJWTTokenForAdmin(body.Username, adminPasswordHash)
|
||||
token, err := generateJWTTokenForAdmin(user.Username, user.Password, user.UUID)
|
||||
if err != nil {
|
||||
recordLoginLog(c, body.Username, 0, "生成令牌失败")
|
||||
authBaseController.HandleInternalError(c, "生成令牌失败", err)
|
||||
@@ -149,6 +120,7 @@ func LoginHandler(c *gin.Context) {
|
||||
|
||||
// 设置JWT Cookie(HttpOnly,安全)
|
||||
// 使用系统配置的Cookie参数
|
||||
settingsService := services.GetSettingsService()
|
||||
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)
|
||||
@@ -156,6 +128,9 @@ func LoginHandler(c *gin.Context) {
|
||||
recordLoginLog(c, body.Username, 1, "登录成功")
|
||||
authBaseController.HandleSuccess(c, "登录成功", gin.H{
|
||||
"redirect": "/admin",
|
||||
"avatar": user.Avatar,
|
||||
"nickname": user.Nickname,
|
||||
"username": user.Username,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -282,7 +257,7 @@ type JWTClaims struct {
|
||||
// - 包含管理员用户名信息和密码哈希
|
||||
// - 设置过期时间
|
||||
// - 使用HMAC-SHA256签名
|
||||
func generateJWTTokenForAdmin(username, passwordHash string) (string, error) {
|
||||
func generateJWTTokenForAdmin(username, passwordHash string, adminUUID string) (string, error) {
|
||||
// 生成密码哈希摘要(使用SHA256)
|
||||
// 注意:传入的 passwordHash 已经是数据库存的 Hash,这里我们再次 Hash 还是直接用?
|
||||
// atomicLibrary 的实现是: utils.GenerateSHA256Hash(adminUser.Password)
|
||||
@@ -292,9 +267,6 @@ func generateJWTTokenForAdmin(username, passwordHash string) (string, error) {
|
||||
// 所以这里也应该对数据库里的值进行 Hash。
|
||||
passwordHashDigest := utils.GenerateSHA256Hash(passwordHash)
|
||||
|
||||
// 获取虚拟管理员UUID (NetworkAuth 项目默认为 admin-uuid-001)
|
||||
adminUUID := services.GetSettingsService().GetString("admin_uuid", "admin-uuid-001")
|
||||
|
||||
claims := JWTClaims{
|
||||
Username: username,
|
||||
UUID: adminUUID,
|
||||
@@ -352,16 +324,16 @@ func validateAdminPasswordHash(claims *JWTClaims, c *gin.Context) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// 获取当前数据库中的管理员密码
|
||||
var adminPassword models.Settings
|
||||
if err := db.Where("name = ?", "admin_password").First(&adminPassword).Error; err != nil {
|
||||
fmt.Printf("[SECURITY WARNING] Admin password not found in database - Username=%s, IP=%s\n",
|
||||
// 获取当前数据库中的管理员用户
|
||||
var adminUser models.User
|
||||
if err := db.Where("username = ? AND role = ?", claims.Username, 0).First(&adminUser).Error; err != nil {
|
||||
fmt.Printf("[SECURITY WARNING] Admin user not found in database - Username=%s, IP=%s\n",
|
||||
claims.Username, c.ClientIP())
|
||||
return false
|
||||
}
|
||||
|
||||
// 生成当前数据库密码的哈希摘要
|
||||
currentPasswordHash := utils.GenerateSHA256Hash(adminPassword.Value)
|
||||
currentPasswordHash := utils.GenerateSHA256Hash(adminUser.Password)
|
||||
|
||||
// 验证JWT中的密码哈希是否与当前数据库中的密码哈希一致
|
||||
if claims.PasswordHash != currentPasswordHash {
|
||||
@@ -417,12 +389,13 @@ func IsAdminAuthenticatedHttp(r *http.Request) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var adminPassword models.Settings
|
||||
if err := db.Where("name = ?", "admin_password").First(&adminPassword).Error; err != nil {
|
||||
var adminUser models.User
|
||||
if err := db.Where("username = ? AND role = ?", claims.Username, 0).First(&adminUser).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
currentPasswordHash := utils.GenerateSHA256Hash(adminPassword.Value)
|
||||
// 验证密码哈希
|
||||
currentPasswordHash := utils.GenerateSHA256Hash(adminUser.Password)
|
||||
if claims.PasswordHash != currentPasswordHash {
|
||||
return false
|
||||
}
|
||||
@@ -518,11 +491,11 @@ func GetCurrentAdminUserWithRefresh(c *gin.Context) (*JWTClaims, bool, error) {
|
||||
if time.Until(claims.ExpiresAt.Time) < refreshThreshold {
|
||||
// 获取当前的 PasswordHash
|
||||
db, _ := database.GetDB()
|
||||
var adminPassword models.Settings
|
||||
db.Where("name = ?", "admin_password").First(&adminPassword)
|
||||
var adminUser models.User
|
||||
db.Where("username = ? AND role = ?", claims.Username, 0).First(&adminUser)
|
||||
|
||||
// 使用新的有效期生成令牌
|
||||
newToken, err := generateJWTTokenForAdmin(claims.Username, adminPassword.Value)
|
||||
newToken, err := generateJWTTokenForAdmin(claims.Username, adminUser.Password, claims.UUID)
|
||||
if err == nil {
|
||||
tokenToSet = newToken
|
||||
refreshed = true
|
||||
@@ -553,19 +526,12 @@ func AdminAuthRequired() gin.HandlerFunc {
|
||||
// 自动清理失效的JWT Cookie,提升安全性和用户体验
|
||||
clearInvalidJWTCookie(c)
|
||||
|
||||
// 中文注释:区分普通页面请求与AJAX/JSON请求
|
||||
accept := c.GetHeader("Accept")
|
||||
xrw := strings.ToLower(strings.TrimSpace(c.GetHeader("X-Requested-With")))
|
||||
if strings.Contains(accept, "application/json") || xrw == "xmlhttprequest" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"success": false,
|
||||
"message": "未登录或会话已过期",
|
||||
"data": nil,
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Redirect(http.StatusFound, "/admin/login")
|
||||
// API 请求直接返回 401 JSON
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"success": false,
|
||||
"message": "未登录或会话已过期",
|
||||
"data": nil,
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mojocn/base64Captcha"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
@@ -100,8 +101,10 @@ func VerifyCaptcha(c *gin.Context, captchaValue string) bool {
|
||||
// 从cookie中获取验证码ID
|
||||
captchaId, err := c.Cookie("captcha_id")
|
||||
if err != nil || captchaId == "" {
|
||||
logrus.WithError(err).Warn("验证码验证失败:无法从Cookie获取captcha_id")
|
||||
return false
|
||||
}
|
||||
logrus.Infof("VerifyCaptcha: received captchaId=%s, captchaValue=%s", captchaId, captchaValue)
|
||||
|
||||
// 先尝试原始值验证
|
||||
if store.Verify(captchaId, captchaValue, false) {
|
||||
|
||||
@@ -6,9 +6,7 @@ import (
|
||||
"NetworkAuth/middleware"
|
||||
"NetworkAuth/models"
|
||||
"NetworkAuth/services"
|
||||
"NetworkAuth/utils"
|
||||
"NetworkAuth/utils/timeutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/spf13/viper"
|
||||
@@ -42,86 +40,12 @@ func formatDBType(dbType string) string {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 页面处理器
|
||||
// ============================================================================
|
||||
|
||||
// AdminIndexHandler 后台首页处理器/admin 与 /admin/ 根路径入口
|
||||
// - 未登录:重定向到 /admin/login
|
||||
// - 已登录:渲染后台布局页(或重定向到 /admin/layout)
|
||||
// - 自动清理失效的JWT Cookie
|
||||
func AdminIndexHandler(c *gin.Context) {
|
||||
if IsAdminAuthenticatedWithCleanup(c) {
|
||||
// 直接渲染布局页,保持URL为 /admin
|
||||
AdminLayoutHandler(c)
|
||||
return
|
||||
}
|
||||
c.Redirect(http.StatusFound, "/admin/login")
|
||||
}
|
||||
|
||||
// AdminLayoutHandler 后台布局页渲染
|
||||
// - 渲染 layout.html,包含顶部导航、侧边栏与动态内容容器
|
||||
func AdminLayoutHandler(c *gin.Context) {
|
||||
// 获取或生成CSRF令牌
|
||||
var token string
|
||||
if existingToken := utils.GetCSRFTokenFromCookie(c); existingToken != "" {
|
||||
// 重用现有的Cookie令牌
|
||||
token = existingToken
|
||||
} else {
|
||||
// 生成新的CSRF令牌并设置到Cookie
|
||||
newToken, err := utils.GenerateCSRFToken()
|
||||
if err != nil {
|
||||
handlersBaseController.HandleInternalError(c, "生成CSRF令牌失败", err)
|
||||
return
|
||||
}
|
||||
token = newToken
|
||||
utils.SetCSRFToken(c, token)
|
||||
}
|
||||
|
||||
// 准备模板数据
|
||||
data := handlersBaseController.GetDefaultTemplateData()
|
||||
data["CSRFToken"] = token
|
||||
|
||||
// 从数据库读取站点标题,如果失败则使用默认值
|
||||
settingsSvc := services.GetSettingsService()
|
||||
data["Title"] = settingsSvc.GetString("site_title", "后台管理")
|
||||
|
||||
// 合并其他数据(如果有的话)
|
||||
extraData := gin.H{}
|
||||
for key, value := range extraData {
|
||||
data[key] = value
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "layout.html", data)
|
||||
}
|
||||
|
||||
// DashboardFragmentHandler 仪表盘片段渲染
|
||||
// - 展示系统信息:版本、开发模式、数据库类型、启动时长
|
||||
func DashboardFragmentHandler(c *gin.Context) {
|
||||
version := constants.AppVersion
|
||||
mode := middleware.IsDevModeFromContext(c)
|
||||
dbType := viper.GetString("database.type")
|
||||
if dbType == "" {
|
||||
dbType = "sqlite"
|
||||
}
|
||||
uptime := timeutil.GetServerUptimeString()
|
||||
|
||||
data := gin.H{
|
||||
"Version": version,
|
||||
"Mode": mode,
|
||||
"DBType": formatDBType(dbType),
|
||||
"Uptime": uptime,
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "dashboard.html", data)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API处理器
|
||||
// ============================================================================
|
||||
|
||||
// SystemInfoHandler 系统信息API接口
|
||||
// - 返回系统运行状态的JSON数据,用于前端定时刷新
|
||||
// 返回系统运行状态的JSON数据,用于前端定时刷新
|
||||
func SystemInfoHandler(c *gin.Context) {
|
||||
version := constants.AppVersion
|
||||
mode := middleware.IsDevModeFromContext(c)
|
||||
@@ -130,19 +54,21 @@ func SystemInfoHandler(c *gin.Context) {
|
||||
dbType = "sqlite"
|
||||
}
|
||||
uptime := timeutil.GetServerUptimeString()
|
||||
uptimeSeconds := int64(timeutil.GetServerUptime().Seconds())
|
||||
|
||||
data := gin.H{
|
||||
"version": version,
|
||||
"mode": mode,
|
||||
"db_type": formatDBType(dbType),
|
||||
"uptime": uptime,
|
||||
"version": version,
|
||||
"mode": mode,
|
||||
"db_type": formatDBType(dbType),
|
||||
"uptime": uptime,
|
||||
"uptime_seconds": uptimeSeconds,
|
||||
}
|
||||
|
||||
handlersBaseController.HandleSuccess(c, "ok", data)
|
||||
}
|
||||
|
||||
// DashboardStatsHandler 仪表盘统计数据API接口
|
||||
// - 返回应用统计数据的JSON数据,包括全部/启用/禁用/变量数量
|
||||
// - 返回应用统计数据的JSON数据,包括全部/启用/变量数量
|
||||
func DashboardStatsHandler(c *gin.Context) {
|
||||
// 获取数据库连接
|
||||
db, ok := handlersBaseController.GetDB(c)
|
||||
@@ -152,8 +78,7 @@ func DashboardStatsHandler(c *gin.Context) {
|
||||
|
||||
// 统计应用数据
|
||||
var totalApps int64
|
||||
var enabledApps int64
|
||||
var disabledApps int64
|
||||
var totalFunctions int64
|
||||
var totalVariables int64
|
||||
|
||||
// 统计全部应用数量
|
||||
@@ -162,15 +87,9 @@ func DashboardStatsHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 统计启用应用数量
|
||||
if err := db.Model(&models.App{}).Where("status = ?", 1).Count(&enabledApps).Error; err != nil {
|
||||
handlersBaseController.HandleInternalError(c, "统计启用应用数量失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 统计禁用应用数量
|
||||
if err := db.Model(&models.App{}).Where("status = ?", 0).Count(&disabledApps).Error; err != nil {
|
||||
handlersBaseController.HandleInternalError(c, "统计禁用应用数量失败", err)
|
||||
// 统计函数数量
|
||||
if err := db.Model(&models.Function{}).Count(&totalFunctions).Error; err != nil {
|
||||
handlersBaseController.HandleInternalError(c, "统计函数数量失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -182,8 +101,7 @@ func DashboardStatsHandler(c *gin.Context) {
|
||||
|
||||
data := gin.H{
|
||||
"total_apps": totalApps,
|
||||
"enabled_apps": enabledApps,
|
||||
"disabled_apps": disabledApps,
|
||||
"total_functions": totalFunctions,
|
||||
"total_variables": totalVariables,
|
||||
}
|
||||
|
||||
|
||||
@@ -20,17 +20,6 @@ import (
|
||||
// 创建基础控制器实例
|
||||
var functionBaseController = controllers.NewBaseController()
|
||||
|
||||
// ============================================================================
|
||||
// 页面处理器
|
||||
// ============================================================================
|
||||
|
||||
// FunctionFragmentHandler 公共函数列表页面片段处理器
|
||||
func FunctionFragmentHandler(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "functions.html", gin.H{
|
||||
"Title": "公共函数",
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API处理器
|
||||
// ============================================================================
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"NetworkAuth/controllers"
|
||||
"NetworkAuth/models"
|
||||
"NetworkAuth/services"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -45,13 +44,6 @@ func RecordLoginLog(c *gin.Context, username string, status int, message string)
|
||||
}
|
||||
}
|
||||
|
||||
// LoginLogsFragmentHandler 登录日志页面片段处理器
|
||||
func LoginLogsFragmentHandler(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "login_logs.html", gin.H{
|
||||
"Title": "登录日志",
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API处理器
|
||||
// ============================================================================
|
||||
@@ -61,7 +53,7 @@ func LoginLogsListHandler(c *gin.Context) {
|
||||
// 获取分页参数
|
||||
page, limit := loginLogBaseController.GetPaginationParams(c)
|
||||
|
||||
// 构建查询
|
||||
// 获取数据库连接
|
||||
db, ok := loginLogBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
@@ -132,17 +124,19 @@ func LoginLogsClearHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
// 由于 NetworkAuth 中没有 SystemAdminUser 全局变量,这里暂时使用 "admin"
|
||||
operator := "admin"
|
||||
// 尝试从上下文获取用户名(如果中间件设置了的话)
|
||||
// if user, exists := c.Get("username"); exists {
|
||||
// operator = user.(string)
|
||||
// }
|
||||
var operator, operatorUUID string
|
||||
if claims, _, err := GetCurrentAdminUserWithRefresh(c); err == nil && claims != nil {
|
||||
operator = claims.Username
|
||||
operatorUUID = claims.UUID
|
||||
} else {
|
||||
operator = "admin"
|
||||
operatorUUID = "00000000-0000-0000-0000-000000000000"
|
||||
}
|
||||
|
||||
log := models.OperationLog{
|
||||
OperationType: "清空登录日志",
|
||||
Operator: operator,
|
||||
OperatorUUID: "", // NetworkAuth 中暂时无法获取 UUID
|
||||
OperatorUUID: operatorUUID,
|
||||
Details: "管理员清空了所有登录日志",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"NetworkAuth/controllers"
|
||||
"NetworkAuth/models"
|
||||
"NetworkAuth/services"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -19,17 +18,6 @@ import (
|
||||
|
||||
var logBaseController = controllers.NewBaseController()
|
||||
|
||||
// ============================================================================
|
||||
// 页面处理器
|
||||
// ============================================================================
|
||||
|
||||
// LogsFragmentHandler 日志操作页面片段处理器
|
||||
func LogsFragmentHandler(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "operation_logs.html", gin.H{
|
||||
"Title": "操作日志",
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API处理器
|
||||
// ============================================================================
|
||||
@@ -92,11 +80,19 @@ func LogsClearHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 记录操作日志 (因为刚刚清空了,这条将是第一条)
|
||||
operator := "admin"
|
||||
var operator, operatorUUID string
|
||||
if claims, _, err := GetCurrentAdminUserWithRefresh(c); err == nil && claims != nil {
|
||||
operator = claims.Username
|
||||
operatorUUID = claims.UUID
|
||||
} else {
|
||||
operator = "admin"
|
||||
operatorUUID = "00000000-0000-0000-0000-000000000000"
|
||||
}
|
||||
|
||||
log := models.OperationLog{
|
||||
OperationType: "清空日志",
|
||||
Operator: operator,
|
||||
OperatorUUID: "",
|
||||
OperatorUUID: operatorUUID,
|
||||
Details: "管理员清空了所有操作日志",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
@@ -5,38 +5,41 @@ import (
|
||||
"NetworkAuth/models"
|
||||
"NetworkAuth/services"
|
||||
"NetworkAuth/utils"
|
||||
"net/http"
|
||||
"fmt"
|
||||
"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)
|
||||
// ProfileQueryHandler 获取当前登录管理员的用户名和昵称等信息
|
||||
// - 返回 JSON: {username, nickname, avatar}
|
||||
// - 从数据库获取最新信息
|
||||
func ProfileQueryHandler(c *gin.Context) {
|
||||
claims, _, err := GetCurrentAdminUserWithRefresh(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 1,
|
||||
"msg": "未登录或会话已过期",
|
||||
"data": nil,
|
||||
})
|
||||
authBaseController.HandleValidationError(c, "未登录或会话已过期")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取最新设置
|
||||
settingsService := services.GetSettingsService()
|
||||
username := settingsService.GetString("admin_username", "admin")
|
||||
db, ok := authBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var adminUser models.User
|
||||
if err := db.Where("uuid = ?", claims.UUID).First(&adminUser).Error; err != nil {
|
||||
authBaseController.HandleInternalError(c, "获取管理员信息失败", err)
|
||||
return
|
||||
}
|
||||
username := adminUser.Username
|
||||
nickname := adminUser.Nickname
|
||||
avatar := adminUser.Avatar
|
||||
|
||||
authBaseController.HandleSuccess(c, "ok", map[string]interface{}{
|
||||
authBaseController.HandleSuccess(c, "ok", gin.H{
|
||||
"username": username,
|
||||
"nickname": nickname,
|
||||
"avatar": avatar,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -44,31 +47,34 @@ func ProfileInfoHandler(c *gin.Context) {
|
||||
// - 接收 JSON: {old_password, new_password, confirm_password}
|
||||
// - 校验旧密码正确性、新密码与确认一致性
|
||||
// - 成功后更新密码哈希
|
||||
// - 自动刷新接近过期的JWT令牌
|
||||
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, "旧密码/新密码/确认密码均不能为空")
|
||||
// 获取当前用户信息用于日志记录
|
||||
claims, _, err := GetCurrentAdminUserWithRefresh(c)
|
||||
if err != nil {
|
||||
authBaseController.HandleValidationError(c, "未登录或会话已过期")
|
||||
return
|
||||
}
|
||||
|
||||
// 基础校验
|
||||
if !authBaseController.ValidateRequired(c, map[string]interface{}{
|
||||
"旧密码": body.OldPassword,
|
||||
"新密码": body.NewPassword,
|
||||
"确认密码": body.ConfirmPassword,
|
||||
}) {
|
||||
return
|
||||
}
|
||||
|
||||
if len(body.NewPassword) < 6 {
|
||||
authBaseController.HandleValidationError(c, "新密码长度不能少于6位")
|
||||
return
|
||||
@@ -82,10 +88,29 @@ func ProfilePasswordUpdateHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前密码设置
|
||||
settingsService := services.GetSettingsService()
|
||||
currentHash := settingsService.GetString("admin_password", "")
|
||||
currentSalt := settingsService.GetString("admin_password_salt", "")
|
||||
// 注释:由于使用了AdminAuthRequired中间件,已确保是管理员用户
|
||||
|
||||
// 获取数据库连接
|
||||
db, ok := authBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 从数据库获取当前管理员信息
|
||||
var adminUser models.User
|
||||
if err := db.Where("uuid = ?", claims.UUID).First(&adminUser).Error; err != nil {
|
||||
authBaseController.HandleInternalError(c, "获取管理员信息失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
currentHash := adminUser.Password
|
||||
currentSalt := adminUser.PasswordSalt
|
||||
|
||||
// 检查必要的设置是否存在
|
||||
if currentHash == "" || currentSalt == "" {
|
||||
authBaseController.HandleInternalError(c, "管理员密码设置不完整", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 校验旧密码
|
||||
if !utils.VerifyPasswordWithSalt(body.OldPassword, currentSalt, currentHash) {
|
||||
@@ -93,91 +118,58 @@ func ProfilePasswordUpdateHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 生成新盐值和哈希
|
||||
// 生成新的密码盐值
|
||||
newSalt, err := utils.GenerateRandomSalt()
|
||||
if err != nil {
|
||||
authBaseController.HandleInternalError(c, "生成盐值失败", err)
|
||||
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
|
||||
}
|
||||
// 更新到数据库
|
||||
err = db.Transaction(func(tx *gorm.DB) error {
|
||||
// 更新密码和盐值
|
||||
return tx.Model(&models.User{}).Where("uuid = ?", claims.UUID).Updates(map[string]interface{}{
|
||||
"password": newHash,
|
||||
"password_salt": newSalt,
|
||||
}).Error
|
||||
})
|
||||
|
||||
// 更新 admin_password
|
||||
if err := updateSetting(db, "admin_password", newHash); err != nil {
|
||||
if 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)
|
||||
|
||||
// 记录操作日志
|
||||
operator := c.GetString("admin_username")
|
||||
if operator == "" {
|
||||
operator = "unknown"
|
||||
}
|
||||
operatorUUID := c.GetString("admin_uuid")
|
||||
services.RecordOperationLog("修改密码", claims.Username, claims.UUID, "管理员修改了登录密码")
|
||||
|
||||
services.RecordOperationLog(
|
||||
"修改密码",
|
||||
operator,
|
||||
operatorUUID,
|
||||
"管理员修改了登录密码",
|
||||
)
|
||||
|
||||
authBaseController.HandleSuccess(c, "密码修改成功", nil)
|
||||
authBaseController.HandleSuccess(c, "密码修改成功,请重新登录", gin.H{
|
||||
"redirect": "/admin/login",
|
||||
})
|
||||
}
|
||||
|
||||
// ProfileUpdateHandler 修改当前登录管理员的用户名
|
||||
// - 接收 JSON: {username}
|
||||
// - 校验用户名非空、长度
|
||||
// ProfileUpdateHandler 修改当前登录管理员的资料(用户名、昵称、头像)
|
||||
// - 接收 JSON: {username, nickname, avatar, old_password}
|
||||
// - 校验旧密码正确性
|
||||
// - 更新数据库后重新签发JWT并写入 Cookie,保持前端展示的一致性
|
||||
// - 自动刷新接近过期的JWT令牌
|
||||
func ProfileUpdateHandler(c *gin.Context) {
|
||||
_, _, err := GetCurrentAdminUserWithRefresh(c)
|
||||
claims, _, err := GetCurrentAdminUserWithRefresh(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 1,
|
||||
"msg": "未登录或会话已过期",
|
||||
"data": nil,
|
||||
})
|
||||
authBaseController.HandleValidationError(c, "未登录或会话已过期")
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
Avatar string `json:"avatar"`
|
||||
OldPassword string `json:"old_password"`
|
||||
}
|
||||
if !authBaseController.BindJSON(c, &body) {
|
||||
@@ -185,6 +177,9 @@ func ProfileUpdateHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
username := strings.TrimSpace(body.Username)
|
||||
nickname := strings.TrimSpace(body.Nickname)
|
||||
avatar := strings.TrimSpace(body.Avatar)
|
||||
|
||||
if username == "" {
|
||||
authBaseController.HandleValidationError(c, "用户名不能为空")
|
||||
return
|
||||
@@ -193,75 +188,86 @@ func ProfileUpdateHandler(c *gin.Context) {
|
||||
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,
|
||||
})
|
||||
if len(nickname) > 64 {
|
||||
authBaseController.HandleValidationError(c, "昵称长度不能超过64字符")
|
||||
return
|
||||
}
|
||||
if len(avatar) > 255 {
|
||||
authBaseController.HandleValidationError(c, "头像URL长度不能超过255字符")
|
||||
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)
|
||||
// 注释:由于使用了AdminAuthRequired中间件,已确保是管理员用户
|
||||
|
||||
// 从数据库获取当前管理员信息
|
||||
var adminUser models.User
|
||||
if err := db.Where("uuid = ?", claims.UUID).First(&adminUser).Error; err != nil {
|
||||
authBaseController.HandleInternalError(c, "获取管理员信息失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 刷新缓存
|
||||
settingsService.RefreshCache()
|
||||
_ = utils.RedisDel(c.Request.Context(), "setting:admin_username")
|
||||
adminUsername := adminUser.Username
|
||||
adminNickname := adminUser.Nickname
|
||||
adminAvatar := adminUser.Avatar
|
||||
adminPassword := adminUser.Password
|
||||
adminPasswordSalt := adminUser.PasswordSalt
|
||||
|
||||
// 检查必要的设置是否存在
|
||||
if adminUsername == "" || adminPassword == "" || adminPasswordSalt == "" {
|
||||
authBaseController.HandleInternalError(c, "管理员设置不完整", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果用户名、昵称和头像都未变化则直接返回成功(无需校验旧密码)
|
||||
if strings.EqualFold(username, adminUsername) && nickname == adminNickname && avatar == adminAvatar {
|
||||
authBaseController.HandleSuccess(c, "保存成功", gin.H{
|
||||
"username": username,
|
||||
"nickname": nickname,
|
||||
"avatar": avatar,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 如果只修改昵称或头像,不需要验证密码
|
||||
if !strings.EqualFold(username, adminUsername) {
|
||||
// 修改用户名需要进行当前密码校验
|
||||
if strings.TrimSpace(body.OldPassword) == "" {
|
||||
authBaseController.HandleValidationError(c, "修改账号需要提供当前密码")
|
||||
return
|
||||
}
|
||||
|
||||
// 使用盐值验证当前密码
|
||||
if !utils.VerifyPasswordWithSalt(body.OldPassword, adminPasswordSalt, adminPassword) {
|
||||
authBaseController.HandleValidationError(c, "当前密码不正确")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 更新管理员资料
|
||||
if dbErr := db.Model(&models.User{}).Where("uuid = ?", claims.UUID).Updates(map[string]interface{}{
|
||||
"username": username,
|
||||
"nickname": nickname,
|
||||
"avatar": avatar,
|
||||
}).Error; dbErr != nil {
|
||||
authBaseController.HandleInternalError(c, "更新管理员资料失败", dbErr)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前管理员并刷新Token(这会生成包含新用户名的Token并更新Cookie)
|
||||
_, _, _ = GetCurrentAdminUserWithRefresh(c)
|
||||
|
||||
// 记录操作日志
|
||||
operator := c.GetString("admin_username")
|
||||
if operator == "" {
|
||||
operator = "unknown"
|
||||
}
|
||||
operatorUUID := c.GetString("admin_uuid")
|
||||
services.RecordOperationLog("修改资料", claims.Username, claims.UUID, fmt.Sprintf("管理员修改资料为 用户名: %s, 昵称: %s, 头像: %s", username, nickname, avatar))
|
||||
|
||||
services.RecordOperationLog(
|
||||
"修改账号",
|
||||
operator,
|
||||
operatorUUID,
|
||||
"管理员修改了用户名为: "+username,
|
||||
)
|
||||
|
||||
// 重新签发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)
|
||||
|
||||
authBaseController.HandleSuccess(c, "用户名修改成功", map[string]interface{}{
|
||||
authBaseController.HandleSuccess(c, "保存成功", gin.H{
|
||||
"username": username,
|
||||
"nickname": nickname,
|
||||
"avatar": avatar,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -13,12 +13,6 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// SettingsFragmentHandler 设置片段渲染
|
||||
// - 渲染设置表单(通过前端JS调用API加载/保存)
|
||||
func SettingsFragmentHandler(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "settings.html", map[string]interface{}{})
|
||||
}
|
||||
|
||||
// SubAccountSimpleListHandler 子账号简单列表API处理器 (Mock)
|
||||
func SubAccountSimpleListHandler(c *gin.Context) {
|
||||
// Mock implementation for NetworkAuth which has no subaccounts
|
||||
@@ -67,6 +61,11 @@ func SettingsUpdateHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var categoryStr string
|
||||
if category, ok := directBody["category"].(string); ok {
|
||||
categoryStr = category
|
||||
}
|
||||
|
||||
// 提取设置数据
|
||||
var settingsData map[string]string
|
||||
|
||||
@@ -87,6 +86,9 @@ func SettingsUpdateHandler(c *gin.Context) {
|
||||
// 直接字段格式
|
||||
settingsData = make(map[string]string)
|
||||
for k, v := range directBody {
|
||||
if k == "category" {
|
||||
continue // 忽略 category 字段,不保存到设置表
|
||||
}
|
||||
if str, ok := v.(string); ok {
|
||||
settingsData[k] = str
|
||||
} else if v != nil {
|
||||
@@ -119,59 +121,6 @@ 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 {
|
||||
// 不存在则创建
|
||||
@@ -201,6 +150,23 @@ func SettingsUpdateHandler(c *gin.Context) {
|
||||
// 刷新内存中的设置缓存,保证后续读取一致
|
||||
services.GetSettingsService().RefreshCache()
|
||||
|
||||
// 获取当前操作人信息
|
||||
claims, _, err := GetCurrentAdminUserWithRefresh(c)
|
||||
var operator, operatorUUID string
|
||||
if err == nil && claims != nil {
|
||||
operator = claims.Username
|
||||
operatorUUID = claims.UUID
|
||||
} else {
|
||||
operator = "system"
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
logType := "系统设置"
|
||||
if categoryStr != "" {
|
||||
logType = fmt.Sprintf("系统设置-%s", categoryStr)
|
||||
}
|
||||
services.RecordOperationLog(logType, operator, operatorUUID, fmt.Sprintf("管理员更新了系统设置,包含 %d 个配置项", len(settingsData)))
|
||||
|
||||
authBaseController.HandleSuccess(c, "保存成功", nil)
|
||||
}
|
||||
|
||||
@@ -253,3 +219,24 @@ func SettingsGenerateKeyHandler(c *gin.Context) {
|
||||
|
||||
authBaseController.HandleSuccess(c, "生成成功", map[string]string{"key": key})
|
||||
}
|
||||
|
||||
// SettingsPublicHandler 公开设置查询API
|
||||
// - 仅返回允许公开的设置项以及所有前端平台配置
|
||||
func SettingsPublicHandler(c *gin.Context) {
|
||||
db, ok := authBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var list []models.Settings
|
||||
// 查询公开的基本信息、维护模式和所有前端平台配置
|
||||
if err := db.Where("name IN ? OR name LIKE ?", []string{"site_title", "site_description", "site_keywords", "site_logo", "contact_email", "maintenance_mode"}, "platform_%").Find(&list).Error; err != nil {
|
||||
authBaseController.HandleInternalError(c, "查询失败", err)
|
||||
return
|
||||
}
|
||||
res := map[string]string{}
|
||||
for _, s := range list {
|
||||
res[s.Name] = s.Value
|
||||
}
|
||||
authBaseController.HandleSuccess(c, "ok", res)
|
||||
}
|
||||
|
||||
@@ -20,17 +20,6 @@ import (
|
||||
// 创建基础控制器实例
|
||||
var variableBaseController = controllers.NewBaseController()
|
||||
|
||||
// ============================================================================
|
||||
// 页面处理器
|
||||
// ============================================================================
|
||||
|
||||
// VariableFragmentHandler 公共变量列表页面片段处理器
|
||||
func VariableFragmentHandler(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "variables.html", gin.H{
|
||||
"Title": "公共变量",
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API处理器
|
||||
// ============================================================================
|
||||
|
||||
@@ -237,8 +237,7 @@ func (bc *BaseController) BindURI(c *gin.Context, obj interface{}) bool {
|
||||
func (bc *BaseController) GetDefaultTemplateData() gin.H {
|
||||
settings := services.GetSettingsService()
|
||||
return gin.H{
|
||||
"Title": settings.GetString("site_title", "NetworkAuth"),
|
||||
"SystemName": settings.GetString("site_title", "NetworkAuth"),
|
||||
"Title": settings.GetString("site_title", "NetworkAuth 系统"),
|
||||
"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"),
|
||||
|
||||
@@ -3,30 +3,26 @@ package default_ctrl
|
||||
import (
|
||||
"NetworkAuth/services"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RootHandler 根路径处理器
|
||||
// 使用模板渲染服务器信息页面
|
||||
// 返回服务器信息 JSON
|
||||
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(),
|
||||
// 传递数据
|
||||
data := gin.H{
|
||||
"title": settings.GetString("site_title", "NetworkAuth Server"),
|
||||
"description": settings.GetString("site_description", ""),
|
||||
"status": "running",
|
||||
"message": "NetworkAuth API Server is running",
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "index.html", data)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,20 +6,15 @@ import (
|
||||
"NetworkAuth/models"
|
||||
"NetworkAuth/services"
|
||||
"NetworkAuth/utils"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
@@ -58,7 +53,24 @@ func InstallSubmitHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 重新初始化数据库连接并执行迁移
|
||||
// 2. 使用新配置尝试连接数据库
|
||||
var testDB *gorm.DB
|
||||
if req.DbType == "mysql" {
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
req.DbUser, req.DbPass, req.DbHost, req.DbPort, req.DbName)
|
||||
testDB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "连接 MySQL 数据库失败,请检查配置是否正确: " + err.Error()})
|
||||
return
|
||||
}
|
||||
sqlDB, err := testDB.DB()
|
||||
if err != nil || sqlDB.Ping() != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "连接 MySQL 数据库失败,无法 Ping 通,请检查配置是否正确"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 重新初始化全局数据库连接并执行迁移
|
||||
db, err := database.ReInit()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "连接数据库失败: " + err.Error()})
|
||||
@@ -66,7 +78,7 @@ func InstallSubmitHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
if db == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "获取数据库实例失败"})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "获取数据库实例失败,请检查数据库配置是否正确"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -91,17 +103,55 @@ func InstallSubmitHandler(c *gin.Context) {
|
||||
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()
|
||||
|
||||
// 更新或创建超级管理员账号
|
||||
var adminUser models.User
|
||||
if err := tx.Where("uuid = ?", "00000000-0000-0000-0000-000000000000").First(&adminUser).Error; err != nil {
|
||||
// 如果不存在则创建
|
||||
adminUser = models.User{
|
||||
UUID: "00000000-0000-0000-0000-000000000000",
|
||||
Username: strings.TrimSpace(req.AdminUsername),
|
||||
Password: adminPasswordHash,
|
||||
PasswordSalt: adminSalt,
|
||||
Nickname: "管理员",
|
||||
Avatar: "",
|
||||
Role: 0,
|
||||
Status: 1,
|
||||
Remark: "系统默认超级管理员",
|
||||
}
|
||||
// 使用 Select("Role") 确保 Role 字段(值为0时是零值)被显式插入,避免使用数据库默认值 1
|
||||
if err := tx.Select("UUID", "Username", "Password", "PasswordSalt", "Nickname", "Avatar", "Role", "Status", "Remark").Create(&adminUser).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "创建管理员账号失败"})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 存在则更新
|
||||
adminUser.Username = strings.TrimSpace(req.AdminUsername)
|
||||
adminUser.Password = adminPasswordHash
|
||||
adminUser.PasswordSalt = adminSalt
|
||||
adminUser.Nickname = "管理员"
|
||||
adminUser.Role = 0
|
||||
if err := tx.Save(&adminUser).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "更新管理员账号失败"})
|
||||
return
|
||||
}
|
||||
// 确保角色被更新为0(GORM的Save可能忽略零值,所以额外Update一次)
|
||||
tx.Model(&adminUser).Update("Role", 0)
|
||||
}
|
||||
|
||||
// 如果是新创建的,再额外确保一次 Role 为 0,避免 default 标签导致的零值问题
|
||||
tx.Model(&adminUser).Update("Role", 0)
|
||||
|
||||
// 4. 更新设置表
|
||||
settingsToUpdate := map[string]string{
|
||||
"site_title": req.SiteTitle,
|
||||
"is_installed": "1", // 标记为已安装
|
||||
}
|
||||
|
||||
for name, value := range settingsToUpdate {
|
||||
// 先尝试更新,如果没有该记录,则忽略(因为 AutoMigrate 已经创建了默认记录)
|
||||
if err := tx.Model(&models.Settings{}).Where("name = ?", name).Update("value", value).Error; err != nil {
|
||||
@@ -113,10 +163,7 @@ func InstallSubmitHandler(c *gin.Context) {
|
||||
tx.Commit()
|
||||
|
||||
// 5. 更新内存缓存
|
||||
settingsService := services.GetSettingsService()
|
||||
for name, value := range settingsToUpdate {
|
||||
settingsService.Set(name, value)
|
||||
}
|
||||
services.ResetSettingsService()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "安装成功"})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user