feat(audit): add localized security audit logs (#5462)
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// auditContentTemplates 将稳定的操作标识 action 映射为英文兜底模板,渲染后写入
|
||||
// Log.Content(供导出 / 经典前端等非本地化消费者使用)。占位符为 ${name},由该
|
||||
// action 的 params 填充。本地化展示文案在前端 i18n 模板中维护,本表是语言中立的
|
||||
// 英文基线——调用方因此无需在每个埋点处手写句子(避免与 params 重复书写同一份值)。
|
||||
var auditContentTemplates = map[string]string{
|
||||
"user.create": "Created user ${username} (role ${role})",
|
||||
"user.update": "Updated user ${username} (ID: ${id})",
|
||||
"user.delete": "Deleted user ${username} (ID: ${id})",
|
||||
"user.manage": "Performed ${action} on user ${username} (ID: ${id})",
|
||||
"user.quota_add": "Increased user quota by ${quota}",
|
||||
"user.quota_subtract": "Decreased user quota by ${quota}",
|
||||
"user.quota_override": "Overrode user quota from ${from} to ${to}",
|
||||
"user.binding_clear": "Cleared ${bindingType} binding for user ${username}",
|
||||
"user.2fa_disable": "Force-disabled two-factor authentication for the user",
|
||||
"user.passkey_register": "Registered a passkey",
|
||||
"user.passkey_delete": "Deleted a passkey",
|
||||
"user.reset_passkey": "Reset the user passkey",
|
||||
"option.update": "Updated system setting ${key}",
|
||||
|
||||
"channel.create": "Created channel ${name} (type ${type}, count ${count})",
|
||||
"channel.update": "Updated channel ${name} (ID: ${id})",
|
||||
"channel.delete": "Deleted channel ${name} (ID: ${id})",
|
||||
"channel.delete_batch": "Batch deleted ${count} channels",
|
||||
"channel.delete_disabled": "Deleted all disabled channels (${count})",
|
||||
"channel.key_view": "Viewed channel key ${name} (ID: ${id})",
|
||||
"channel.tag_disable": "Disabled channels with tag ${tag}",
|
||||
"channel.tag_enable": "Enabled channels with tag ${tag}",
|
||||
"channel.tag_edit": "Edited channels with tag ${tag}",
|
||||
"channel.tag_batch_set": "Batch set tag for ${count} channels",
|
||||
"channel.copy": "Copied channel (source ID: ${sourceId}) to ${name} (new ID: ${id})",
|
||||
"channel.multi_key_manage": "Multi-key management ${action} on channel (ID: ${id})",
|
||||
"channel.upstream_apply": "Applied upstream model changes to channel (ID: ${id})",
|
||||
"channel.upstream_apply_all": "Applied upstream model changes to ${count} channels",
|
||||
|
||||
"redemption.create": "Created ${count} redemption codes named ${name} (${quota} each)",
|
||||
}
|
||||
|
||||
// auditContentEN 按 action 模板渲染英文兜底文本;未登记的 action 退回 action 本身。
|
||||
func auditContentEN(action string, params map[string]interface{}) string {
|
||||
tmpl, ok := auditContentTemplates[action]
|
||||
if !ok {
|
||||
return action
|
||||
}
|
||||
return os.Expand(tmpl, func(key string) string {
|
||||
if v, ok := params[key]; ok {
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
return ""
|
||||
})
|
||||
}
|
||||
|
||||
// auditOperatorInfo 从上下文构建操作者身份信息(管理员 id/用户名/角色)。
|
||||
func auditOperatorInfo(c *gin.Context) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"admin_id": c.GetInt("id"),
|
||||
"admin_username": c.GetString("username"),
|
||||
"admin_role": c.GetInt("role"),
|
||||
}
|
||||
}
|
||||
|
||||
// markAuditLogged 标记当前请求已在 handler 内手动记录审计日志,
|
||||
// 使鉴权链路中的审计兜底(finishAdminAudit)跳过兜底记录,避免重复。
|
||||
func markAuditLogged(c *gin.Context) {
|
||||
common.SetContextKey(c, constant.ContextKeyAuditLogged, true)
|
||||
}
|
||||
|
||||
// recordManageAudit 记录一条由操作者本人归属的管理/高危审计日志(资源类操作:
|
||||
// 渠道 / 系统设置 / 兑换码等)。content 由 action+params 自动渲染。
|
||||
func recordManageAudit(c *gin.Context, action string, params map[string]interface{}) {
|
||||
recordManageAuditFor(c, c.GetInt("id"), action, params)
|
||||
}
|
||||
|
||||
// recordManageAuditFor 记录一条归属于 logUserId 的管理审计日志(面向用户的操作:
|
||||
// 对目标用户的额度调整 / 解绑 / 2FA 等,使该用户也能在自己的日志中看到)。
|
||||
func recordManageAuditFor(c *gin.Context, logUserId int, action string, params map[string]interface{}) {
|
||||
model.RecordOperationAuditLog(logUserId, auditContentEN(action, params), c.ClientIP(), action, params, auditOperatorInfo(c), nil)
|
||||
markAuditLogged(c)
|
||||
}
|
||||
|
||||
// recordUserSecurityAudit 记录普通用户自己的安全敏感操作(如 passkey 绑定/解绑)。
|
||||
// 这类日志没有管理员操作者,不写 admin_info;同时不依赖 AdminAuth/RootAuth 的兜底。
|
||||
func recordUserSecurityAudit(c *gin.Context, userId int, action string, params map[string]interface{}) {
|
||||
model.RecordOperationAuditLog(userId, auditContentEN(action, params), c.ClientIP(), action, params, nil, nil)
|
||||
}
|
||||
+87
-3
@@ -404,7 +404,6 @@ func GetChannel(c *gin.Context) {
|
||||
// GetChannelKey 获取渠道密钥(需要通过安全验证中间件)
|
||||
// 此函数依赖 SecureVerificationRequired 中间件,确保用户已通过安全验证
|
||||
func GetChannelKey(c *gin.Context) {
|
||||
userId := c.GetInt("id")
|
||||
channelId, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
common.ApiError(c, fmt.Errorf("渠道ID格式错误: %v", err))
|
||||
@@ -423,8 +422,11 @@ func GetChannelKey(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
model.RecordLog(userId, model.LogTypeSystem, fmt.Sprintf("查看渠道密钥信息 (渠道ID: %d)", channelId))
|
||||
// 记录操作审计日志(高危:查看渠道密钥)
|
||||
recordManageAudit(c, "channel.key_view", map[string]interface{}{
|
||||
"id": channelId,
|
||||
"name": channel.Name,
|
||||
})
|
||||
|
||||
// 返回渠道密钥
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -677,6 +679,11 @@ func AddChannel(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
service.ResetProxyClientCache()
|
||||
recordManageAudit(c, "channel.create", map[string]interface{}{
|
||||
"name": addChannelRequest.Channel.Name,
|
||||
"type": addChannelRequest.Channel.Type,
|
||||
"count": len(channels),
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
@@ -686,6 +693,10 @@ func AddChannel(c *gin.Context) {
|
||||
|
||||
func DeleteChannel(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
channelName := ""
|
||||
if existing, err := model.GetChannelById(id, false); err == nil && existing != nil {
|
||||
channelName = existing.Name
|
||||
}
|
||||
channel := model.Channel{Id: id}
|
||||
err := channel.Delete()
|
||||
if err != nil {
|
||||
@@ -693,6 +704,10 @@ func DeleteChannel(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
model.InitChannelCache()
|
||||
recordManageAudit(c, "channel.delete", map[string]interface{}{
|
||||
"id": id,
|
||||
"name": channelName,
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
@@ -707,6 +722,9 @@ func DeleteDisabledChannel(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
model.InitChannelCache()
|
||||
recordManageAudit(c, "channel.delete_disabled", map[string]interface{}{
|
||||
"count": rows,
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
@@ -743,6 +761,9 @@ func DisableTagChannels(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
model.InitChannelCache()
|
||||
recordManageAudit(c, "channel.tag_disable", map[string]interface{}{
|
||||
"tag": channelTag.Tag,
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
@@ -766,6 +787,9 @@ func EnableTagChannels(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
model.InitChannelCache()
|
||||
recordManageAudit(c, "channel.tag_enable", map[string]interface{}{
|
||||
"tag": channelTag.Tag,
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
@@ -818,6 +842,9 @@ func EditTagChannels(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
model.InitChannelCache()
|
||||
recordManageAudit(c, "channel.tag_edit", map[string]interface{}{
|
||||
"tag": channelTag.Tag,
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
@@ -846,6 +873,9 @@ func DeleteChannelBatch(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
model.InitChannelCache()
|
||||
recordManageAudit(c, "channel.delete_batch", map[string]interface{}{
|
||||
"count": len(channelBatch.Ids),
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
@@ -981,6 +1011,31 @@ func UpdateChannel(c *gin.Context) {
|
||||
}
|
||||
model.InitChannelCache()
|
||||
service.ResetProxyClientCache()
|
||||
// 记录变更的字段名(语言无关的字段标识),密钥仅记录"已更换"绝不记录内容。
|
||||
changedFields := make([]string, 0)
|
||||
if channel.Status != originChannel.Status {
|
||||
changedFields = append(changedFields, "status")
|
||||
}
|
||||
if channel.Models != originChannel.Models {
|
||||
changedFields = append(changedFields, "models")
|
||||
}
|
||||
if channel.Group != originChannel.Group {
|
||||
changedFields = append(changedFields, "group")
|
||||
}
|
||||
if channel.Type != originChannel.Type {
|
||||
changedFields = append(changedFields, "type")
|
||||
}
|
||||
if !equalStringPtr(channel.BaseURL, originChannel.BaseURL) {
|
||||
changedFields = append(changedFields, "base_url")
|
||||
}
|
||||
if channel.Key != "" && channel.Key != originChannel.Key {
|
||||
changedFields = append(changedFields, "key")
|
||||
}
|
||||
recordManageAudit(c, "channel.update", map[string]interface{}{
|
||||
"id": channel.Id,
|
||||
"name": channel.Name,
|
||||
"changed_fields": changedFields,
|
||||
})
|
||||
channel.Key = ""
|
||||
clearChannelInfo(&channel.Channel)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -991,6 +1046,17 @@ func UpdateChannel(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// equalStringPtr 比较两个 *string 是否相等(均为 nil 视为相等)。
|
||||
func equalStringPtr(a, b *string) bool {
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
if a == nil || b == nil {
|
||||
return false
|
||||
}
|
||||
return *a == *b
|
||||
}
|
||||
|
||||
func FetchModels(c *gin.Context) {
|
||||
var req struct {
|
||||
BaseURL string `json:"base_url"`
|
||||
@@ -1127,6 +1193,9 @@ func BatchSetChannelTag(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
model.InitChannelCache()
|
||||
recordManageAudit(c, "channel.tag_batch_set", map[string]interface{}{
|
||||
"count": len(channelBatch.Ids),
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
@@ -1224,6 +1293,11 @@ func CopyChannel(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
model.InitChannelCache()
|
||||
recordManageAudit(c, "channel.copy", map[string]interface{}{
|
||||
"sourceId": id,
|
||||
"id": clone.Id,
|
||||
"name": clone.Name,
|
||||
})
|
||||
// success
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "", "data": gin.H{"id": clone.Id}})
|
||||
}
|
||||
@@ -1285,6 +1359,16 @@ func ManageMultiKeys(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// get_key_status 为只读查询,不记录审计;其余为修改操作,记录审计并跳过中间件兜底。
|
||||
if request.Action == "get_key_status" {
|
||||
markAuditLogged(c)
|
||||
} else {
|
||||
recordManageAudit(c, "channel.multi_key_manage", map[string]interface{}{
|
||||
"action": request.Action,
|
||||
"id": channel.Id,
|
||||
})
|
||||
}
|
||||
|
||||
lock := model.GetChannelPollingLock(channel.Id)
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
@@ -717,6 +717,9 @@ func ApplyChannelUpstreamModelUpdates(c *gin.Context) {
|
||||
refreshChannelRuntimeCache()
|
||||
}
|
||||
|
||||
recordManageAudit(c, "channel.upstream_apply", map[string]interface{}{
|
||||
"id": channel.Id,
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
@@ -912,6 +915,9 @@ func ApplyAllChannelUpstreamModelUpdates(c *gin.Context) {
|
||||
refreshChannelRuntimeCache()
|
||||
}
|
||||
|
||||
recordManageAudit(c, "channel.upstream_apply_all", map[string]interface{}{
|
||||
"count": len(results),
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
|
||||
@@ -337,6 +337,10 @@ func UpdateOption(c *gin.Context) {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
// 出于安全考虑只记录被修改的配置项名称,不记录配置值(可能含密钥等敏感信息)。
|
||||
recordManageAudit(c, "option.update", map[string]interface{}{
|
||||
"key": option.Key,
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
|
||||
@@ -143,6 +143,7 @@ func PasskeyRegisterFinish(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
recordUserSecurityAudit(c, user.Id, "user.passkey_register", nil)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "Passkey 注册成功",
|
||||
@@ -168,6 +169,7 @@ func PasskeyDelete(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
recordUserSecurityAudit(c, user.Id, "user.passkey_delete", nil)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "Passkey 已解绑",
|
||||
@@ -335,7 +337,6 @@ func PasskeyLoginFinish(c *gin.Context) {
|
||||
}
|
||||
|
||||
setupLogin(modelUser, c)
|
||||
return
|
||||
}
|
||||
|
||||
func AdminResetPasskey(c *gin.Context) {
|
||||
@@ -373,6 +374,10 @@ func AdminResetPasskey(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
recordManageAuditFor(c, user.Id, "user.reset_passkey", map[string]interface{}{
|
||||
"username": user.Username,
|
||||
"id": user.Id,
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "Passkey 已重置",
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/i18n"
|
||||
"github.com/QuantumNous/new-api/logger"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/setting/operation_setting"
|
||||
|
||||
@@ -110,6 +111,11 @@ func AddRedemption(c *gin.Context) {
|
||||
}
|
||||
keys = append(keys, key)
|
||||
}
|
||||
recordManageAudit(c, "redemption.create", map[string]interface{}{
|
||||
"name": redemption.Name,
|
||||
"count": redemption.Count,
|
||||
"quota": logger.LogQuota(redemption.Quota),
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
|
||||
+1
-9
@@ -541,15 +541,7 @@ func AdminDisable2FA(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 记录操作日志:管理员身份通过 admin_info 传递,避免在非管理员可见的日志内容中泄露。
|
||||
adminId := c.GetInt("id")
|
||||
adminName := c.GetString("username")
|
||||
adminInfo := map[string]interface{}{
|
||||
"admin_id": adminId,
|
||||
"admin_username": adminName,
|
||||
}
|
||||
model.RecordLogWithAdminInfo(userId, model.LogTypeManage,
|
||||
"管理员强制禁用了用户的两步验证", adminInfo)
|
||||
recordManageAuditFor(c, userId, "user.2fa_disable", nil)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
|
||||
+69
-13
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user