From c31343ac76ec5afe337b50f5582a1fbc0735d097 Mon Sep 17 00:00:00 2001 From: CaIon Date: Sat, 18 Apr 2026 00:16:52 +0800 Subject: [PATCH] fix(log): hide admin identity in user-visible management logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Admin username/ID was embedded directly into the log Content for quota changes and forced 2FA disable, leaking the operator's identity to the target user via their own usage log page. Move operator info into Other.admin_info so formatUserLogs strips it for non-admin viewers, and render it in the expand panel only for admins as "操作管理员". Closes #4301 --- controller/twofa.go | 12 ++++++--- controller/user.go | 17 ++++++++----- model/log.go | 24 ++++++++++++++++++ web/src/hooks/usage-logs/useUsageLogsData.jsx | 25 +++++++++++++++++++ web/src/i18n/locales/en.json | 1 + web/src/i18n/locales/fr.json | 1 + web/src/i18n/locales/ja.json | 1 + web/src/i18n/locales/ru.json | 1 + web/src/i18n/locales/vi.json | 1 + web/src/i18n/locales/zh-CN.json | 1 + web/src/i18n/locales/zh-TW.json | 1 + 11 files changed, 75 insertions(+), 10 deletions(-) diff --git a/controller/twofa.go b/controller/twofa.go index 556c07e9e0..123c74e2cf 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 0e4786fbaa..d6becdd8f0 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 f9d159856d..c7242688fe 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 2c3c131a94..47a3f72fb3 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 cd7a1bbd60..5881298857 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 ef92f79d22..ff9840ef17 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 178b38a7b7..16edae7c11 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 6ec34b8718..80f657c575 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 42d33c189c..c2d079784f 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 155e877f0f..19b32e04bc 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 6ff1707576..c25eb6ffc6 100644 --- a/web/src/i18n/locales/zh-TW.json +++ b/web/src/i18n/locales/zh-TW.json @@ -1666,6 +1666,7 @@ "操作成功完成!": "操作成功完成!", "操作暂时被禁用": "操作暫時被禁用", "操作确认": "操作確認", + "操作管理员": "操作管理員", "操作类型": "", "操练场": "操練場", "操练场和聊天功能": "操練場和聊天功能",