Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions controller/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ var auditContentTemplates = map[string]string{

"subscription.plan_reset": "Reset active subscriptions for plan ${plan_id}",
"subscription.user_plan_reset": "Reset active plan ${plan_id} subscriptions for user ${target_user_id}",

"token.admin_create": "Created token for user ${target_username} (ID: ${target_user_id})",
"token.admin_update": "Updated token ${token_name} (ID: ${token_id}) of user ${target_username}",
"token.admin_delete": "Deleted token ${token_name} (ID: ${token_id}) of user ${target_username}",
"token.admin_batch_delete": "Batch deleted ${count} tokens of user ${target_username}",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

The batch template assumes one owner.

token.admin_batch_delete renders ${target_username}, but a batch can contain tokens from several users. For a mixed batch the placeholder expands to an empty string, because auditContentEN returns "" for missing keys. Drop the owner from the template, or record one audit event per owner.

-	"token.admin_batch_delete": "Batch deleted ${count} tokens of user ${target_username}",
+	"token.admin_batch_delete": "Batch deleted ${count} tokens (IDs: ${token_ids})",

No handler currently emits this action; that gap is raised on controller/token.go Lines 543-557.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"token.admin_batch_delete": "Batch deleted ${count} tokens of user ${target_username}",
"token.admin_batch_delete": "Batch deleted ${count} tokens (IDs: ${token_ids})",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/audit.go` at line 56, Update the token.admin_batch_delete audit
template and its associated batch-delete audit flow to avoid assuming a single
target_username; use an owner-independent message for mixed batches, or emit
separate audit events per owner. Also address the missing audit emission in the
batch-delete handler near the token deletion flow so the corrected action is
actually recorded.

}

// auditContentEN 按 action 模板渲染英文兜底文本;未登记的 action 退回 action 本身。
Expand Down
157 changes: 138 additions & 19 deletions controller/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,27 @@ func setTokenAutoGroups(c *gin.Context, token *model.Token, groups []string) boo
func GetAllTokens(c *gin.Context) {
userId := c.GetInt("id")
pageInfo := common.GetPageQuery(c)
tokens, err := model.GetAllUserTokens(userId, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
if err != nil {
common.ApiError(c, err)
return
// Root 可不传 user_id 看全部,也可传 ?user_id=X 筛选
if c.GetInt("role") == common.RoleRootUser {
filterUserId, _ := strconv.Atoi(c.Query("user_id"))
tokens, err := model.GetAllTokensAdmin(filterUserId, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
if err != nil {
common.ApiError(c, err)
return
}
total, _ := model.CountTokensAdmin(filterUserId)
pageInfo.SetTotal(int(total))
pageInfo.SetItems(buildMaskedTokenResponses(tokens))
} else {
tokens, err := model.GetAllUserTokens(userId, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
if err != nil {
common.ApiError(c, err)
return
}
total, _ := model.CountUserTokens(userId)
pageInfo.SetTotal(int(total))
pageInfo.SetItems(buildMaskedTokenResponses(tokens))
}
total, _ := model.CountUserTokens(userId)
pageInfo.SetTotal(int(total))
pageInfo.SetItems(buildMaskedTokenResponses(tokens))
common.ApiSuccess(c, pageInfo)
}

Expand All @@ -137,13 +150,25 @@ func SearchTokens(c *gin.Context) {

pageInfo := common.GetPageQuery(c)

tokens, total, err := model.SearchUserTokens(userId, keyword, token, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
if err != nil {
common.ApiError(c, err)
return
// Root 可不传 user_id 搜全部,也可传 ?user_id=X 筛选
if c.GetInt("role") == common.RoleRootUser {
filterUserId, _ := strconv.Atoi(c.Query("user_id"))
tokens, total, err := model.SearchTokensAdmin(filterUserId, keyword, token, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
if err != nil {
common.ApiError(c, err)
return
}
pageInfo.SetTotal(int(total))
pageInfo.SetItems(buildMaskedTokenResponses(tokens))
} else {
tokens, total, err := model.SearchUserTokens(userId, keyword, token, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
if err != nil {
common.ApiError(c, err)
return
}
pageInfo.SetTotal(int(total))
pageInfo.SetItems(buildMaskedTokenResponses(tokens))
}
pageInfo.SetTotal(int(total))
pageInfo.SetItems(buildMaskedTokenResponses(tokens))
common.ApiSuccess(c, pageInfo)
}

Expand All @@ -154,7 +179,18 @@ func GetToken(c *gin.Context) {
common.ApiError(c, err)
return
}
token, err := model.GetTokenByIds(id, userId)
// Root 可查看任意用户的令牌,非 Root 仅限自己的
var token *model.Token
if c.GetInt("role") == common.RoleRootUser {
token, err = model.GetTokenById(id)
if err == nil {
if user, uErr := model.GetUserCache(token.UserId); uErr == nil {
token.Username = user.Username
}
}
} else {
token, err = model.GetTokenByIds(id, userId)
}
if err != nil {
common.ApiError(c, err)
return
Expand All @@ -181,7 +217,13 @@ func GetTokenKey(c *gin.Context) {
common.ApiError(c, err)
return
}
token, err := model.GetTokenByIds(id, userId)
// Root 可查看任意令牌的 Key,非 Root 仅限自己的
var token *model.Token
if c.GetInt("role") == common.RoleRootUser {
token, err = model.GetTokenById(id)
} else {
token, err = model.GetTokenByIds(id, userId)
}
if err != nil {
common.ApiError(c, err)
return
Expand Down Expand Up @@ -269,6 +311,18 @@ func AddToken(c *gin.Context) {
return
}
token := request.Token

// Root 可通过 user_id 为其他用户创建令牌
isAdminCreate := c.GetInt("role") == common.RoleRootUser && token.UserId > 0 && token.UserId != c.GetInt("id")
if isAdminCreate {
if _, err := model.GetUserCache(token.UserId); err != nil {
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
return
}
} else {
token.UserId = c.GetInt("id")
}

if len(token.Name) > 50 {
common.ApiErrorI18n(c, i18n.MsgTokenNameTooLong)
return
Expand All @@ -287,7 +341,7 @@ func AddToken(c *gin.Context) {
}
// 检查用户令牌数量是否已达上限
maxTokens := operation_setting.GetMaxUserTokens()
count, err := model.CountUserTokens(c.GetInt("id"))
count, err := model.CountUserTokens(token.UserId)
if err != nil {
common.ApiError(c, err)
return
Expand All @@ -314,7 +368,7 @@ func AddToken(c *gin.Context) {
return
}
cleanToken := model.Token{
UserId: c.GetInt("id"),
UserId: token.UserId,
Name: token.Name,
Key: key,
CreatedTime: common.GetTimestamp(),
Expand All @@ -334,6 +388,13 @@ func AddToken(c *gin.Context) {
common.ApiError(c, err)
return
}
if isAdminCreate {
recordManageAuditFor(c, token.UserId, "token.admin_create", map[string]interface{}{
"target_user_id": token.UserId,
"target_username": cleanToken.Username,
"token_name": cleanToken.Name,
})
}
Comment on lines +391 to +397

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

target_username is always empty in the create audit.

cleanToken is built at Line 370 without a Username value, and Insert() does not populate it. The audit template token.admin_create in controller/audit.go expands ${target_username} to an empty string. Reuse the user already loaded at Line 318.

🛠️ Proposed fix
 	isAdminCreate := c.GetInt("role") == common.RoleRootUser && token.UserId > 0 && token.UserId != c.GetInt("id")
+	var targetUser *model.User
 	if isAdminCreate {
-		if _, err := model.GetUserCache(token.UserId); err != nil {
+		user, err := model.GetUserCache(token.UserId)
+		if err != nil {
 			common.ApiErrorI18n(c, i18n.MsgInvalidParams)
 			return
 		}
+		targetUser = user
 	} else {
 	if isAdminCreate {
 		recordManageAuditFor(c, token.UserId, "token.admin_create", map[string]interface{}{
 			"target_user_id":  token.UserId,
-			"target_username": cleanToken.Username,
+			"target_username": targetUser.Username,
 			"token_name":      cleanToken.Name,
 		})
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if isAdminCreate {
recordManageAuditFor(c, token.UserId, "token.admin_create", map[string]interface{}{
"target_user_id": token.UserId,
"target_username": cleanToken.Username,
"token_name": cleanToken.Name,
})
}
if isAdminCreate {
recordManageAuditFor(c, token.UserId, "token.admin_create", map[string]interface{}{
"target_user_id": token.UserId,
"target_username": targetUser.Username,
"token_name": cleanToken.Name,
})
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/token.go` around lines 391 - 397, Update the admin-create audit
block in the token creation flow to populate target_username from the user
loaded earlier in the handler, rather than cleanToken.Username. Keep the
existing audit event and other fields unchanged so token.admin_create receives
the loaded user’s username.

c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
Expand All @@ -343,7 +404,29 @@ func AddToken(c *gin.Context) {
func DeleteToken(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
userId := c.GetInt("id")
err := model.DeleteTokenById(id, userId)
// Root 可删除任意用户的令牌,非 Root 仅限自己的
var err error
if c.GetInt("role") == common.RoleRootUser {
token, tErr := model.GetTokenById(id)
if tErr != nil {
common.ApiError(c, tErr)
return
}
if token.UserId != userId {
targetUser, _ := model.GetUserCache(token.UserId)
if targetUser != nil {
recordManageAuditFor(c, token.UserId, "token.admin_delete", map[string]interface{}{
"target_user_id": token.UserId,
"target_username": targetUser.Username,
"token_id": token.Id,
"token_name": token.Name,
})
}
}
err = token.Delete()
} else {
err = model.DeleteTokenById(id, userId)
}
Comment on lines +407 to +429

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Record the delete audit after the delete succeeds.

recordManageAuditFor runs before token.Delete(). If the delete fails, the audit trail still reports a completed administrative deletion. Move the audit call after the error check.

🛠️ Proposed fix
-		if token.UserId != userId {
-			targetUser, _ := model.GetUserCache(token.UserId)
-			if targetUser != nil {
-				recordManageAuditFor(c, token.UserId, "token.admin_delete", map[string]interface{}{
-					"target_user_id":  token.UserId,
-					"target_username": targetUser.Username,
-					"token_id":        token.Id,
-					"token_name":      token.Name,
-				})
-			}
-		}
 		err = token.Delete()
+		if err == nil && token.UserId != userId {
+			targetUser, _ := model.GetUserCache(token.UserId)
+			if targetUser != nil {
+				recordManageAuditFor(c, token.UserId, "token.admin_delete", map[string]interface{}{
+					"target_user_id":  token.UserId,
+					"target_username": targetUser.Username,
+					"token_id":        token.Id,
+					"token_name":      token.Name,
+				})
+			}
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Root 可删除任意用户的令牌,非 Root 仅限自己的
var err error
if c.GetInt("role") == common.RoleRootUser {
token, tErr := model.GetTokenById(id)
if tErr != nil {
common.ApiError(c, tErr)
return
}
if token.UserId != userId {
targetUser, _ := model.GetUserCache(token.UserId)
if targetUser != nil {
recordManageAuditFor(c, token.UserId, "token.admin_delete", map[string]interface{}{
"target_user_id": token.UserId,
"target_username": targetUser.Username,
"token_id": token.Id,
"token_name": token.Name,
})
}
}
err = token.Delete()
} else {
err = model.DeleteTokenById(id, userId)
}
// Root 可删除任意用户的令牌,非 Root 仅限自己的
var err error
if c.GetInt("role") == common.RoleRootUser {
token, tErr := model.GetTokenById(id)
if tErr != nil {
common.ApiError(c, tErr)
return
}
err = token.Delete()
if err == nil && token.UserId != userId {
targetUser, _ := model.GetUserCache(token.UserId)
if targetUser != nil {
recordManageAuditFor(c, token.UserId, "token.admin_delete", map[string]interface{}{
"target_user_id": token.UserId,
"target_username": targetUser.Username,
"token_id": token.Id,
"token_name": token.Name,
})
}
}
} else {
err = model.DeleteTokenById(id, userId)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/token.go` around lines 407 - 429, Move the administrative audit
call in the root-user branch of the token deletion flow to execute only after
token.Delete() succeeds. Check the returned err before invoking
recordManageAuditFor, while preserving the existing target-user condition and
audit payload.

if err != nil {
common.ApiError(c, err)
return
Expand Down Expand Up @@ -379,7 +462,13 @@ func UpdateToken(c *gin.Context) {
return
}
}
cleanToken, err := model.GetTokenByIds(token.Id, userId)
// Root 可更新任意用户的令牌,非 Root 仅限自己的
var cleanToken *model.Token
if c.GetInt("role") == common.RoleRootUser {
cleanToken, err = model.GetTokenById(token.Id)
} else {
cleanToken, err = model.GetTokenByIds(token.Id, userId)
}
if err != nil {
common.ApiError(c, err)
return
Expand Down Expand Up @@ -421,6 +510,17 @@ func UpdateToken(c *gin.Context) {
common.ApiError(c, err)
return
}
if cleanToken.UserId != userId {
targetUser, _ := model.GetUserCache(cleanToken.UserId)
if targetUser != nil {
recordManageAuditFor(c, cleanToken.UserId, "token.admin_update", map[string]interface{}{
"target_user_id": cleanToken.UserId,
"target_username": targetUser.Username,
"token_id": cleanToken.Id,
"token_name": cleanToken.Name,
})
}
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
Expand All @@ -440,6 +540,21 @@ func DeleteTokenBatch(c *gin.Context) {
}
userId := c.GetInt("id")
count, err := model.BatchDeleteTokens(tokenBatch.Ids, userId)
// Root 可批量删除任意用户的令牌
if err != nil && c.GetInt("role") == common.RoleRootUser {
for _, id := range tokenBatch.Ids {
_, tErr := model.GetTokenById(id)
if tErr != nil {
common.ApiError(c, tErr)
return
}
}
count, err = model.BatchDeleteTokensAdmin(tokenBatch.Ids)
if err != nil {
common.ApiError(c, err)
return
}
}
Comment on lines +543 to +557

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Root batch fallbacks are unreachable because the scoped model functions do not return an error. model.BatchDeleteTokens and model.GetTokenKeysByIds return an empty result with a nil error when no row matches the user_id scope. Both handlers trigger the administrator path only on err != nil, so a root user operating on another user's tokens receives a success response with no effect.

  • controller/token.go#L543-L557: select model.BatchDeleteTokensAdmin by role instead of by error, and record a token.admin_batch_delete audit event after a successful root deletion.
  • controller/token.go#L581-L584: select model.GetTokenKeysByIdsAdmin by role instead of by error.
📍 Affects 1 file
  • controller/token.go#L543-L557 (this comment)
  • controller/token.go#L581-L584
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/token.go` around lines 543 - 557, The root batch operations
currently choose administrator fallbacks only when scoped model calls return
errors, so root actions on other users’ tokens do nothing. In
controller/token.go lines 543-557, branch on the root role before using the
scoped deletion, call model.BatchDeleteTokensAdmin, and record a
token.admin_batch_delete audit event after successful deletion; in lines
581-584, likewise select model.GetTokenKeysByIdsAdmin by role instead of by
error.

if err != nil {
common.ApiError(c, err)
return
Expand All @@ -463,6 +578,10 @@ func GetTokenKeysBatch(c *gin.Context) {
}
userId := c.GetInt("id")
tokens, err := model.GetTokenKeysByIds(tokenBatch.Ids, userId)
// Root 可批量获取任意令牌的 Key
if err != nil && c.GetInt("role") == common.RoleRootUser {
tokens, err = model.GetTokenKeysByIdsAdmin(tokenBatch.Ids)
}
if err != nil {
common.ApiError(c, err)
return
Expand Down
Loading