diff --git a/controller/twofa.go b/controller/twofa.go index 556c07e9..123c74e2 100644 --- a/controller/twofa.go +++ b/controller/twofa.go @@ -2,7 +2,6 @@ package controller import ( "errors" - "fmt" "net/http" "strconv" @@ -542,10 +541,15 @@ func AdminDisable2FA(c *gin.Context) { return } - // 记录操作日志 + // 记录操作日志:管理员身份通过 admin_info 传递,避免在非管理员可见的日志内容中泄露。 adminId := c.GetInt("id") - model.RecordLog(userId, model.LogTypeManage, - fmt.Sprintf("管理员(ID:%d)强制禁用了用户的两步验证", adminId)) + adminName := c.GetString("username") + adminInfo := map[string]interface{}{ + "admin_id": adminId, + "admin_username": adminName, + } + model.RecordLogWithAdminInfo(userId, model.LogTypeManage, + "管理员强制禁用了用户的两步验证", adminInfo) c.JSON(http.StatusOK, gin.H{ "success": true, diff --git a/controller/user.go b/controller/user.go index 0e4786fb..d6becdd8 100644 --- a/controller/user.go +++ b/controller/user.go @@ -918,6 +918,11 @@ 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 { @@ -928,8 +933,8 @@ func ManageUser(c *gin.Context) { common.ApiError(c, err) return } - model.RecordLog(user.Id, model.LogTypeManage, - fmt.Sprintf("管理员(%s)增加用户额度 %s", adminName, logger.LogQuota(req.Value))) + model.RecordLogWithAdminInfo(user.Id, model.LogTypeManage, + fmt.Sprintf("管理员增加用户额度 %s", logger.LogQuota(req.Value)), adminInfo) case "subtract": if req.Value <= 0 { common.ApiErrorI18n(c, i18n.MsgUserQuotaChangeZero) @@ -939,16 +944,16 @@ func ManageUser(c *gin.Context) { common.ApiError(c, err) return } - model.RecordLog(user.Id, model.LogTypeManage, - fmt.Sprintf("管理员(%s)减少用户额度 %s", adminName, logger.LogQuota(req.Value))) + model.RecordLogWithAdminInfo(user.Id, model.LogTypeManage, + fmt.Sprintf("管理员减少用户额度 %s", logger.LogQuota(req.Value)), adminInfo) 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.RecordLog(user.Id, model.LogTypeManage, - fmt.Sprintf("管理员(%s)覆盖用户额度从 %s 为 %s", adminName, logger.LogQuota(oldQuota), logger.LogQuota(req.Value))) + model.RecordLogWithAdminInfo(user.Id, model.LogTypeManage, + fmt.Sprintf("管理员覆盖用户额度从 %s 为 %s", logger.LogQuota(oldQuota), logger.LogQuota(req.Value)), adminInfo) default: common.ApiErrorI18n(c, i18n.MsgInvalidParams) return diff --git a/model/log.go b/model/log.go index f9d15985..c7242688 100644 --- a/model/log.go +++ b/model/log.go @@ -90,6 +90,30 @@ func RecordLog(userId int, logType int, content string) { } } +// RecordLogWithAdminInfo 记录操作日志,并将管理员相关信息存入 Other.admin_info, +func RecordLogWithAdminInfo(userId int, logType int, content string, adminInfo map[string]interface{}) { + if logType == LogTypeConsume && !common.LogConsumeEnabled { + return + } + username, _ := GetUsernameById(userId, false) + log := &Log{ + UserId: userId, + Username: username, + CreatedAt: common.GetTimestamp(), + Type: logType, + Content: content, + } + if len(adminInfo) > 0 { + other := map[string]interface{}{ + "admin_info": adminInfo, + } + log.Other = common.MapToJsonStr(other) + } + if err := LOG_DB.Create(log).Error; err != nil { + common.SysLog("failed to record log: " + err.Error()) + } +} + func RecordTopupLog(userId int, content string, callerIp string, paymentMethod string, callbackPaymentMethod string) { username, _ := GetUsernameById(userId, false) adminInfo := map[string]interface{}{ diff --git a/web/src/hooks/usage-logs/useUsageLogsData.jsx b/web/src/hooks/usage-logs/useUsageLogsData.jsx index 2c3c131a..47a3f72f 100644 --- a/web/src/hooks/usage-logs/useUsageLogsData.jsx +++ b/web/src/hooks/usage-logs/useUsageLogsData.jsx @@ -746,6 +746,31 @@ export const useLogsData = () => { }); } } + if (isAdminUser && logs[i].type === 3 && other?.admin_info) { + const adminInfo = other.admin_info; + const hasUsername = + adminInfo.admin_username !== undefined && + adminInfo.admin_username !== null && + adminInfo.admin_username !== ''; + const hasId = + adminInfo.admin_id !== undefined && + adminInfo.admin_id !== null && + adminInfo.admin_id !== ''; + if (hasUsername || hasId) { + let operatorValue = ''; + if (hasUsername && hasId) { + operatorValue = `${adminInfo.admin_username} (ID: ${adminInfo.admin_id})`; + } else if (hasUsername) { + operatorValue = String(adminInfo.admin_username); + } else { + operatorValue = `ID: ${adminInfo.admin_id}`; + } + expandDataLocal.push({ + key: t('操作管理员'), + value: operatorValue, + }); + } + } expandDatesLocal[logs[i].key] = expandDataLocal; } diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index cd7a1bbd..58812988 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1695,6 +1695,7 @@ "操作成功完成!": "Operation completed successfully!", "操作暂时被禁用": "Operation temporarily disabled", "操作确认": "Operation confirmation", + "操作管理员": "Operator Admin", "操作类型": "Operation Type", "操练场": "Playground", "操练场和聊天功能": "Playground and chat functions", diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index ef92f79d..ff9840ef 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -1695,6 +1695,7 @@ "操作失败,请重试": "L'opération a échoué, veuillez réessayer", "操作成功完成!": "Opération terminée avec succès !", "操作暂时被禁用": "Opération temporairement désactivée", + "操作管理员": "Administrateur opérateur", "操作类型": "Type d'opération", "操练场": "Terrain de jeu", "操练场和聊天功能": "Terrain de jeu et fonctions de discussion", diff --git a/web/src/i18n/locales/ja.json b/web/src/i18n/locales/ja.json index 178b38a7..16edae7c 100644 --- a/web/src/i18n/locales/ja.json +++ b/web/src/i18n/locales/ja.json @@ -1666,6 +1666,7 @@ "操作失败,请重试": "操作に失敗しました。再試行してください。", "操作成功完成!": "操作が正常に完了しました", "操作暂时被禁用": "この操作は一時的に無効にされています", + "操作管理员": "操作管理者", "操作类型": "操作タイプ", "操练场": "Playground", "操练场和聊天功能": "プレイグラウンドとチャット機能", diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index 6ec34b87..80f657c5 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -1713,6 +1713,7 @@ "操作失败,请重试": "Операция не удалась, попробуйте еще раз", "操作成功完成!": "Операция успешно завершена!", "操作暂时被禁用": "Операция временно отключена", + "操作管理员": "Администратор операции", "操作类型": "Тип операции", "操练场": "Тренировочная площадка", "操练场和聊天功能": "Тренировочная площадка и чат-функции", diff --git a/web/src/i18n/locales/vi.json b/web/src/i18n/locales/vi.json index 42d33c18..c2d07978 100644 --- a/web/src/i18n/locales/vi.json +++ b/web/src/i18n/locales/vi.json @@ -1667,6 +1667,7 @@ "操作失败,请重试": "Thao tác thất bại, vui lòng thử lại", "操作成功完成!": "Thao tác hoàn tất thành công!", "操作暂时被禁用": "Thao tác tạm thời bị vô hiệu hóa", + "操作管理员": "Quản trị viên thao tác", "操作类型": "Loại thao tác", "操练场": "Sân chơi", "操练场和聊天功能": "Chức năng sân chơi và trò chuyện", diff --git a/web/src/i18n/locales/zh-CN.json b/web/src/i18n/locales/zh-CN.json index 155e877f..19b32e04 100644 --- a/web/src/i18n/locales/zh-CN.json +++ b/web/src/i18n/locales/zh-CN.json @@ -1655,6 +1655,7 @@ "操作成功完成!": "操作成功完成!", "操作暂时被禁用": "操作暂时被禁用", "操作确认": "操作确认", + "操作管理员": "操作管理员", "操作类型": "操作类型", "操练场": "操练场", "操练场和聊天功能": "操练场和聊天功能", diff --git a/web/src/i18n/locales/zh-TW.json b/web/src/i18n/locales/zh-TW.json index 6ff17075..c25eb6ff 100644 --- a/web/src/i18n/locales/zh-TW.json +++ b/web/src/i18n/locales/zh-TW.json @@ -1666,6 +1666,7 @@ "操作成功完成!": "操作成功完成!", "操作暂时被禁用": "操作暫時被禁用", "操作确认": "操作確認", + "操作管理员": "操作管理員", "操作类型": "", "操练场": "操練場", "操练场和聊天功能": "操練場和聊天功能",