fix(permissions):修复权限相关问题
This commit is contained in:
1 parent
8d4e8744d5
commit
9d61abda86
23 files changed
+225
-33
No files matched your search
@@ -37,6 +37,7 @@ type IAdminV1 interface {
|
||||
ForwardingTraceGet(ctx context.Context, req *v1.ForwardingTraceGetReq) (res *v1.ForwardingTraceGetRes, err error)
|
||||
OperationLogExport(ctx context.Context, req *v1.OperationLogExportReq) (res *v1.OperationLogExportRes, err error)
|
||||
ContentFilterLogList(ctx context.Context, req *v1.ContentFilterLogListReq) (res *v1.ContentFilterLogListRes, err error)
|
||||
ContentFilterLogClear(ctx context.Context, req *v1.ContentFilterLogClearReq) (res *v1.ContentFilterLogClearRes, err error)
|
||||
AdminLogin(ctx context.Context, req *v1.AdminLoginReq) (res *v1.AdminLoginRes, err error)
|
||||
AdminMe(ctx context.Context, req *v1.AdminMeReq) (res *v1.AdminMeRes, err error)
|
||||
AdminLogout(ctx context.Context, req *v1.AdminLogoutReq) (res *v1.AdminLogoutRes, err error)
|
||||
@@ -75,6 +76,7 @@ type IAdminV1 interface {
|
||||
ChannelErrorTrend(ctx context.Context, req *v1.ChannelErrorTrendReq) (res *v1.ChannelErrorTrendRes, err error)
|
||||
ChannelErrorTopChannels(ctx context.Context, req *v1.ChannelErrorTopChannelsReq) (res *v1.ChannelErrorTopChannelsRes, err error)
|
||||
ChannelErrorCategories(ctx context.Context, req *v1.ChannelErrorCategoriesReq) (res *v1.ChannelErrorCategoriesRes, err error)
|
||||
ChannelErrorClear(ctx context.Context, req *v1.ChannelErrorClearReq) (res *v1.ChannelErrorClearRes, err error)
|
||||
ChannelOAuthAuthURL(ctx context.Context, req *v1.ChannelOAuthAuthURLReq) (res *v1.ChannelOAuthAuthURLRes, err error)
|
||||
ChannelOAuthExchange(ctx context.Context, req *v1.ChannelOAuthExchangeReq) (res *v1.ChannelOAuthExchangeRes, err error)
|
||||
ChannelOAuthRefresh(ctx context.Context, req *v1.ChannelOAuthRefreshReq) (res *v1.ChannelOAuthRefreshRes, err error)
|
||||
|
||||
@@ -142,3 +142,13 @@ type ContentFilterLogListRes struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// ContentFilterLogClearReq 清空全部内容过滤拦截日志(硬删除,用于日志大量堆积时快速释放数据库空间)
|
||||
type ContentFilterLogClearReq struct {
|
||||
g.Meta `path:"/audit/content-filter-logs/clear" method:"delete" mime:"json" tags:"管理后台-审计" summary:"清空内容过滤拦截日志(硬删除全部)"`
|
||||
}
|
||||
|
||||
// ContentFilterLogClearRes 清空内容过滤拦截日志响应
|
||||
type ContentFilterLogClearRes struct {
|
||||
Deleted int64 `json:"deleted" dc:"删除的记录数"`
|
||||
}
|
||||
@@ -97,11 +97,14 @@ type AdminSessionItem struct {
|
||||
UserId int64 `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
DisplayName string `json:"display_name"`
|
||||
IpAddress string `json:"ip_address"`
|
||||
DeviceInfo string `json:"device_info"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
IsCurrent bool `json:"is_current"`
|
||||
// Role 是会话所属账号的角色(super_admin / admin),
|
||||
// 前端据此隐藏非本人超管会话的撤销入口(后端另有 assertCanOperateSessions 兜底)
|
||||
Role string `json:"role"`
|
||||
IpAddress string `json:"ip_address"`
|
||||
DeviceInfo string `json:"device_info"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
IsCurrent bool `json:"is_current"`
|
||||
}
|
||||
|
||||
// AdminRevokeSessionReq 踢出指定会话请求
|
||||
|
||||
@@ -64,3 +64,13 @@ type ChannelErrorCategoriesReq struct {
|
||||
type ChannelErrorCategoriesRes struct {
|
||||
Data []map[string]string `json:"data"`
|
||||
}
|
||||
|
||||
// ChannelErrorClearReq 清空全部渠道错误事件(硬删除,用于错误大量堆积时快速释放数据库空间)
|
||||
type ChannelErrorClearReq struct {
|
||||
g.Meta `path:"/monitor/channel-errors/clear" method:"delete" mime:"json" tags:"管理后台-监控" summary:"清空渠道错误事件(硬删除全部)"`
|
||||
}
|
||||
|
||||
// ChannelErrorClearRes 清空渠道错误事件响应
|
||||
type ChannelErrorClearRes struct {
|
||||
Deleted int64 `json:"deleted" dc:"删除的记录数"`
|
||||
}
|
||||
@@ -34,3 +34,6 @@ func (c *ControllerV1) ContentFilterLogList(ctx context.Context, req *v1.Content
|
||||
func (c *ControllerV1) ForwardingTraceGet(ctx context.Context, req *v1.ForwardingTraceGetReq) (res *v1.ForwardingTraceGetRes, err error) {
|
||||
return service.Admin().ForwardingTraceGet(ctx, req)
|
||||
}
|
||||
func (c *ControllerV1) ContentFilterLogClear(ctx context.Context, req *v1.ContentFilterLogClearReq) (res *v1.ContentFilterLogClearRes, err error) {
|
||||
return service.Admin().ContentFilterLogClear(ctx, req)
|
||||
}
|
||||
@@ -22,3 +22,6 @@ func (c *ControllerV1) ChannelErrorTopChannels(ctx context.Context, req *v1.Chan
|
||||
func (c *ControllerV1) ChannelErrorCategories(ctx context.Context, req *v1.ChannelErrorCategoriesReq) (res *v1.ChannelErrorCategoriesRes, err error) {
|
||||
return service.Admin().ChannelErrorCategories(ctx, req)
|
||||
}
|
||||
func (c *ControllerV1) ChannelErrorClear(ctx context.Context, req *v1.ChannelErrorClearReq) (res *v1.ChannelErrorClearRes, err error) {
|
||||
return service.Admin().ChannelErrorClear(ctx, req)
|
||||
}
|
||||
@@ -600,3 +600,18 @@ func (s *sAdmin) ContentFilterLogList(ctx context.Context, req *v1.ContentFilter
|
||||
PageSize: pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ContentFilterLogClear 硬删除全部内容过滤拦截日志。
|
||||
// 拦截日志为追加型审计流水,堆积后用 TRUNCATE 秒级清空并立即归还磁盘空间
|
||||
// (DELETE 需等 VACUUM 回收)。TRUNCATE 不返回行数,故先取删除前条数作为反馈。
|
||||
// 表无数据库级外键,TRUNCATE 安全。
|
||||
func (s *sAdmin) ContentFilterLogClear(ctx context.Context, _ *v1.ContentFilterLogClearReq) (*v1.ContentFilterLogClearRes, error) {
|
||||
count, err := dao.AudContentFilterLogs.Ctx(ctx).Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := g.DB().Ctx(ctx).Exec(ctx, "TRUNCATE TABLE aud_content_filter_logs RESTART IDENTITY"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.ContentFilterLogClearRes{Deleted: int64(count)}, nil
|
||||
}
|
||||
@@ -344,9 +344,9 @@ func (s *sAdmin) ListSessions(ctx context.Context, req *v1.AdminSessionListReq)
|
||||
ids = append(ids, id)
|
||||
}
|
||||
var users []entity.SysAdminUsers
|
||||
_ = dao.SysAdminUsers.Ctx(ctx).Fields("id, username, display_name").WhereIn("id", ids).Scan(&users)
|
||||
_ = dao.SysAdminUsers.Ctx(ctx).Fields("id, username, display_name, role").WhereIn("id", ids).Scan(&users)
|
||||
for _, u := range users {
|
||||
userMap[u.Id] = adminUserBrief{Username: u.Username, DisplayName: u.DisplayName}
|
||||
userMap[u.Id] = adminUserBrief{Username: u.Username, DisplayName: u.DisplayName, Role: u.Role}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,6 +357,7 @@ func (s *sAdmin) ListSessions(ctx context.Context, req *v1.AdminSessionListReq)
|
||||
UserId: sess.UserId,
|
||||
Username: userMap[sess.UserId].Username,
|
||||
DisplayName: userMap[sess.UserId].DisplayName,
|
||||
Role: userMap[sess.UserId].Role,
|
||||
IpAddress: sess.IpAddress,
|
||||
DeviceInfo: sess.DeviceInfo,
|
||||
IsCurrent: sess.Id == currentSessionID,
|
||||
@@ -377,6 +378,31 @@ func (s *sAdmin) ListSessions(ctx context.Context, req *v1.AdminSessionListReq)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// assertCanOperateSessions 校验当前调用者能否处置目标管理员账号的会话。
|
||||
//
|
||||
// 超级管理员的登录态任何人都不能代为操作(包括其他超管):踢掉超管会话等同于
|
||||
// 掐断平台最高权限入口,一旦下放就存在「借会话管理打击超管」的滥用路径。
|
||||
// 唯一例外是账号本人管理自己的登录设备(等同在其他设备登出)。
|
||||
// 查看不受限:会话列表对超管账号仍然展示,只是不可操作。
|
||||
func assertCanOperateSessions(ctx context.Context, targetUserID int64) error {
|
||||
if targetUserID == 0 {
|
||||
return nil
|
||||
}
|
||||
var user *entity.SysAdminUsers
|
||||
err := dao.SysAdminUsers.Ctx(ctx).Where("id", targetUserID).Scan(&user)
|
||||
if err = common.IgnoreScanNoRows(err); err != nil {
|
||||
return err
|
||||
}
|
||||
// 目标账号不存在或不是超管:无可保护对象,交由路由层 user:edit 把关
|
||||
if user == nil || user.Role != "super_admin" {
|
||||
return nil
|
||||
}
|
||||
if common.GetCtxUserID(ctx) == targetUserID {
|
||||
return nil
|
||||
}
|
||||
return common.NewBusinessError(consts.CodeForbidden, "超级管理员的会话仅限本人管理,其他人只能查看")
|
||||
}
|
||||
|
||||
// RevokeSession revokes a specific session.
|
||||
func (s *sAdmin) RevokeSession(ctx context.Context, req *v1.AdminRevokeSessionReq) (*v1.AdminRevokeSessionRes, error) {
|
||||
// Look up session to get jti for Redis revocation
|
||||
@@ -385,6 +411,9 @@ func (s *sAdmin) RevokeSession(ctx context.Context, req *v1.AdminRevokeSessionRe
|
||||
return nil, err
|
||||
}
|
||||
if sess != nil {
|
||||
if err := assertCanOperateSessions(ctx, sess.UserId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
common.MarkSessionRevoked(ctx, sess.Jti)
|
||||
}
|
||||
err = common.RevokeSession(ctx, "admin", req.Id)
|
||||
@@ -396,6 +425,9 @@ func (s *sAdmin) RevokeSession(ctx context.Context, req *v1.AdminRevokeSessionRe
|
||||
|
||||
// ForceLogout revokes all sessions for a specific user.
|
||||
func (s *sAdmin) ForceLogout(ctx context.Context, req *v1.AdminForceLogoutReq) (*v1.AdminForceLogoutRes, error) {
|
||||
if err := assertCanOperateSessions(ctx, req.Id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err := common.RevokeAllSessions(ctx, "admin", req.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
v1 "github.com/qianfree/team-api/api/admin/v1"
|
||||
"github.com/qianfree/team-api/internal/dao"
|
||||
"github.com/qianfree/team-api/internal/logic/common"
|
||||
)
|
||||
|
||||
@@ -195,3 +196,18 @@ func (s *sAdmin) ChannelErrorCategories(ctx context.Context, req *v1.ChannelErro
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ChannelErrorClear 硬删除全部渠道错误事件。
|
||||
// 错误风暴时事件表堆积很快,用 TRUNCATE 而非逐行 DELETE 秒级清空并立即归还磁盘空间。
|
||||
// TRUNCATE 不返回行数,故先取删除前条数作为反馈。表无数据库级外键,TRUNCATE 安全。
|
||||
// 注意异步写入器(DefaultChannelErrorWriter)清空后继续写入新事件,属预期行为。
|
||||
func (s *sAdmin) ChannelErrorClear(ctx context.Context, _ *v1.ChannelErrorClearReq) (*v1.ChannelErrorClearRes, error) {
|
||||
count, err := dao.ChnErrorEvents.Ctx(ctx).Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := g.DB().Ctx(ctx).Exec(ctx, "TRUNCATE TABLE chn_error_events RESTART IDENTITY"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.ChannelErrorClearRes{Deleted: int64(count)}, nil
|
||||
}
|
||||
@@ -69,7 +69,7 @@ var predefinedPermissionGroups = []v1.PermissionGroup{
|
||||
Name: "audit",
|
||||
Label: "审计日志",
|
||||
Permissions: []string{
|
||||
"audit:view", "audit:export", "audit:read_sensitive",
|
||||
"audit:view", "audit:export", "audit:read_sensitive", "audit:clear",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -97,7 +97,8 @@ var predefinedPermissionGroups = []v1.PermissionGroup{
|
||||
Name: "system",
|
||||
Label: "系统设置",
|
||||
Permissions: []string{
|
||||
"system:view", "system:edit", "system:update", "system:plugin",
|
||||
// 注:在线自更新无权限点 —— 整个 update 域硬性限定超管(superAdminOnlyRules)
|
||||
"system:view", "system:edit", "system:plugin",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -110,7 +110,7 @@ var tierPermissions = map[string]map[string][]string{
|
||||
"audit": {
|
||||
TierRead: {"audit:view"},
|
||||
TierOperate: {"audit:view", "audit:export"},
|
||||
TierFull: {"audit:view", "audit:export", "audit:read_sensitive"},
|
||||
TierFull: {"audit:view", "audit:export", "audit:read_sensitive", "audit:clear"},
|
||||
},
|
||||
"user": {
|
||||
TierRead: {"user:view"},
|
||||
@@ -120,7 +120,8 @@ var tierPermissions = map[string]map[string][]string{
|
||||
"system": {
|
||||
TierRead: {"system:view"},
|
||||
TierOperate: {"system:view", "system:edit"},
|
||||
TierFull: {"system:view", "system:edit", "system:update", "system:plugin"},
|
||||
// 在线自更新硬性限定超管、无权限点,「完全」档与「操作」档的差异只剩插件
|
||||
TierFull: {"system:view", "system:edit", "system:plugin"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -130,8 +131,8 @@ var dangerousPermissions = map[string]string{
|
||||
"billing:refund": "可对任意租户钱包退款/调账",
|
||||
"order:refund": "可对订单发起退款到支付渠道",
|
||||
"audit:read_sensitive": "可查看敏感数据访问日志",
|
||||
"audit:clear": "可硬删除全部拦截日志(不可恢复)",
|
||||
"system:edit": "可修改支付、汇率、存储、邮件等全站配置",
|
||||
"system:update": "可触发系统版本更新",
|
||||
"system:plugin": "可安装/启停插件(等同于部署代码)",
|
||||
"user:create": "可创建管理员账号",
|
||||
"user:edit": "可修改管理员账号与其角色(含提权)",
|
||||
|
||||
@@ -22,7 +22,8 @@ import (
|
||||
//
|
||||
// 注意这些只是初始值而非系统契约:用户可以任意修改,甚至删掉预置角色自建一套。
|
||||
var builtinRoleDefaults = map[string][]string{
|
||||
// 管理员:除 user:*(账号与角色)、system:update/plugin 外的全部权限。
|
||||
// 管理员:除 user:*(账号与角色)与 system:plugin 外的全部权限
|
||||
//(在线自更新为超管专属,不设权限点)。
|
||||
// 与运营的区别就是「能碰钱(退款)和系统配置」—— 去掉这两项,两个角色就没有分开的必要。
|
||||
"admin": {
|
||||
"dashboard:view",
|
||||
|
||||
@@ -159,9 +159,9 @@ func TestBuiltinRoleBoundaries(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 插件是代码执行面、更新是版本变更,仅限超管
|
||||
// 插件是代码执行面、敏感日志是隐私面,仅限超管(在线自更新无权限点,超管专属)
|
||||
for code := range builtinRoleDefaults {
|
||||
for _, perm := range []string{"system:plugin", "system:update", "audit:read_sensitive"} {
|
||||
for _, perm := range []string{"system:plugin", "audit:read_sensitive"} {
|
||||
if has(code, perm) {
|
||||
t.Errorf("预置角色 %s 不应拥有 %s(仅限超级管理员)", code, perm)
|
||||
}
|
||||
|
||||
@@ -255,6 +255,8 @@ func (s *sAdmin) LoginHistory(ctx context.Context, req *v1.AdminLoginHistoryReq)
|
||||
type adminUserBrief struct {
|
||||
Username string
|
||||
DisplayName string
|
||||
// Role 用于会话列表标记超管会话(前端隐藏撤销入口),其他场景可为空
|
||||
Role string
|
||||
}
|
||||
|
||||
func adminUserIdsByUsername(ctx context.Context, keyword string) ([]int64, error) {
|
||||
|
||||
@@ -203,6 +203,7 @@ var adminPermissionRules = []permissionRule{
|
||||
{method: "GET", prefix: "/api/admin/audit/forwarding-trace/", perm: "audit:view"},
|
||||
{method: "GET", path: "/api/admin/audit/sensitive-logs", perm: "audit:read_sensitive"},
|
||||
{method: "GET", path: "/api/admin/audit/content-filter-logs", perm: "audit:view"},
|
||||
{method: "DELETE", path: "/api/admin/audit/content-filter-logs/clear", perm: "audit:clear"},
|
||||
|
||||
// ── file 文件管理 ──
|
||||
{method: "GET", path: "/api/admin/files", perm: "file:view"},
|
||||
@@ -228,6 +229,7 @@ var adminPermissionRules = []permissionRule{
|
||||
{method: "PUT", prefix: "/api/admin/error-logs/", perm: "monitor:edit"},
|
||||
{method: "PUT", path: "/api/admin/error-logs/batch-resolve", perm: "monitor:edit"},
|
||||
{method: "DELETE", path: "/api/admin/error-logs/clear", perm: "monitor:edit"},
|
||||
{method: "DELETE", path: "/api/admin/monitor/channel-errors/clear", perm: "monitor:edit"},
|
||||
{method: "GET", path: "/api/admin/error-logs/stats", perm: "monitor:view"},
|
||||
{method: "GET", path: "/api/admin/cron-jobs", perm: "system:view"},
|
||||
{method: "POST", prefix: "/api/admin/cron-jobs/", suffix: "/trigger", perm: "system:edit"},
|
||||
@@ -243,8 +245,6 @@ var adminPermissionRules = []permissionRule{
|
||||
{method: "PUT", prefix: "/api/admin/payment-channels/", perm: "system:edit"},
|
||||
{method: "GET", path: "/api/admin/payment-settings", perm: "system:view"},
|
||||
{method: "PUT", path: "/api/admin/payment-settings", perm: "system:edit"},
|
||||
{method: "GET", prefix: "/api/admin/update/", perm: "system:update"},
|
||||
{method: "POST", prefix: "/api/admin/update/", perm: "system:update"},
|
||||
{method: "GET", path: "/api/admin/data-governance/settings", perm: "system:view"},
|
||||
{method: "PUT", path: "/api/admin/data-governance/settings", perm: "system:edit"},
|
||||
{method: "POST", prefix: "/api/admin/data-governance/", perm: "system:edit"},
|
||||
@@ -305,9 +305,11 @@ var adminPermissionRules = []permissionRule{
|
||||
{method: "POST", path: "/api/admin/auth/logout", perm: "self:access"},
|
||||
// 当前用户信息与有效权限:登录即可访问自己的资料,不需要任何业务权限点
|
||||
{method: "GET", path: "/api/admin/auth/me", perm: "self:access"},
|
||||
{method: "GET", path: "/api/admin/auth/sessions", perm: "self:access"},
|
||||
{method: "DELETE", prefix: "/api/admin/auth/sessions/user/", perm: "user:edit"},
|
||||
{method: "DELETE", prefix: "/api/admin/auth/sessions/", perm: "self:access"},
|
||||
// 会话管理列出并处置的是【全部管理员】的登录态,属于账号管理(user 域)的操作能力:
|
||||
// 按用户管理模块的操作档位(user:edit)授权,只读档位不可见。
|
||||
// 超管会话的额外保护在 logic 层(assertCanOperateSessions):仅账号本人可操作。
|
||||
{method: "GET", path: "/api/admin/auth/sessions", perm: "user:edit"},
|
||||
{method: "DELETE", prefix: "/api/admin/auth/sessions/", perm: "user:edit"},
|
||||
{method: "PUT", path: "/api/admin/auth/change-password", perm: "self:access"},
|
||||
{method: "POST", prefix: "/api/admin/security/2fa/", perm: "self:access"},
|
||||
{method: "GET", path: "/api/admin/security/login-history", perm: "audit:view"},
|
||||
@@ -356,6 +358,13 @@ var superAdminOnlyRules = []permissionRule{
|
||||
// 给账号分配角色 —— 这是最直接的提权入口
|
||||
{method: "GET", prefix: "/api/admin/users/", suffix: "/roles"},
|
||||
{method: "PUT", prefix: "/api/admin/users/", suffix: "/roles"},
|
||||
|
||||
// 在线自更新(版本状态 / 检查 / 执行 / 回滚):替换平台二进制并重启服务,
|
||||
// 等同于在服务器上执行代码,是比角色管理更根本的平台级元操作,不下放。
|
||||
// 版本检查(GET)看似只读,但前端每次刷新都会请求,若走权限点授权,
|
||||
// 未授权角色会持续收到 403 —— 硬性限定超管后非超管前端不再发起请求。
|
||||
{method: "GET", prefix: "/api/admin/update/"},
|
||||
{method: "POST", prefix: "/api/admin/update/"},
|
||||
}
|
||||
|
||||
// isSuperAdminOnly 判断路由是否仅限超级管理员。
|
||||
@@ -407,9 +416,9 @@ func AdminPermissionGuard(r *ghttp.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// 角色管理是权限体系的元操作,硬性限定超管,不参与权限点授权
|
||||
// 角色管理与在线自更新是平台级元操作,硬性限定超管,不参与权限点授权
|
||||
if isSuperAdminOnly(r.Method, r.URL.Path) {
|
||||
response.ErrorMsg(r, consts.CodeForbidden, "角色权限管理仅超级管理员可用")
|
||||
response.ErrorMsg(r, consts.CodeForbidden, "该操作仅超级管理员可用")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -170,6 +170,11 @@ func TestEveryAdminRouteHasPermissionRule(t *testing.T) {
|
||||
if isAdminPublicPath(path) {
|
||||
continue
|
||||
}
|
||||
// 超管专属路由(角色管理、在线自更新)由 superAdminOnlyRules 硬性拦截,
|
||||
// 先于权限规则判定,不走权限点授权,因此不要求出现在 adminPermissionRules 里。
|
||||
if isSuperAdminOnly(r.Method, path) {
|
||||
continue
|
||||
}
|
||||
if perm := matchPermission(r.Method, path); perm == "" {
|
||||
missing = append(missing, r.Method+" "+r.Path+" ("+r.Source+")")
|
||||
}
|
||||
|
||||
@@ -69,6 +69,11 @@ type (
|
||||
// ContentFilterLogList returns a paginated list of content filter interception logs.
|
||||
// 审计数据从审计库查询,关联信息从主库批量查询后在应用层合并。
|
||||
ContentFilterLogList(ctx context.Context, req *v1.ContentFilterLogListReq) (*v1.ContentFilterLogListRes, error)
|
||||
// ContentFilterLogClear 硬删除全部内容过滤拦截日志。
|
||||
// 拦截日志为追加型审计流水,堆积后用 TRUNCATE 秒级清空并立即归还磁盘空间
|
||||
// (DELETE 需等 VACUUM 回收)。TRUNCATE 不返回行数,故先取删除前条数作为反馈。
|
||||
// 表无数据库级外键,TRUNCATE 安全。
|
||||
ContentFilterLogClear(ctx context.Context, _ *v1.ContentFilterLogClearReq) (*v1.ContentFilterLogClearRes, error)
|
||||
// Login handles admin login.
|
||||
Login(ctx context.Context, req *v1.AdminLoginReq) (*v1.AdminLoginRes, error)
|
||||
// Logout handles admin logout.
|
||||
@@ -151,6 +156,11 @@ type (
|
||||
ChannelErrorTopChannels(ctx context.Context, req *v1.ChannelErrorTopChannelsReq) (*v1.ChannelErrorTopChannelsRes, error)
|
||||
// ChannelErrorCategories 错误分类选项
|
||||
ChannelErrorCategories(ctx context.Context, req *v1.ChannelErrorCategoriesReq) (*v1.ChannelErrorCategoriesRes, error)
|
||||
// ChannelErrorClear 硬删除全部渠道错误事件。
|
||||
// 错误风暴时事件表堆积很快,用 TRUNCATE 而非逐行 DELETE 秒级清空并立即归还磁盘空间。
|
||||
// TRUNCATE 不返回行数,故先取删除前条数作为反馈。表无数据库级外键,TRUNCATE 安全。
|
||||
// 注意异步写入器(DefaultChannelErrorWriter)清空后继续写入新事件,属预期行为。
|
||||
ChannelErrorClear(ctx context.Context, _ *v1.ChannelErrorClearReq) (*v1.ChannelErrorClearRes, error)
|
||||
// ChannelOAuthAuthURL 生成 OAuth 授权链接
|
||||
ChannelOAuthAuthURL(ctx context.Context, req *v1.ChannelOAuthAuthURLReq) (*v1.ChannelOAuthAuthURLRes, error)
|
||||
// ChannelOAuthExchange OAuth 授权码换取令牌
|
||||
|
||||
@@ -119,7 +119,7 @@ const adminRoutes: RouteRecordRaw[] = [
|
||||
path: 'sessions',
|
||||
name: 'AdminSessions',
|
||||
component: () => import('@/views/security/SessionsPage.vue'),
|
||||
meta: { title: '会话管理' },
|
||||
meta: { title: '会话管理', perm: 'user:edit' },
|
||||
},
|
||||
{
|
||||
path: 'permissions',
|
||||
|
||||
@@ -128,6 +128,8 @@ async function fetchUpdateStatus() {
|
||||
}
|
||||
|
||||
function openUpdateModal() {
|
||||
// 在线更新是超管专属能力(后端 superAdminOnlyRules 拦截),非超管不打开弹窗也不发请求
|
||||
if (!isSuperAdminUser.value) return
|
||||
updateModalVisible.value = true
|
||||
fetchUpdateStatus()
|
||||
checkUpdate() // 打开弹窗时刷新版本检查,确保当前版本/最新版本为最新
|
||||
@@ -356,7 +358,7 @@ const ALL_MENU_GROUPS = [
|
||||
items: [
|
||||
{ name: 'AdminLoginHistory', label: '登录历史', icon: IconClockCircle, perm: 'audit:view' },
|
||||
{ name: 'AdminTenantLoginHistory', label: '租户登录历史', icon: IconClockCircle, perm: 'audit:view' },
|
||||
{ name: 'AdminSessions', label: '会话管理', icon: IconClockCircle },
|
||||
{ name: 'AdminSessions', label: '会话管理', icon: IconClockCircle, perm: 'user:edit' },
|
||||
{ name: 'AdminAudit', label: '操作日志', icon: IconFile, perm: 'audit:view' },
|
||||
],
|
||||
},
|
||||
@@ -515,9 +517,12 @@ onMounted(() => {
|
||||
updateMobile()
|
||||
window.addEventListener('resize', updateMobile)
|
||||
|
||||
// Check for updates on mount
|
||||
checkUpdate()
|
||||
fetchUpdateStatus() // 取后端实时版本,用于准确的"是否可更新"判断
|
||||
// 在线更新是超管专属能力(后端 superAdminOnlyRules 拦截),
|
||||
// 非超管不发请求,避免每次刷新页面都弹「缺少权限」错误提示
|
||||
if (isSuperAdminUser.value) {
|
||||
checkUpdate()
|
||||
fetchUpdateStatus() // 取后端实时版本,用于准确的"是否可更新"判断
|
||||
}
|
||||
|
||||
fetchWorkbenchBadges()
|
||||
badgeTimer = setInterval(fetchWorkbenchBadges, 60000)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, h, computed } from 'vue'
|
||||
import { Tag, Button, Select, Input, DatePicker } from '@arco-design/web-vue'
|
||||
import { Message, Modal, Tag, Button, Select, Input, DatePicker } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
|
||||
const loading = ref(false)
|
||||
const data = ref<any[]>([])
|
||||
@@ -202,6 +203,28 @@ function handlePageSizeChange(pageSize: number) {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 清空全部渠道错误事件(硬删除):错误风暴时事件表堆积很快,需二次确认。
|
||||
function clearErrors() {
|
||||
Modal.confirm({
|
||||
title: '清空全部渠道错误事件',
|
||||
content: '将硬删除全部渠道错误事件记录(含全部统计依据的历史数据),此操作不可恢复,确定继续?',
|
||||
okText: '清空',
|
||||
okButtonProps: { status: 'danger' },
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await request.delete('/admin/monitor/channel-errors/clear')
|
||||
Message.success('渠道错误事件已清空')
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
fetchStats()
|
||||
} catch {
|
||||
// error auto-shown by Axios interceptor
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const statsCards = computed(() => {
|
||||
if (!stats.value) return []
|
||||
const rateLimitCount = stats.value.by_category?.find((c: any) => c.error_category === 'rate_limit')?.count || 0
|
||||
@@ -224,7 +247,13 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<div class="page-table">
|
||||
<PageHeader title="渠道错误监控" description="上游渠道错误事件记录与趋势分析" />
|
||||
<PageHeader title="渠道错误监控" description="上游渠道错误事件记录与趋势分析">
|
||||
<template #actions>
|
||||
<a-button v-if="hasPermission('monitor:edit')" status="danger" @click="clearErrors">
|
||||
清空日志
|
||||
</a-button>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="stats-row">
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, h } from 'vue'
|
||||
import { Tag } from '@arco-design/web-vue'
|
||||
import { Message, Modal, Tag } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
|
||||
const loading = ref(false)
|
||||
const data = ref<any[]>([])
|
||||
@@ -223,6 +224,27 @@ function handleReset() {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 清空全部拦截日志(硬删除):日志大量堆积时快速释放数据库空间,需二次确认。
|
||||
function clearLogs() {
|
||||
Modal.confirm({
|
||||
title: '清空全部拦截日志',
|
||||
content: '将硬删除全部内容过滤拦截日志记录,此操作不可恢复,确定继续?',
|
||||
okText: '清空',
|
||||
okButtonProps: { status: 'danger' },
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await request.delete('/admin/audit/content-filter-logs/clear')
|
||||
Message.success('拦截日志已清空')
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
} catch {
|
||||
// error auto-shown by Axios interceptor
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
@@ -230,7 +252,13 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<div class="page-table">
|
||||
<PageHeader title="过滤拦截日志" description="查看内容过滤拦截记录,分析敏感词触发趋势" />
|
||||
<PageHeader title="过滤拦截日志" description="查看内容过滤拦截记录,分析敏感词触发趋势">
|
||||
<template #actions>
|
||||
<a-button v-if="hasPermission('audit:clear')" status="danger" @click="clearLogs">
|
||||
清空日志
|
||||
</a-button>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<a-card :bordered="false" class="mb-4">
|
||||
<a-space wrap>
|
||||
|
||||
@@ -8,6 +8,9 @@ import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const loading = ref(false)
|
||||
const data = ref<any[]>([])
|
||||
@@ -71,6 +74,8 @@ const columns: TableColumnData[] = [
|
||||
fixed: 'right',
|
||||
render({ record }) {
|
||||
if (record.is_current) return null
|
||||
// 超管会话仅账号本人可撤销(后端 assertCanOperateSessions 同规则兜底),其他人只能查看
|
||||
if (record.role === 'super_admin' && record.user_id !== authStore.user?.id) return null
|
||||
return h(Popconfirm, {
|
||||
content: '确定撤销该会话?',
|
||||
onOk: () => revokeSession(record.id),
|
||||
|
||||
@@ -415,7 +415,9 @@ async function submitExport() {
|
||||
onMounted(() => {
|
||||
fetchStats()
|
||||
fetchList()
|
||||
fetchRetention()
|
||||
// 保留策略读的是系统设置接口(system:view/system:edit 权限域),
|
||||
// 页面本身只要求 file:view —— 无权限时不请求,避免进页面就弹 403
|
||||
if (hasPermission('system:edit')) fetchRetention()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -442,7 +444,7 @@ onMounted(() => {
|
||||
</template>
|
||||
</APopover>
|
||||
<AButton v-if="hasPermission('file:cleanup')" :loading="cleanupLoading" @click="manualCleanup">手动清理过期文件</AButton>
|
||||
<AButton type="primary" @click="openExportDialog">
|
||||
<AButton v-if="hasPermission('system:edit')" type="primary" @click="openExportDialog">
|
||||
<template #icon><IconExport /></template>
|
||||
触发数据导出
|
||||
</AButton>
|
||||
|
||||
Reference in new issue
Block a user