调整日志和鉴权接管方案

This commit is contained in:
2026-04-04 20:50:45 +08:00
parent 15f72873db
commit 76f0d815aa
20 changed files with 944 additions and 402 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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