package controller import ( "errors" "fmt" "os" "strings" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/model" "github.com/gin-gonic/gin" ) // auditContentTemplates 将稳定的操作标识 action 映射为英文兜底模板,渲染后写入 // Log.Content(供导出等非本地化消费者使用)。占位符为 ${name},由该 // action 的 params 填充。本地化展示文案在前端 i18n 模板中维护,本表是语言中立的 // 英文基线——调用方因此无需在每个埋点处手写句子(避免与 params 重复书写同一份值)。 var auditContentTemplates = map[string]string{ "user.create": "Created user ${username} (role ${role})", "user.update": "Updated user ${username} (ID: ${id})", "user.delete": "Deleted user ${username} (ID: ${id})", "user.account_delete": "Account deletion", "user.manage": "Performed ${action} on user ${username} (ID: ${id})", "user.quota_add": "Increased user quota by ${quota}", "user.quota_subtract": "Decreased user quota by ${quota}", "user.quota_override": "Overrode user quota from ${from} to ${to}", "user.binding_clear": "Cleared ${bindingType} binding for user ${username}", "user.2fa_disable": "Force-disabled two-factor authentication for the user", "user.passkey_register": "Registered a passkey", "access_token.generate": "Generated a system access token", "access_token.revoke": "Revoked the system access token", "user.2fa_setup": "Started two-factor authentication setup", "user.2fa_enable": "Enabled two-factor authentication", "user.2fa_disable_self": "Disabled two-factor authentication", "user.2fa_backup_codes": "Regenerated two-factor backup codes", "user.security_verify": "Completed security verification", "user.password_change": "Account password change", "user.binding_start": "Account binding request", "user.binding_bind": "Account binding", "user.binding_unbind": "Account unlinking", "user.email_binding_resend": "Email confirmation code resend", "user.passkey_delete": "Deleted a passkey", "user.reset_passkey": "Reset the user passkey", "option.update": "Updated system setting ${key}", "option.passkey_domains": "Updated Passkey domains: removed ${domains}; affected ${known}; unknown ${unknown}", "option.passkey_domains_confirmed": "Confirmed removal of Passkey domains: ${domains}; affected ${known}; unknown ${unknown}", "option.passkey_domains_blocked": "Passkey domain change blocked: ${domains}; affected ${known}; unknown ${unknown}", "option.passkey_domains_failed": "Passkey domain update failed", "channel.create": "Created channel ${name} (type ${type}, count ${count})", "channel.update": "Updated channel ${name} (ID: ${id})", "channel.delete": "Deleted channel ${name} (ID: ${id})", "channel.delete_batch": "Batch deleted ${count} channels", "channel.delete_disabled": "Deleted all disabled channels (${count})", "channel.key_view": "Viewed channel key ${name} (ID: ${id})", "channel.tag_disable": "Disabled channels with tag ${tag}", "channel.tag_enable": "Enabled channels with tag ${tag}", "channel.tag_edit": "Edited channels with tag ${tag}", "channel.tag_batch_set": "Batch set tag for ${count} channels", "channel.copy": "Copied channel (source ID: ${sourceId}) to ${name} (new ID: ${id})", "channel.multi_key_manage": "Multi-key management ${action} on channel (ID: ${id})", "channel.upstream_apply": "Applied upstream model changes to channel (ID: ${id})", "channel.upstream_apply_all": "Applied upstream model changes to ${count} channels", "redemption.create": "Created ${count} redemption codes named ${name} (${quota} each)", "redemption.delete_batch": "Batch deleted ${count} redemption codes", "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}", } func recordPasskeyDomainAudit(c *gin.Context, change *model.PasskeyDomainChange, confirmed bool, err error) { confirmed = confirmed && err == nil && change != nil && len(change.RemovedRPIDs) > 0 params := map[string]any{"success": err == nil, "confirmed": confirmed} if change != nil { params["domains"] = strings.Join(change.RemovedRPIDs, ", ") params["removed_rp_ids"] = change.RemovedRPIDs params["known"] = change.AffectedCredentials params["unknown"] = change.UnknownCredentials params["previous_rp_id"] = change.PreviousRPID params["effective_rp_id"] = change.EffectiveRPID } action := "option.passkey_domains" if errors.Is(err, model.ErrPasskeyDomainRemovalConfirmation) { action = "option.passkey_domains_blocked" } else if err != nil { action = "option.passkey_domains_failed" } else if confirmed && change != nil && len(change.RemovedRPIDs) > 0 { action = "option.passkey_domains_confirmed" } auditInfo := &model.AuditRequestInfo{ Method: c.Request.Method, Route: c.FullPath(), Path: c.FullPath(), Status: c.Writer.Status(), Success: err == nil, } model.RecordOperationAuditLog(c.GetInt("id"), c.GetInt("role"), auditContentEN(action, params), c.ClientIP(), action, params, auditOperatorInfo(c), auditInfo, c) markAuditLogged(c) } // auditContentEN 按 action 模板渲染英文兜底文本;未登记的 action 退回 action 本身。 func auditContentEN(action string, params map[string]any) string { tmpl, ok := auditContentTemplates[action] if !ok { return action } return os.Expand(tmpl, func(key string) string { if v, ok := params[key]; ok { return fmt.Sprintf("%v", v) } return "" }) } // auditOperatorInfo 从上下文构建操作者身份信息(管理员 id/用户名/角色)。 func auditOperatorInfo(c *gin.Context) *model.AuditAdminInfo { return &model.AuditAdminInfo{ AdminID: c.GetInt("id"), AdminUsername: c.GetString("username"), AdminRole: c.GetInt("role"), AuthMethod: auditAuthMethod(c), } } func auditAuthMethod(c *gin.Context) string { if c.GetBool("use_access_token") { return "access_token" } return "session" } // markAuditLogged 标记当前请求已在 handler 内手动记录审计日志, // 使鉴权链路中的审计兜底(finishAdminAudit)跳过兜底记录,避免重复。 func markAuditLogged(c *gin.Context) { common.SetContextKey(c, constant.ContextKeyAuditLogged, true) } // recordManageAudit 记录一条由操作者本人归属的管理/高危审计日志(资源类操作: // 渠道 / 系统设置 / 兑换码等)。content 由 action+params 自动渲染。 func recordManageAudit(c *gin.Context, action string, params map[string]any) { recordManageAuditFor(c, c.GetInt("id"), action, params) } // recordManageAuditFor 记录一条管理审计日志,日志归属于操作者;targetUserId // 只表示被操作用户,用于在结构化参数中保留目标上下文。 func recordManageAuditFor(c *gin.Context, targetUserId int, action string, params map[string]any) { if params == nil { params = map[string]any{} } operatorUserId := c.GetInt("id") if _, ok := params["target_user_id"]; !ok && targetUserId > 0 && targetUserId != operatorUserId { params["target_user_id"] = targetUserId } model.RecordOperationAuditLog(operatorUserId, c.GetInt("role"), auditContentEN(action, params), c.ClientIP(), action, params, auditOperatorInfo(c), nil, c) markAuditLogged(c) } // recordUserSecurityAudit 记录普通用户自己的安全敏感操作(如 passkey 绑定/解绑)。 // 这类日志没有管理员操作者,不写 admin_info;同时不依赖 AdminAuth/RootAuth 的兜底。 func recordUserSecurityAudit(c *gin.Context, userId int, action string, params map[string]any) { if code := c.GetString("security_error_code"); code != "" { if params == nil { params = map[string]any{} } params["code"] = code } var auditInfo *model.AuditRequestInfo if success, ok := params["success"].(bool); ok { auditInfo = &model.AuditRequestInfo{ Method: c.Request.Method, Route: c.FullPath(), Path: c.FullPath(), Status: c.Writer.Status(), Success: success, } } model.RecordOperationAuditLog(userId, c.GetInt("role"), auditContentEN(action, params), c.ClientIP(), action, params, nil, auditInfo, c) } func tokenAuditParams(c *gin.Context) model.AuditFields { params, ok := common.GetContextKeyType[model.AuditFields](c, constant.ContextKeyTokenAuditParams) if !ok { params = model.AuditFields{} common.SetContextKey(c, constant.ContextKeyTokenAuditParams, params) } return params } func tokenBatchAuditParams(c *gin.Context, ids []int) model.AuditFields { params := tokenAuditParams(c) params["total"] = len(ids) // Bound audit payloads without changing the batch operation's limits. params["requested_ids"] = append([]int{}, ids[:min(len(ids), 100)]...) if len(ids) > 100 { params["requested_ids_truncated"] = true } return params }