Compare commits

...

2 Commits

Author SHA1 Message Date
CaIon 6ff8c7ab03 fix(topup-log): keep row expandable and warn admins on legacy logs
Top-up logs written by pre-upgrade instances have no admin_info, which
made the expanded row empty and the row un-expandable. For admins, always
emit an entry: either the audit fields from admin_info when present, or a
warning prompting the operator to upgrade the instance so audit fields
(server IP, callback IP, payment method, system version) are recorded.
2026-04-18 00:36:05 +08:00
CaIon c31343ac76 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
2026-04-18 00:16:52 +08:00
11 changed files with 130 additions and 38 deletions
+8 -4
View File
@@ -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
View File
@@ -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
+24
View File
@@ -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{}{
+66 -28
View File
@@ -713,36 +713,74 @@ export const useLogsData = () => {
value: localCountMode,
});
}
if (isAdminUser && logs[i].type === 1 && other?.admin_info) {
if (isAdminUser && logs[i].type === 1) {
const adminInfo = other?.admin_info;
if (adminInfo) {
if (adminInfo.payment_method) {
expandDataLocal.push({
key: t('订单支付方式'),
value: adminInfo.payment_method,
});
}
if (adminInfo.callback_payment_method) {
expandDataLocal.push({
key: t('回调支付方式'),
value: adminInfo.callback_payment_method,
});
}
if (adminInfo.caller_ip) {
expandDataLocal.push({
key: t('回调调用者IP'),
value: adminInfo.caller_ip,
});
}
if (adminInfo.server_ip) {
expandDataLocal.push({
key: t('服务器IP'),
value: adminInfo.server_ip,
});
}
if (adminInfo.version) {
expandDataLocal.push({
key: t('系统版本'),
value: adminInfo.version,
});
}
} else {
expandDataLocal.push({
key: t('审计信息'),
value: (
<span style={{ color: 'var(--semi-color-warning)' }}>
{t(
'该记录由旧版本实例写入,缺少审计信息,建议将实例升级至最新版本以便记录服务器IP、回调IP、支付方式与系统版本等审计字段。',
)}
</span>
),
});
}
}
if (isAdminUser && logs[i].type === 3 && other?.admin_info) {
const adminInfo = other.admin_info;
if (adminInfo.payment_method) {
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: adminInfo.payment_method,
});
}
if (adminInfo.callback_payment_method) {
expandDataLocal.push({
key: t('回调支付方式'),
value: adminInfo.callback_payment_method,
});
}
if (adminInfo.caller_ip) {
expandDataLocal.push({
key: t('回调调用者IP'),
value: adminInfo.caller_ip,
});
}
if (adminInfo.server_ip) {
expandDataLocal.push({
key: t('服务器IP'),
value: adminInfo.server_ip,
});
}
if (adminInfo.version) {
expandDataLocal.push({
key: t('系统版本'),
value: adminInfo.version,
key: t('操作管理员'),
value: operatorValue,
});
}
}
+3
View File
@@ -1230,6 +1230,7 @@
"实际模型": "Actual model",
"实际结算金额:{{symbol}}{{total}}(已包含分组价格调整)": "Actual charge: {{symbol}}{{total}} (group pricing adjustment included)",
"实际请求体": "Actual request body",
"审计信息": "Audit Info",
"容器": "Container",
"容器ID": "Container ID",
"容器创建失败: ": "Container creation failed: ",
@@ -1695,6 +1696,7 @@
"操作成功完成!": "Operation completed successfully!",
"操作暂时被禁用": "Operation temporarily disabled",
"操作确认": "Operation confirmation",
"操作管理员": "Operator Admin",
"操作类型": "Operation Type",
"操练场": "Playground",
"操练场和聊天功能": "Playground and chat functions",
@@ -2980,6 +2982,7 @@
"该规则未设置参数覆盖模板": "This rule has no parameter override template set",
"该规则的缓存保留时长;0 表示使用默认 TTL:": "Cache retention duration for this rule; 0 means using default TTL: ",
"该记录不包含可用的 token 统计口径。": "This record does not contain available token statistics.",
"该记录由旧版本实例写入,缺少审计信息,建议将实例升级至最新版本以便记录服务器IP、回调IP、支付方式与系统版本等审计字段。": "This record was written by an older instance and lacks audit info. Please upgrade the instance to the latest version so that server IP, callback IP, payment method, and system version audit fields are recorded.",
"详情": "Details",
"详见「特殊倍率」和「可用分组」标签页。": "See \"Special Ratios\" and \"Usable Groups\" tabs for details.",
"语言偏好": "Language Preference",
+3
View File
@@ -1226,6 +1226,7 @@
"实际模型": "Modèle réel",
"实际结算金额:{{symbol}}{{total}}(已包含分组价格调整)": "Montant facturé réel : {{symbol}}{{total}} (ajustement tarifaire de groupe inclus)",
"实际请求体": "Corps de requête réel",
"审计信息": "Informations d'audit",
"容器": "Container",
"容器ID": "Container ID",
"容器创建失败: ": "Container creation failed: ",
@@ -1695,6 +1696,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",
@@ -2966,6 +2968,7 @@
"该规则未设置参数覆盖模板": "Cette règle n'a pas de modèle de remplacement de paramètres défini",
"该规则的缓存保留时长;0 表示使用默认 TTL:": "Durée de rétention du cache pour cette règle ; 0 signifie utiliser le TTL par défaut : ",
"该记录不包含可用的 token 统计口径。": "Cet enregistrement ne contient pas de statistiques de tokens disponibles.",
"该记录由旧版本实例写入,缺少审计信息,建议将实例升级至最新版本以便记录服务器IP、回调IP、支付方式与系统版本等审计字段。": "Cet enregistrement a été écrit par une ancienne version de l'instance et ne contient pas d'informations d'audit. Veuillez mettre à jour l'instance vers la dernière version afin d'enregistrer les champs d'audit tels que l'IP du serveur, l'IP de rappel, le mode de paiement et la version du système.",
"详情": "Détails",
"详见「特殊倍率」和「可用分组」标签页。": "See \"Special Ratios\" and \"Usable Groups\" tabs for details.",
"语言偏好": "Préférence linguistique",
+3
View File
@@ -1213,6 +1213,7 @@
"实际模型": "アップストリームモデル",
"实际结算金额:{{symbol}}{{total}}(已包含分组价格调整)": "実際の請求額:{{symbol}}{{total}}(グループ価格調整込み)",
"实际请求体": "実際のリクエストボディ",
"审计信息": "監査情報",
"容器": "Container",
"容器ID": "Container ID",
"容器创建失败: ": "Container creation failed: ",
@@ -1666,6 +1667,7 @@
"操作失败,请重试": "操作に失敗しました。再試行してください。",
"操作成功完成!": "操作が正常に完了しました",
"操作暂时被禁用": "この操作は一時的に無効にされています",
"操作管理员": "操作管理者",
"操作类型": "操作タイプ",
"操练场": "Playground",
"操练场和聊天功能": "プレイグラウンドとチャット機能",
@@ -2935,6 +2937,7 @@
"该规则未设置参数覆盖模板": "このルールにはパラメータオーバーライドテンプレートが設定されていません",
"该规则的缓存保留时长;0 表示使用默认 TTL:": "このルールのキャッシュ保持期間。0はデフォルトTTLを使用:",
"该记录不包含可用的 token 统计口径。": "このレコードには利用可能なトークン統計がありません。",
"该记录由旧版本实例写入,缺少审计信息,建议将实例升级至最新版本以便记录服务器IP、回调IP、支付方式与系统版本等审计字段。": "このレコードは古いバージョンのインスタンスによって書き込まれており、監査情報が欠落しています。サーバーIP、コールバックIP、支払い方法、システムバージョンなどの監査フィールドを記録するために、インスタンスを最新バージョンにアップグレードすることをお勧めします。",
"详情": "詳細",
"详见「特殊倍率」和「可用分组」标签页。": "詳しくは「特殊レート」と「利用可能グループ」タブをご覧ください。",
"语言偏好": "言語設定",
+3
View File
@@ -1234,6 +1234,7 @@
"实际模型": "Фактическая модель",
"实际结算金额:{{symbol}}{{total}}(已包含分组价格调整)": "Фактическое списание: {{symbol}}{{total}} (включая групповую ценовую корректировку)",
"实际请求体": "Фактическое тело запроса",
"审计信息": "Информация об аудите",
"容器": "Container",
"容器ID": "Container ID",
"容器创建失败: ": "Container creation failed: ",
@@ -1713,6 +1714,7 @@
"操作失败,请重试": "Операция не удалась, попробуйте еще раз",
"操作成功完成!": "Операция успешно завершена!",
"操作暂时被禁用": "Операция временно отключена",
"操作管理员": "Администратор операции",
"操作类型": "Тип операции",
"操练场": "Тренировочная площадка",
"操练场和聊天功能": "Тренировочная площадка и чат-функции",
@@ -2986,6 +2988,7 @@
"该规则未设置参数覆盖模板": "У этого правила не задан шаблон переопределения параметров",
"该规则的缓存保留时长;0 表示使用默认 TTL:": "Время хранения кэша для этого правила; 0 — использовать TTL по умолчанию: ",
"该记录不包含可用的 token 统计口径。": "Эта запись не содержит доступной статистики токенов.",
"该记录由旧版本实例写入,缺少审计信息,建议将实例升级至最新版本以便记录服务器IP、回调IP、支付方式与系统版本等审计字段。": "Эта запись была создана более старой версией экземпляра и не содержит данных аудита. Рекомендуется обновить экземпляр до последней версии, чтобы фиксировать поля аудита: IP сервера, IP callback, способ оплаты и версию системы.",
"详情": "Подробности",
"详见「特殊倍率」和「可用分组」标签页。": "See \"Special Ratios\" and \"Usable Groups\" tabs for details.",
"语言偏好": "Языковые настройки",
+3
View File
@@ -1214,6 +1214,7 @@
"实际模型": "Mô hình thực tế",
"实际结算金额:{{symbol}}{{total}}(已包含分组价格调整)": "Khoản phí thực tế: {{symbol}}{{total}} (đã bao gồm điều chỉnh giá theo nhóm)",
"实际请求体": "Thân yêu cầu thực tế",
"审计信息": "Thông tin kiểm toán",
"容器": "Container",
"容器ID": "Container ID",
"容器创建失败: ": "Container creation failed: ",
@@ -1667,6 +1668,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",
@@ -3296,6 +3298,7 @@
"该规则未设置参数覆盖模板": "Quy tắc này chưa thiết lập mẫu ghi đè tham số",
"该规则的缓存保留时长;0 表示使用默认 TTL:": "Thời gian lưu bộ nhớ đệm cho quy tắc này; 0 nghĩa là sử dụng TTL mặc định: ",
"该记录不包含可用的 token 统计口径。": "Bản ghi này không chứa thống kê token khả dụng.",
"该记录由旧版本实例写入,缺少审计信息,建议将实例升级至最新版本以便记录服务器IP、回调IP、支付方式与系统版本等审计字段。": "Bản ghi này được ghi bởi phiên bản cũ của instance và thiếu thông tin kiểm toán. Khuyến nghị nâng cấp instance lên phiên bản mới nhất để ghi lại các trường kiểm toán như IP máy chủ, IP callback, phương thức thanh toán và phiên bản hệ thống.",
"详情": "Chi tiết",
"详细信息": "Thông tin chi tiết",
"详见「特殊倍率」和「可用分组」标签页。": "See \"Special Ratios\" and \"Usable Groups\" tabs for details.",
+3
View File
@@ -1202,6 +1202,7 @@
"实际模型": "实际模型",
"实际结算金额:{{symbol}}{{total}}(已包含分组价格调整)": "实际结算金额:{{symbol}}{{total}}(已包含分组价格调整)",
"实际请求体": "实际请求体",
"审计信息": "审计信息",
"容器": "容器",
"容器ID": "容器ID",
"容器创建失败: ": "容器创建失败: ",
@@ -1655,6 +1656,7 @@
"操作成功完成!": "操作成功完成!",
"操作暂时被禁用": "操作暂时被禁用",
"操作确认": "操作确认",
"操作管理员": "操作管理员",
"操作类型": "操作类型",
"操练场": "操练场",
"操练场和聊天功能": "操练场和聊天功能",
@@ -2934,6 +2936,7 @@
"该规则未设置参数覆盖模板": "该规则未设置参数覆盖模板",
"该规则的缓存保留时长;0 表示使用默认 TTL:": "该规则的缓存保留时长;0 表示使用默认 TTL:",
"该记录不包含可用的 token 统计口径。": "该记录不包含可用的 token 统计口径。",
"该记录由旧版本实例写入,缺少审计信息,建议将实例升级至最新版本以便记录服务器IP、回调IP、支付方式与系统版本等审计字段。": "该记录由旧版本实例写入,缺少审计信息,建议将实例升级至最新版本以便记录服务器IP、回调IP、支付方式与系统版本等审计字段。",
"详情": "详情",
"详见「特殊倍率」和「可用分组」标签页。": "详见「特殊倍率」和「可用分组」标签页。",
"语言偏好": "语言偏好",
+3
View File
@@ -1212,6 +1212,7 @@
"实际模型": "實際模型",
"实际结算金额:{{symbol}}{{total}}(已包含分组价格调整)": "實際結算金額:{{symbol}}{{total}}(已包含分組價格調整)",
"实际请求体": "實際請求體",
"审计信息": "審計資訊",
"容器": "容器",
"容器ID": "容器ID",
"容器创建失败: ": "容器建立失敗: ",
@@ -1666,6 +1667,7 @@
"操作成功完成!": "操作成功完成!",
"操作暂时被禁用": "操作暫時被禁用",
"操作确认": "操作確認",
"操作管理员": "操作管理員",
"操作类型": "",
"操练场": "操練場",
"操练场和聊天功能": "操練場和聊天功能",
@@ -2946,6 +2948,7 @@
"该规则未设置参数覆盖模板": "",
"该规则的缓存保留时长;0 表示使用默认 TTL:": "",
"该记录不包含可用的 token 统计口径。": "",
"该记录由旧版本实例写入,缺少审计信息,建议将实例升级至最新版本以便记录服务器IP、回调IP、支付方式与系统版本等审计字段。": "此記錄由舊版本執行個體寫入,缺少審計資訊,建議將執行個體升級至最新版本,以便記錄伺服器IP、回調IP、支付方式與系統版本等審計欄位。",
"详情": "詳情",
"详见「特殊倍率」和「可用分组」标签页。": "詳見「特殊倍率」和「可用分組」標籤頁。",
"语言偏好": "語言偏好",