mirror of
https://github.com/skyle1995/NetworkAuth.git
synced 2026-05-25 02:24:05 +08:00
调整日志和鉴权接管方案
This commit is contained in:
@@ -521,24 +521,42 @@ func AppDeleteHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 删除相关的变量记录
|
||||
if err := tx.Where("app_uuid = ?", app.UUID).Delete(&models.Variable{}).Error; err != nil {
|
||||
// 检查是否有关联的变量
|
||||
var varCount int64
|
||||
if err := tx.Model(&models.Variable{}).Where("app_uuid = ?", app.UUID).Count(&varCount).Error; err != nil {
|
||||
tx.Rollback()
|
||||
logrus.WithError(err).Error("Failed to delete related variables")
|
||||
logrus.WithError(err).Error("Failed to count related variables")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 1,
|
||||
"msg": "删除相关变量失败",
|
||||
"msg": "检查关联变量失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
if varCount > 0 {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 1,
|
||||
"msg": "该应用下存在关联变量,禁止删除",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 删除相关的函数记录
|
||||
if err := tx.Where("app_uuid = ?", app.UUID).Delete(&models.Function{}).Error; err != nil {
|
||||
// 检查是否有关联的函数
|
||||
var funcCount int64
|
||||
if err := tx.Model(&models.Function{}).Where("app_uuid = ?", app.UUID).Count(&funcCount).Error; err != nil {
|
||||
tx.Rollback()
|
||||
logrus.WithError(err).Error("Failed to delete related functions")
|
||||
logrus.WithError(err).Error("Failed to count related functions")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 1,
|
||||
"msg": "删除相关函数失败",
|
||||
"msg": "检查关联函数失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
if funcCount > 0 {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 1,
|
||||
"msg": "该应用下存在关联函数,禁止删除",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -581,7 +599,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, Variables and Functions")
|
||||
}).Debug("Successfully deleted app and related APIs")
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
@@ -1254,6 +1272,46 @@ func AppsBatchDeleteHandler(c *gin.Context) {
|
||||
|
||||
// 删除这些应用的所有相关接口
|
||||
if len(appUUIDs) > 0 {
|
||||
// 检查是否有关联的变量
|
||||
var varCount int64
|
||||
if err := tx.Model(&models.Variable{}).Where("app_uuid IN ?", appUUIDs).Count(&varCount).Error; err != nil {
|
||||
tx.Rollback()
|
||||
logrus.WithError(err).Error("Failed to count related variables")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 1,
|
||||
"msg": "检查关联变量失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
if varCount > 0 {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 1,
|
||||
"msg": "所选应用中存在关联变量,禁止删除",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否有关联的函数
|
||||
var funcCount int64
|
||||
if err := tx.Model(&models.Function{}).Where("app_uuid IN ?", appUUIDs).Count(&funcCount).Error; err != nil {
|
||||
tx.Rollback()
|
||||
logrus.WithError(err).Error("Failed to count related functions")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 1,
|
||||
"msg": "检查关联函数失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
if funcCount > 0 {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 1,
|
||||
"msg": "所选应用中存在关联函数,禁止删除",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := tx.Where("app_uuid IN ?", appUUIDs).Delete(&models.API{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
logrus.WithError(err).Error("Failed to delete related APIs")
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
@@ -83,37 +84,51 @@ func LoginHandler(c *gin.Context) {
|
||||
|
||||
// 验证验证码
|
||||
if !VerifyCaptcha(c, body.Captcha) {
|
||||
recordLoginLog(c, body.Username, 0, "验证码错误")
|
||||
authBaseController.HandleValidationError(c, "验证码错误")
|
||||
recordLoginLog(c, "", body.Username, 0, "验证码错误或已过期")
|
||||
authBaseController.HandleValidationError(c, "验证码错误或已过期")
|
||||
return
|
||||
}
|
||||
|
||||
// 从数据库中查找对应的用户
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
recordLoginLog(c, body.Username, 0, "数据库连接失败")
|
||||
recordLoginLog(c, "", body.Username, 0, "数据库连接失败")
|
||||
authBaseController.HandleInternalError(c, "数据库连接失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := db.Where("username = ? AND role = ?", body.Username, 0).First(&user).Error; err != nil {
|
||||
recordLoginLog(c, body.Username, 0, "用户不存在或非管理员")
|
||||
if err := db.Where("username = ?", body.Username).First(&user).Error; err != nil {
|
||||
recordLoginLog(c, user.UUID, body.Username, 0, "用户不存在")
|
||||
authBaseController.HandleValidationError(c, "用户不存在或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查账号状态 (Status=1 表示启用,否则禁止登录)
|
||||
if user.Status != 1 {
|
||||
recordLoginLog(c, user.UUID, body.Username, 0, "账号已被禁用")
|
||||
authBaseController.HandleValidationError(c, "该账号已被禁用,请联系超级管理员")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否允许登录 (role=0 或 role=1 允许登录,role=2 不允许)
|
||||
if user.Role > 1 {
|
||||
recordLoginLog(c, user.UUID, body.Username, 0, "权限不足")
|
||||
authBaseController.HandleValidationError(c, "权限不足,禁止登录")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证密码(使用盐值校验)
|
||||
if !utils.VerifyPasswordWithSalt(body.Password, user.PasswordSalt, user.Password) {
|
||||
recordLoginLog(c, body.Username, 0, "密码错误")
|
||||
recordLoginLog(c, user.UUID, body.Username, 0, "密码错误")
|
||||
authBaseController.HandleValidationError(c, "用户不存在或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 生成JWT令牌
|
||||
token, err := generateJWTTokenForAdmin(user.Username, user.Password, user.UUID)
|
||||
token, err := generateJWTTokenForAdmin(user.Username, user.Password, user.UUID, user.Role)
|
||||
if err != nil {
|
||||
recordLoginLog(c, body.Username, 0, "生成令牌失败")
|
||||
recordLoginLog(c, user.UUID, body.Username, 0, "生成令牌失败")
|
||||
authBaseController.HandleInternalError(c, "生成令牌失败", err)
|
||||
return
|
||||
}
|
||||
@@ -125,19 +140,20 @@ func LoginHandler(c *gin.Context) {
|
||||
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, "登录成功")
|
||||
recordLoginLog(c, user.UUID, body.Username, 1, "登录成功")
|
||||
authBaseController.HandleSuccess(c, "登录成功", gin.H{
|
||||
"redirect": "/admin",
|
||||
"avatar": user.Avatar,
|
||||
"nickname": user.Nickname,
|
||||
"username": user.Username,
|
||||
"role": user.Role,
|
||||
"token": token,
|
||||
})
|
||||
}
|
||||
|
||||
// recordLoginLog 记录登录日志
|
||||
// status: 1-成功, 0-失败
|
||||
func recordLoginLog(c *gin.Context, username string, status int, message string) {
|
||||
func recordLoginLog(c *gin.Context, uuid string, username string, status int, message string) {
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
// 记录日志失败不应影响主流程,但可以记录到系统日志
|
||||
@@ -147,10 +163,11 @@ func recordLoginLog(c *gin.Context, username string, status int, message string)
|
||||
|
||||
log := models.LoginLog{
|
||||
Type: "admin",
|
||||
UUID: uuid,
|
||||
Username: username,
|
||||
IP: c.ClientIP(),
|
||||
Status: status,
|
||||
Message: message,
|
||||
Message: "登录管理 - " + message,
|
||||
UserAgent: c.Request.UserAgent(),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
@@ -237,8 +254,9 @@ func getJWTSecret() []byte {
|
||||
return []byte(secret)
|
||||
}
|
||||
|
||||
// 3. 使用默认不安全密钥(仅开发环境)
|
||||
return []byte("default-insecure-jwt-secret")
|
||||
// 3. 如果仍未获取到,则记录严重错误并抛出 panic,拒绝使用硬编码的不安全密钥
|
||||
logrus.Fatal("致命安全错误: 无法获取有效的 JWT 密钥,请检查数据库设置或重新安装系统。系统拒绝以不安全模式运行。")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -248,8 +266,8 @@ func getJWTSecret() []byte {
|
||||
// JWTClaims JWT载荷结构体
|
||||
type JWTClaims struct {
|
||||
Username string `json:"username"`
|
||||
UUID string `json:"uuid"` // 添加虚拟角色UUID
|
||||
Role int `json:"role"` // 添加虚拟角色
|
||||
UUID string `json:"uuid"` // 用户UUID
|
||||
Role int `json:"role"` // 用户角色
|
||||
PasswordHash string `json:"password_hash"` // 密码哈希摘要,用于验证密码是否被修改
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
@@ -258,7 +276,7 @@ type JWTClaims struct {
|
||||
// - 包含管理员用户名信息和密码哈希
|
||||
// - 设置过期时间
|
||||
// - 使用HMAC-SHA256签名
|
||||
func generateJWTTokenForAdmin(username, passwordHash string, adminUUID string) (string, error) {
|
||||
func generateJWTTokenForAdmin(username, passwordHash string, adminUUID string, role int) (string, error) {
|
||||
// 生成密码哈希摘要(使用SHA256)
|
||||
// 注意:传入的 passwordHash 已经是数据库存的 Hash,这里我们再次 Hash 还是直接用?
|
||||
// atomicLibrary 的实现是: utils.GenerateSHA256Hash(adminUser.Password)
|
||||
@@ -271,7 +289,7 @@ func generateJWTTokenForAdmin(username, passwordHash string, adminUUID string) (
|
||||
claims := JWTClaims{
|
||||
Username: username,
|
||||
UUID: adminUUID,
|
||||
Role: 0, // 0表示超级管理员
|
||||
Role: role, // 用户真实角色
|
||||
PasswordHash: passwordHashDigest, // 包含密码哈希摘要
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(services.GetSettingsService().GetJWTExpire()) * time.Hour)),
|
||||
@@ -339,9 +357,23 @@ func validateAdminPasswordHash(claims *JWTClaims, c *gin.Context) bool {
|
||||
|
||||
// 获取当前数据库中的管理员用户
|
||||
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())
|
||||
if err := db.Where("uuid = ?", claims.UUID).First(&adminUser).Error; err != nil {
|
||||
fmt.Printf("[SECURITY WARNING] Admin user not found in database - UUID=%s, IP=%s\n",
|
||||
claims.UUID, c.ClientIP())
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查账号状态 (Status=1 表示启用,否则强制下线)
|
||||
if adminUser.Status != 1 {
|
||||
fmt.Printf("[SECURITY WARNING] Admin user is disabled - UUID=%s, IP=%s\n",
|
||||
claims.UUID, c.ClientIP())
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查是否允许登录 (role=0 或 role=1 允许,role=2不允许访问admin后台)
|
||||
if adminUser.Role > 1 {
|
||||
fmt.Printf("[SECURITY WARNING] Admin user role > 1 - UUID=%s, IP=%s\n",
|
||||
claims.UUID, c.ClientIP())
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -413,7 +445,17 @@ func IsAdminAuthenticatedHttp(r *http.Request) bool {
|
||||
}
|
||||
|
||||
var adminUser models.User
|
||||
if err := db.Where("username = ? AND role = ?", claims.Username, 0).First(&adminUser).Error; err != nil {
|
||||
if err := db.Where("uuid = ?", claims.UUID).First(&adminUser).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查账号状态 (Status=1 表示启用,否则强制下线)
|
||||
if adminUser.Status != 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查是否允许登录 (role=0 或 role=1 允许,role=2不允许访问admin后台)
|
||||
if adminUser.Role > 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -525,10 +567,10 @@ func GetCurrentAdminUserWithRefresh(c *gin.Context) (*JWTClaims, bool, error) {
|
||||
// 获取当前的 PasswordHash
|
||||
db, _ := database.GetDB()
|
||||
var adminUser models.User
|
||||
db.Where("username = ? AND role = ?", claims.Username, 0).First(&adminUser)
|
||||
db.Where("uuid = ? AND role = ?", claims.UUID, claims.Role).First(&adminUser)
|
||||
|
||||
// 使用新的有效期生成令牌
|
||||
newToken, err := generateJWTTokenForAdmin(claims.Username, adminUser.Password, claims.UUID)
|
||||
newToken, err := generateJWTTokenForAdmin(claims.Username, adminUser.Password, claims.UUID, adminUser.Role)
|
||||
if err == nil {
|
||||
tokenToSet = newToken
|
||||
refreshed = true
|
||||
|
||||
@@ -1,67 +1,33 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"NetworkAuth/middleware"
|
||||
"NetworkAuth/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mojocn/base64Captcha"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// 全局变量
|
||||
// ============================================================================
|
||||
|
||||
// 全局验证码存储器
|
||||
var store = base64Captcha.DefaultMemStore
|
||||
|
||||
// ============================================================================
|
||||
// 辅助函数
|
||||
// ============================================================================
|
||||
|
||||
// secureRandomInt 生成安全的随机整数,范围 [0, max)
|
||||
func secureRandomInt(max int) (int, error) {
|
||||
n, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(n.Int64()), nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API处理器
|
||||
// ============================================================================
|
||||
|
||||
// CaptchaHandler 生成验证码图片
|
||||
// GET /admin/captcha - 返回验证码图片
|
||||
func CaptchaHandler(c *gin.Context) {
|
||||
// 随机生成4-6位长度
|
||||
// 使用crypto/rand生成安全的随机数
|
||||
randomNum, err := secureRandomInt(3)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "生成随机数失败")
|
||||
return
|
||||
}
|
||||
captchaLength := 4 + randomNum // 4-6位随机长度
|
||||
|
||||
// 配置验证码参数 - 使用字母数字混合
|
||||
// 配置与 User 端一致,采用较弱的验证码强度以提升正常用户体验
|
||||
driver := base64Captcha.DriverString{
|
||||
Height: 60,
|
||||
Width: 200,
|
||||
NoiseCount: 0,
|
||||
ShowLineOptions: 2 | 4,
|
||||
Length: captchaLength,
|
||||
Source: "ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789", // 混合大小写字母和数字,去除易混淆字符
|
||||
Length: 4,
|
||||
NoiseCount: 20, // 加点背景噪点干扰
|
||||
ShowLineOptions: 2 | 4, // 加点干扰线
|
||||
Source: "ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789",
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
captcha := base64Captcha.NewCaptcha(&driver, store)
|
||||
// 生成验证码,使用共享的 CaptchaStore
|
||||
captcha := base64Captcha.NewCaptcha(&driver, utils.CaptchaStore)
|
||||
id, b64s, _, err := captcha.Generate()
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "生成验证码失败")
|
||||
@@ -105,24 +71,6 @@ func VerifyCaptcha(c *gin.Context, captchaValue string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// 先尝试原始值验证
|
||||
if store.Verify(captchaId, captchaValue, false) {
|
||||
// 验证成功后删除验证码
|
||||
store.Verify(captchaId, captchaValue, true)
|
||||
return true
|
||||
}
|
||||
|
||||
// 如果原始值验证失败,尝试小写验证
|
||||
if store.Verify(captchaId, strings.ToLower(captchaValue), false) {
|
||||
// 验证成功后删除验证码
|
||||
store.Verify(captchaId, strings.ToLower(captchaValue), true)
|
||||
return true
|
||||
}
|
||||
|
||||
// 最后尝试大写验证
|
||||
if store.Verify(captchaId, strings.ToUpper(captchaValue), true) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
// 调用共享的 VerifyCaptcha
|
||||
return utils.VerifyCaptcha(captchaId, captchaValue)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"NetworkAuth/controllers"
|
||||
"NetworkAuth/middleware"
|
||||
"NetworkAuth/models"
|
||||
"NetworkAuth/services"
|
||||
"NetworkAuth/utils/timeutil"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -43,8 +42,6 @@ func formatDBType(dbType string) string {
|
||||
// ============================================================================
|
||||
// API处理器
|
||||
// ============================================================================
|
||||
|
||||
// SystemInfoHandler 系统信息API接口
|
||||
// 返回系统运行状态的JSON数据,用于前端定时刷新
|
||||
func SystemInfoHandler(c *gin.Context) {
|
||||
version := constants.AppVersion
|
||||
@@ -107,41 +104,3 @@ func DashboardStatsHandler(c *gin.Context) {
|
||||
|
||||
handlersBaseController.HandleSuccess(c, "ok", data)
|
||||
}
|
||||
|
||||
// DashboardLoginLogsHandler 获取管理员最近登录日志
|
||||
func DashboardLoginLogsHandler(c *gin.Context) {
|
||||
db, ok := handlersBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取分页参数
|
||||
page, limit := handlersBaseController.GetPaginationParams(c)
|
||||
|
||||
// 获取当前管理员信息(可能是 username 或 admin_username,具体取决于认证中间件设置的 key)
|
||||
username := c.GetString("admin_username")
|
||||
if username == "" {
|
||||
// 尝试获取其他可能的键名
|
||||
username = c.GetString("username")
|
||||
}
|
||||
|
||||
var total int64
|
||||
query := db.Model(&models.LoginLog{}).Where("type = ?", "admin")
|
||||
|
||||
// 如果有用户名,则仅过滤该用户的日志
|
||||
if username != "" {
|
||||
query = query.Where("username = ?", username)
|
||||
}
|
||||
|
||||
logs, total, err := services.Paginate[models.LoginLog](query, page, limit, "created_at desc")
|
||||
if err != nil {
|
||||
handlersBaseController.HandleInternalError(c, "获取登录日志失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
data := gin.H{
|
||||
"total": total,
|
||||
"list": logs,
|
||||
}
|
||||
handlersBaseController.HandleSuccess(c, "获取登录日志成功", data)
|
||||
}
|
||||
|
||||
287
controllers/admin/log.go
Normal file
287
controllers/admin/log.go
Normal file
@@ -0,0 +1,287 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"NetworkAuth/controllers"
|
||||
"NetworkAuth/models"
|
||||
"NetworkAuth/services"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// 全局变量
|
||||
// ============================================================================
|
||||
|
||||
var logBaseController = controllers.NewBaseController()
|
||||
|
||||
// ============================================================================
|
||||
// 登录日志 API处理器
|
||||
// ============================================================================
|
||||
|
||||
// DashboardLoginLogsHandler 获取管理员最近登录日志
|
||||
func DashboardLoginLogsHandler(c *gin.Context) {
|
||||
db, ok := logBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取分页参数
|
||||
page, limit := logBaseController.GetPaginationParams(c)
|
||||
|
||||
// 获取当前管理员信息
|
||||
uuid := c.GetString("admin_uuid")
|
||||
username := c.GetString("admin_username")
|
||||
|
||||
var total int64
|
||||
query := db.Model(&models.LoginLog{})
|
||||
|
||||
// 如果有用户名,则仅过滤该用户的日志
|
||||
if uuid != "" {
|
||||
query = query.Where("uuid = ? OR (uuid = '' AND username = ?)", uuid, username)
|
||||
}
|
||||
|
||||
logs, total, err := services.Paginate[models.LoginLog](query, page, limit, "created_at desc")
|
||||
if err != nil {
|
||||
logBaseController.HandleInternalError(c, "获取登录日志失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
data := gin.H{
|
||||
"total": total,
|
||||
"list": logs,
|
||||
}
|
||||
logBaseController.HandleSuccess(c, "获取登录日志成功", data)
|
||||
}
|
||||
|
||||
// LoginLogsListHandler 登录日志列表API处理器
|
||||
func LoginLogsListHandler(c *gin.Context) {
|
||||
// 获取分页参数
|
||||
page, limit := logBaseController.GetPaginationParams(c)
|
||||
|
||||
// 获取数据库连接
|
||||
db, ok := logBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
query := db.Model(&models.LoginLog{})
|
||||
|
||||
// 筛选条件:账号或UUID合并搜索
|
||||
if username := strings.TrimSpace(c.Query("username")); username != "" {
|
||||
query = query.Where("username = ? OR uuid = ?", 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 筛选条件:时间范围
|
||||
query = logBaseController.ApplyTimeRangeQuery(c, query, "created_at")
|
||||
|
||||
// 泛型分页查询
|
||||
logs, total, err := services.Paginate[models.LoginLog](query, page, limit, "created_at DESC")
|
||||
if err != nil {
|
||||
logBaseController.HandleInternalError(c, "获取日志列表失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换数据格式
|
||||
var list []map[string]interface{}
|
||||
for _, log := range logs {
|
||||
list = append(list, map[string]interface{}{
|
||||
"id": log.ID,
|
||||
"uuid": log.UUID,
|
||||
"username": log.Username,
|
||||
"ip": log.IP,
|
||||
"status": log.Status,
|
||||
"message": log.Message,
|
||||
"user_agent": log.UserAgent,
|
||||
"created_at": log.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
logBaseController.HandleSuccess(c, "ok", gin.H{
|
||||
"list": list,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
|
||||
// LoginLogsClearHandler 清空登录日志API处理器
|
||||
func LoginLogsClearHandler(c *gin.Context) {
|
||||
// 鉴权拦截:仅超级管理员 (role=0) 允许清空日志
|
||||
if role, exists := c.Get("admin_role"); !exists || role.(int) != 0 {
|
||||
logBaseController.HandleValidationError(c, "权限不足,仅超级管理员可清空日志")
|
||||
return
|
||||
}
|
||||
|
||||
db, ok := logBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查数据库类型
|
||||
dbType := db.Dialector.Name()
|
||||
|
||||
if dbType == "sqlite" {
|
||||
// SQLite 不支持 TRUNCATE,直接使用 DELETE 和重置自增序列
|
||||
if err := db.Session(&gorm.Session{AllowGlobalUpdate: true}).Unscoped().Where("1 = 1").Delete(&models.LoginLog{}).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to clear login logs")
|
||||
logBaseController.HandleInternalError(c, "清空登录日志失败", err)
|
||||
return
|
||||
}
|
||||
// 重置 sqlite 的自增序列
|
||||
db.Exec("UPDATE sqlite_sequence SET seq = 0 WHERE name = 'login_logs'")
|
||||
// 释放空间
|
||||
db.Exec("VACUUM")
|
||||
} else {
|
||||
// 其他数据库(如 MySQL/PostgreSQL)尝试使用 TRUNCATE
|
||||
if err := db.Exec("TRUNCATE TABLE login_logs").Error; err != nil {
|
||||
// 如果 TRUNCATE 失败,回退到 DELETE
|
||||
if err := db.Session(&gorm.Session{AllowGlobalUpdate: true}).Unscoped().Where("1 = 1").Delete(&models.LoginLog{}).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to clear login logs")
|
||||
logBaseController.HandleInternalError(c, "清空登录日志失败", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
var operator, operatorUUID string
|
||||
operator = c.GetString("admin_username")
|
||||
operatorUUID = c.GetString("admin_uuid")
|
||||
if operator == "" {
|
||||
operator = "system"
|
||||
operatorUUID = "system"
|
||||
}
|
||||
|
||||
log := models.OperationLog{
|
||||
OperationType: "清空登录日志",
|
||||
Operator: operator,
|
||||
OperatorUUID: operatorUUID,
|
||||
Details: "管理员清空了所有登录日志",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
db.Create(&log)
|
||||
|
||||
logBaseController.HandleSuccess(c, "登录日志已清空", nil)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 操作日志 API处理器
|
||||
// ============================================================================
|
||||
|
||||
// LogsListHandler 日志列表API处理器
|
||||
func LogsListHandler(c *gin.Context) {
|
||||
// 获取分页参数
|
||||
page, limit := logBaseController.GetPaginationParams(c)
|
||||
|
||||
// 获取搜索参数
|
||||
operationType := strings.TrimSpace(c.Query("operation_type"))
|
||||
operator := strings.TrimSpace(c.Query("operator"))
|
||||
|
||||
// 获取数据库连接
|
||||
db, ok := logBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 筛选条件:时间范围
|
||||
query = logBaseController.ApplyTimeRangeQuery(c, query, "created_at")
|
||||
|
||||
// 泛型分页查询
|
||||
logs, total, err := services.Paginate[models.OperationLog](query, page, limit, "created_at DESC")
|
||||
if 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) {
|
||||
// 鉴权拦截:仅超级管理员 (role=0) 允许清空日志
|
||||
if role, exists := c.Get("admin_role"); !exists || role.(int) != 0 {
|
||||
logBaseController.HandleValidationError(c, "权限不足,仅超级管理员可清空日志")
|
||||
return
|
||||
}
|
||||
|
||||
db, ok := logBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查数据库类型
|
||||
dbType := db.Dialector.Name()
|
||||
|
||||
if dbType == "sqlite" {
|
||||
// SQLite 不支持 TRUNCATE,直接使用 DELETE 和重置自增序列
|
||||
if err := db.Session(&gorm.Session{AllowGlobalUpdate: true}).Unscoped().Where("1 = 1").Delete(&models.OperationLog{}).Error; err != nil {
|
||||
logrus.WithError(err).Error("清空操作日志失败")
|
||||
logBaseController.HandleInternalError(c, "清空操作日志失败", err)
|
||||
return
|
||||
}
|
||||
// 重置 sqlite 的自增序列
|
||||
db.Exec("UPDATE sqlite_sequence SET seq = 0 WHERE name = 'operation_logs'")
|
||||
// 释放空间
|
||||
db.Exec("VACUUM")
|
||||
} else {
|
||||
// 其他数据库(如 MySQL/PostgreSQL)尝试使用 TRUNCATE
|
||||
if err := db.Exec("TRUNCATE TABLE operation_logs").Error; err != nil {
|
||||
// 如果 TRUNCATE 失败,回退到 DELETE
|
||||
if err := db.Session(&gorm.Session{AllowGlobalUpdate: true}).Unscoped().Where("1 = 1").Delete(&models.OperationLog{}).Error; err != nil {
|
||||
logrus.WithError(err).Error("清空操作日志失败")
|
||||
logBaseController.HandleInternalError(c, "清空操作日志失败", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 记录操作日志 (因为刚刚清空了,这条将是第一条)
|
||||
var operator, operatorUUID string
|
||||
operator = c.GetString("admin_username")
|
||||
operatorUUID = c.GetString("admin_uuid")
|
||||
if operator == "" {
|
||||
operator = "system"
|
||||
operatorUUID = "system"
|
||||
}
|
||||
|
||||
log := models.OperationLog{
|
||||
OperationType: "清空日志",
|
||||
Operator: operator,
|
||||
OperatorUUID: operatorUUID,
|
||||
Details: "管理员清空了所有操作日志",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
db.Create(&log)
|
||||
|
||||
logBaseController.HandleSuccess(c, "日志已清空", nil)
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"NetworkAuth/controllers"
|
||||
"NetworkAuth/models"
|
||||
"NetworkAuth/services"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// 全局变量
|
||||
// ============================================================================
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API处理器
|
||||
// ============================================================================
|
||||
|
||||
// LoginLogsListHandler 登录日志列表API处理器
|
||||
func LoginLogsListHandler(c *gin.Context) {
|
||||
// 获取分页参数
|
||||
page, limit := loginLogBaseController.GetPaginationParams(c)
|
||||
|
||||
// 获取数据库连接
|
||||
db, ok := loginLogBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 兼容旧数据(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)
|
||||
}
|
||||
}
|
||||
|
||||
// 筛选条件:时间范围
|
||||
query = loginLogBaseController.ApplyTimeRangeQuery(c, query, "created_at")
|
||||
|
||||
// 泛型分页查询
|
||||
logs, total, err := services.Paginate[models.LoginLog](query, page, limit, "created_at DESC")
|
||||
if 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.Session(&gorm.Session{AllowGlobalUpdate: true}).Unscoped().Where("type = ? OR type = ? OR type IS NULL", "admin", "").Delete(&models.LoginLog{}).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to clear login logs")
|
||||
loginLogBaseController.HandleInternalError(c, "清空登录日志失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
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,
|
||||
Details: "管理员清空了所有登录日志",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
db.Create(&log)
|
||||
|
||||
loginLogBaseController.HandleSuccess(c, "登录日志已清空", nil)
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"NetworkAuth/controllers"
|
||||
"NetworkAuth/models"
|
||||
"NetworkAuth/services"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// 全局变量
|
||||
// ============================================================================
|
||||
|
||||
var logBaseController = controllers.NewBaseController()
|
||||
|
||||
// ============================================================================
|
||||
// API处理器
|
||||
// ============================================================================
|
||||
|
||||
// LogsListHandler 日志列表API处理器
|
||||
func LogsListHandler(c *gin.Context) {
|
||||
// 获取分页参数
|
||||
page, limit := logBaseController.GetPaginationParams(c)
|
||||
|
||||
// 获取搜索参数
|
||||
operationType := strings.TrimSpace(c.Query("operation_type"))
|
||||
operator := strings.TrimSpace(c.Query("operator"))
|
||||
|
||||
// 获取数据库连接
|
||||
db, ok := logBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 筛选条件:时间范围
|
||||
query = logBaseController.ApplyTimeRangeQuery(c, query, "created_at")
|
||||
|
||||
// 泛型分页查询
|
||||
logs, total, err := services.Paginate[models.OperationLog](query, page, limit, "created_at DESC")
|
||||
if 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
|
||||
}
|
||||
|
||||
// 记录操作日志 (因为刚刚清空了,这条将是第一条)
|
||||
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,
|
||||
Details: "管理员清空了所有操作日志",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
db.Create(&log)
|
||||
|
||||
logBaseController.HandleSuccess(c, "日志已清空", nil)
|
||||
}
|
||||
@@ -244,7 +244,7 @@ func SettingsPublicHandler(c *gin.Context) {
|
||||
|
||||
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 {
|
||||
if err := db.Where("name IN ? OR name LIKE ?", []string{"site_title", "site_description", "site_keywords", "site_logo", "contact_email", "maintenance_mode", "hide_login_entrance"}, "platform_%").Find(&list).Error; err != nil {
|
||||
authBaseController.HandleInternalError(c, "查询失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -18,6 +18,13 @@ import (
|
||||
|
||||
// InstallSubmitHandler 处理安装表单提交
|
||||
func InstallSubmitHandler(c *gin.Context) {
|
||||
// 二次安全校验:检查系统是否已经安装
|
||||
isInstalledStr := services.GetSettingsService().GetString("is_installed", "0")
|
||||
if isInstalledStr == "1" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"code": 403, "msg": "系统已安装,禁止重复初始化"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
// 数据库配置
|
||||
DbType string `json:"db_type" binding:"required,oneof=sqlite mysql"`
|
||||
@@ -109,14 +116,13 @@ func InstallSubmitHandler(c *gin.Context) {
|
||||
|
||||
// 更新或创建超级管理员账号
|
||||
var adminUser models.User
|
||||
if err := tx.Where("uuid = ?", "00000000-0000-0000-0000-000000000000").First(&adminUser).Error; err != nil {
|
||||
if err := tx.Where("role = ?", 0).First(&adminUser).Error; err != nil {
|
||||
// 如果不存在则创建
|
||||
adminUser = models.User{
|
||||
UUID: "00000000-0000-0000-0000-000000000000",
|
||||
Username: strings.TrimSpace(req.AdminUsername),
|
||||
Password: adminPasswordHash,
|
||||
PasswordSalt: adminSalt,
|
||||
Nickname: "管理员",
|
||||
Nickname: "超级管理员",
|
||||
Avatar: "",
|
||||
Role: 0,
|
||||
Status: 1,
|
||||
@@ -133,7 +139,7 @@ func InstallSubmitHandler(c *gin.Context) {
|
||||
adminUser.Username = strings.TrimSpace(req.AdminUsername)
|
||||
adminUser.Password = adminPasswordHash
|
||||
adminUser.PasswordSalt = adminSalt
|
||||
adminUser.Nickname = "管理员"
|
||||
adminUser.Nickname = "超级管理员"
|
||||
adminUser.Role = 0
|
||||
if err := tx.Save(&adminUser).Error; err != nil {
|
||||
tx.Rollback()
|
||||
|
||||
Reference in New Issue
Block a user