fix(log): hide admin identity in user-visible management logs
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
This commit is contained in:
+8
-4
@@ -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,
|
||||
|
||||
+11
-6
@@ -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
|
||||
|
||||
@@ -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{}{
|
||||
|
||||
+25
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Vendored
+1
@@ -1695,6 +1695,7 @@
|
||||
"操作成功完成!": "Operation completed successfully!",
|
||||
"操作暂时被禁用": "Operation temporarily disabled",
|
||||
"操作确认": "Operation confirmation",
|
||||
"操作管理员": "Operator Admin",
|
||||
"操作类型": "Operation Type",
|
||||
"操练场": "Playground",
|
||||
"操练场和聊天功能": "Playground and chat functions",
|
||||
|
||||
Vendored
+1
@@ -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",
|
||||
|
||||
Vendored
+1
@@ -1666,6 +1666,7 @@
|
||||
"操作失败,请重试": "操作に失敗しました。再試行してください。",
|
||||
"操作成功完成!": "操作が正常に完了しました",
|
||||
"操作暂时被禁用": "この操作は一時的に無効にされています",
|
||||
"操作管理员": "操作管理者",
|
||||
"操作类型": "操作タイプ",
|
||||
"操练场": "Playground",
|
||||
"操练场和聊天功能": "プレイグラウンドとチャット機能",
|
||||
|
||||
Vendored
+1
@@ -1713,6 +1713,7 @@
|
||||
"操作失败,请重试": "Операция не удалась, попробуйте еще раз",
|
||||
"操作成功完成!": "Операция успешно завершена!",
|
||||
"操作暂时被禁用": "Операция временно отключена",
|
||||
"操作管理员": "Администратор операции",
|
||||
"操作类型": "Тип операции",
|
||||
"操练场": "Тренировочная площадка",
|
||||
"操练场和聊天功能": "Тренировочная площадка и чат-функции",
|
||||
|
||||
Vendored
+1
@@ -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",
|
||||
|
||||
Vendored
+1
@@ -1655,6 +1655,7 @@
|
||||
"操作成功完成!": "操作成功完成!",
|
||||
"操作暂时被禁用": "操作暂时被禁用",
|
||||
"操作确认": "操作确认",
|
||||
"操作管理员": "操作管理员",
|
||||
"操作类型": "操作类型",
|
||||
"操练场": "操练场",
|
||||
"操练场和聊天功能": "操练场和聊天功能",
|
||||
|
||||
Vendored
+1
@@ -1666,6 +1666,7 @@
|
||||
"操作成功完成!": "操作成功完成!",
|
||||
"操作暂时被禁用": "操作暫時被禁用",
|
||||
"操作确认": "操作確認",
|
||||
"操作管理员": "操作管理員",
|
||||
"操作类型": "",
|
||||
"操练场": "操練場",
|
||||
"操练场和聊天功能": "操練場和聊天功能",
|
||||
|
||||
Reference in New Issue
Block a user