feat(audit): add localized security audit logs (#5462)

This commit is contained in:
Calcium-Ion
2026-06-12 23:40:40 +08:00
committed by GitHub
parent 27b2b2c4b9
commit d0c4305a16
25 changed files with 1249 additions and 33 deletions
+69 -13
View File
@@ -90,6 +90,43 @@ func Login(c *gin.Context) {
setupLogin(&user, c)
}
// loginMethodFromContext 根据请求路径推导登录方式,用于登录审计日志。
func loginMethodFromContext(c *gin.Context) string {
switch c.FullPath() {
case "/api/user/login":
return "password"
case "/api/user/login/2fa":
return "2fa"
case "/api/user/passkey/login/finish":
return "passkey"
case "/api/oauth/wechat":
return "wechat"
case "/api/oauth/telegram/login":
return "telegram"
case "/api/oauth/:provider":
if provider := c.Param("provider"); provider != "" {
return "oauth:" + provider
}
return "oauth"
default:
return "unknown"
}
}
// recordLoginAudit 记录登录成功审计日志(对所有用户启用,仅记录成功,不记录失败)。
func recordLoginAudit(user *model.User, c *gin.Context) {
method := loginMethodFromContext(c)
ip := c.ClientIP()
extra := map[string]interface{}{
"login_method": method,
"user_agent": c.Request.UserAgent(),
}
content := fmt.Sprintf("Logged in successfully via %s", method)
model.RecordLoginLog(user.Id, user.Username, content, ip, "login", map[string]interface{}{
"method": method,
}, extra)
}
// setup session & cookies and then return user info
func setupLogin(user *model.User, c *gin.Context) {
model.UpdateUserLastLoginAt(user.Id)
@@ -104,6 +141,7 @@ func setupLogin(user *model.User, c *gin.Context) {
common.ApiErrorI18n(c, i18n.MsgUserSessionSaveFailed)
return
}
recordLoginAudit(user, c)
c.JSON(http.StatusOK, gin.H{
"message": "",
"success": true,
@@ -599,6 +637,10 @@ func UpdateUser(c *gin.Context) {
common.ApiError(c, err)
return
}
recordManageAuditFor(c, updatedUser.Id, "user.update", map[string]interface{}{
"username": originUser.Username,
"id": updatedUser.Id,
})
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
@@ -636,7 +678,10 @@ func AdminClearUserBinding(c *gin.Context) {
return
}
model.RecordLog(user.Id, model.LogTypeManage, fmt.Sprintf("admin cleared %s binding for user %s", bindingType, user.Username))
recordManageAuditFor(c, user.Id, "user.binding_clear", map[string]interface{}{
"bindingType": bindingType,
"username": user.Username,
})
c.JSON(http.StatusOK, gin.H{
"success": true,
@@ -797,6 +842,10 @@ func DeleteUser(c *gin.Context) {
common.ApiError(c, err)
return
}
recordManageAuditFor(c, originUser.Id, "user.delete", map[string]interface{}{
"username": originUser.Username,
"id": originUser.Id,
})
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
@@ -857,6 +906,10 @@ func CreateUser(c *gin.Context) {
return
}
recordManageAuditFor(c, cleanUser.Id, "user.create", map[string]interface{}{
"username": cleanUser.Username,
"role": cleanUser.Role,
})
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
@@ -941,12 +994,6 @@ func ManageUser(c *gin.Context) {
}
user.Role = common.RoleCommonUser
case "add_quota":
adminName := c.GetString("username")
adminId := c.GetInt("id")
adminInfo := map[string]interface{}{
"admin_id": adminId,
"admin_username": adminName,
}
switch req.Mode {
case "add":
if req.Value <= 0 {
@@ -957,8 +1004,9 @@ func ManageUser(c *gin.Context) {
common.ApiError(c, err)
return
}
model.RecordLogWithAdminInfo(user.Id, model.LogTypeManage,
fmt.Sprintf("管理员增加用户额度 %s", logger.LogQuota(req.Value)), adminInfo)
recordManageAuditFor(c, user.Id, "user.quota_add", map[string]interface{}{
"quota": logger.LogQuota(req.Value),
})
case "subtract":
if req.Value <= 0 {
common.ApiErrorI18n(c, i18n.MsgUserQuotaChangeZero)
@@ -968,16 +1016,19 @@ func ManageUser(c *gin.Context) {
common.ApiError(c, err)
return
}
model.RecordLogWithAdminInfo(user.Id, model.LogTypeManage,
fmt.Sprintf("管理员减少用户额度 %s", logger.LogQuota(req.Value)), adminInfo)
recordManageAuditFor(c, user.Id, "user.quota_subtract", map[string]interface{}{
"quota": logger.LogQuota(req.Value),
})
case "override":
oldQuota := user.Quota
if err := model.DB.Model(&model.User{}).Where("id = ?", user.Id).Update("quota", req.Value).Error; err != nil {
common.ApiError(c, err)
return
}
model.RecordLogWithAdminInfo(user.Id, model.LogTypeManage,
fmt.Sprintf("管理员覆盖用户额度从 %s 为 %s", logger.LogQuota(oldQuota), logger.LogQuota(req.Value)), adminInfo)
recordManageAuditFor(c, user.Id, "user.quota_override", map[string]interface{}{
"from": logger.LogQuota(oldQuota),
"to": logger.LogQuota(req.Value),
})
default:
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
return
@@ -1005,6 +1056,11 @@ func ManageUser(c *gin.Context) {
common.SysLog(fmt.Sprintf("failed to invalidate tokens cache for user %d: %s", user.Id, err.Error()))
}
}
recordManageAuditFor(c, user.Id, "user.manage", map[string]interface{}{
"action": req.Action,
"username": user.Username,
"id": user.Id,
})
clearUser := model.User{
Role: user.Role,
Status: user.Status,