修改项目为前后端分离方案

This commit is contained in:
2026-03-28 23:30:02 +08:00
parent d8536354d4
commit 7a7d3aeaaa
77 changed files with 1447 additions and 23765 deletions

View File

@@ -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)

View File

@@ -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,

View File

@@ -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 CookieHttpOnly安全
// 使用系统配置的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
}

View File

@@ -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) {

View File

@@ -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,
}

View File

@@ -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处理器
// ============================================================================

View File

@@ -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(),
}

View File

@@ -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(),
}

View File

@@ -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,
})
}

View File

@@ -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)
}

View File

@@ -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处理器
// ============================================================================

View File

@@ -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"),

View File

@@ -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,
})
}

View File

@@ -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
}
// 确保角色被更新为0GORM的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": "安装成功"})
}