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 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 @@ "操作成功完成!": "操作成功完成!", "操作暂时被禁用": "操作暫時被禁用", "操作确认": "操作確認", + "操作管理员": "操作管理員", "操作类型": "", "操练场": "操練場", "操练场和聊天功能": "操練場和聊天功能",