diff --git a/rbac/init.py b/rbac/init.py index eb14b49..7fef733 100644 --- a/rbac/init.py +++ b/rbac/init.py @@ -5,6 +5,7 @@ from .orgs import ( get_platform_providers ) from .userperm import UserPermissions +from .user_stats import get_user_stats from rbac.check_perm import ( objcheckperm, get_org_users, @@ -76,6 +77,7 @@ def load_rbac(): env.sor_get_org_users = sor_get_org_users env.get_owner_orgid = get_owner_orgid env.sor_add_user_roles = sor_add_user_roles + env.get_user_stats = get_user_stats # Cache invalidation methods for use after role/permission changes env.invalidate_user_perm_cache = env.userpermissions.invalidate_user_cache env.invalidate_all_perm_caches = env.userpermissions.invalidate_all_user_caches diff --git a/rbac/user_stats.py b/rbac/user_stats.py new file mode 100644 index 0000000..b4fdb69 --- /dev/null +++ b/rbac/user_stats.py @@ -0,0 +1,59 @@ +from sqlor.dbpools import get_sor_context +from appPublic.timeUtils import curDateString, timestampstr +from datetime import datetime, timedelta +from appPublic.log import debug, exception + +async def get_user_stats(request): + """Get user statistics for the platform""" + env = request._run_ns + today = curDateString() + tomorrow = (datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d') + month_start = datetime.now().strftime('%Y-%m-01') + + stats = { + 'total_users': 0, + 'active_users_today': 0, + 'new_users_this_month': 0, + 'total_orgs': 0 + } + + async with get_sor_context(env, 'rbac') as sor: + # Total users + sql_users = """ + SELECT COUNT(*) as cnt FROM users + """ + recs = await sor.sqlExe(sql_users, {}) + if recs: + stats['total_users'] = int(recs[0].cnt or 0) + + # Active users today (users with llmusage records today) + sql_active = """ + SELECT COUNT(DISTINCT userid) as cnt FROM llmusage + WHERE use_date >= ${today}$ + AND use_date < ${tomorrow}$ + """ + recs = await sor.sqlExe(sql_active, { + 'today': today, + 'tomorrow': tomorrow + }) + if recs: + stats['active_users_today'] = int(recs[0].cnt or 0) + + # New users this month + sql_new = """ + SELECT COUNT(*) as cnt FROM users + WHERE created_date >= ${month_start}$ + """ + recs = await sor.sqlExe(sql_new, {'month_start': month_start}) + if recs: + stats['new_users_this_month'] = int(recs[0].cnt or 0) + + # Total organizations + sql_orgs = """ + SELECT COUNT(*) as cnt FROM organization + """ + recs = await sor.sqlExe(sql_orgs, {}) + if recs: + stats['total_orgs'] = int(recs[0].cnt or 0) + + return stats diff --git a/script/register_rbac_tools_perm.py b/script/register_rbac_tools_perm.py new file mode 100644 index 0000000..e978d50 --- /dev/null +++ b/script/register_rbac_tools_perm.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +""" +注册 RBAC 工具的权限到数据库。 +运行在 Sage Python 虚拟环境中。 + +用法: + ./py3/bin/python ../rbac/script/register_rbac_tools_perm.py + +或在 Sage 根目录执行: + cd ~/repos/sage && ./py3/bin/python ../rbac/script/register_rbac_tools_perm.py +""" +import os +import sys +import asyncio + +# 确保 Sage 路径在 sys.path 中 +sage_root = os.environ.get('SAGE_ROOT') +if sage_root and sage_root not in sys.path: + sys.path.insert(0, sage_root) + +from sqlor.dbpools import DBPools +from appPublic.jsonConfig import getConfig +from appPublic.uniqueID import getID + +# 需要注册的权限列表: (path, role) +permissions = [ + ('/rbac/list_path_roles.ui', 'owner.superuser'), + ('/rbac/list_path_roles.dspy', 'owner.superuser'), + ('/rbac/find_unauth_files.dspy', 'owner.superuser'), + ('/rbac/admin_menu.ui', 'owner.superuser'), +] + + +async def main(): + config = getConfig('.') + db = DBPools(config.databases) + registered = 0 + + async with db.sqlorContext('sage') as sor: + # 查找 superuser 角色 ID + role_recs = await sor.sqlExe( + "SELECT id FROM role WHERE orgtypeid='owner' AND name='superuser'", {} + ) + if not role_recs: + print("错误: 未找到 owner.superuser 角色") + sys.exit(1) + superuser_id = role_recs[0].id + print(f"superuser role_id: {superuser_id}") + + for path, role in permissions: + # 检查 permission 是否已存在 + existing_perm = await sor.sqlExe( + "SELECT id FROM permission WHERE path=${path}$", {'path': path} + ) + if existing_perm: + perm_id = existing_perm[0].id + print(f" permission 已存在: {path} (id={perm_id})") + else: + perm_id = getID() + await sor.C('permission', {'id': perm_id, 'path': path}) + print(f" + permission: {path}") + + # 检查 rolepermission 是否已存在 + existing_rp = await sor.sqlExe( + "SELECT id FROM rolepermission WHERE roleid=${roleid}$ AND permid=${permid}$", + {'roleid': superuser_id, 'permid': perm_id} + ) + if existing_rp: + print(f" rolepermission 已存在") + else: + await sor.C('rolepermission', { + 'id': getID(), + 'roleid': superuser_id, + 'permid': perm_id + }) + registered += 1 + print(f" + rolepermission: superuser -> {path}") + + print(f"\n共注册 {registered} 条新权限。") + if registered > 0: + print("请重启 Sage 以刷新权限缓存。") + else: + print("所有权限已存在,无需操作。") + + +if __name__ == '__main__': + asyncio.get_event_loop().run_until_complete(main()) diff --git a/wwwroot/find_unauth_files.dspy b/wwwroot/find_unauth_files.dspy index 003bca3..6cf69b4 100644 --- a/wwwroot/find_unauth_files.dspy +++ b/wwwroot/find_unauth_files.dspy @@ -2,25 +2,36 @@ import os wwwroot = params_kw.get('wwwroot', '').strip() -if not wwwroot: - wwwroot = None - -from appPublic.dictObject import DictObject # 定位 wwwroot if not wwwroot: - # 默认: 当前模块 wwwroot 的父目录 wwwroot - # rbac/wwwroot -> 找 sage/wwwroot - # 如果 sage_root 环境变量存在 sage_root = os.environ.get('SAGE_ROOT') if sage_root: wwwroot = os.path.join(sage_root, 'wwwroot') else: - # 尝试从当前文件路径推断: rbac/wwwroot/xxx.dspy -> sage/wwwroot + # 从当前文件路径向上找 wwwroot + # 生产: ~/token/sage/wwwroot/rbac/find_unauth_files.dspy + # 开发: ~/repos/sage/wwwroot/rbac/find_unauth_files.dspy (通过symlink) + # 或: ~/repos/rbac/wwwroot/find_unauth_files.dspy (rbac repo自身) this_file = os.path.abspath(__file__) - # 通常在 repos/sage/wwwroot/rbac/find_unauth_files.dspy - # wwwroot 是上一层 - wwwroot = os.path.dirname(os.path.dirname(this_file)) + # 如果路径中包含 /wwwroot/ 段,直接截断 + if '/wwwroot/' in this_file: + idx = this_file.index('/wwwroot/') + wwwroot = this_file[:idx + len('/wwwroot')] + else: + # 尝试找 wwwroot 子目录 + for level in range(5): + candidate = this_file + for _ in range(level): + candidate = os.path.dirname(candidate) + if not candidate or candidate == '/': + break + wr = os.path.join(candidate, 'wwwroot') + if os.path.isdir(wr): + wwwroot = wr + break + if not wwwroot: + return UiError(title='错误', message='无法自动定位 wwwroot 目录,请通过参数传入 wwwroot 路径,或设置 SAGE_ROOT 环境变量。') wwwroot = os.path.abspath(wwwroot) if not os.path.isdir(wwwroot): diff --git a/wwwroot/stat_active_users.ui b/wwwroot/stat_active_users.ui new file mode 100644 index 0000000..bf92739 --- /dev/null +++ b/wwwroot/stat_active_users.ui @@ -0,0 +1,53 @@ +{% set stats = get_user_stats(request) %} +{ + "widgettype": "VBox", + "options": { + "bgcolor": "#1E293B", + "padding": "20px", + "borderRadius": "12px", + "border": "1px solid #334155", + "flex": "1", + "minHeight": "110px" + }, + "subwidgets": [ + { + "widgettype": "HBox", + "options": { + "alignItems": "center", + "marginBottom": "12px" + }, + "subwidgets": [ + { + "widgettype": "Svg", + "options": { + "svg": "", + "width": "24px", + "height": "24px" + } + }, + { + "widgettype": "Filler" + } + ] + }, + { + "widgettype": "Text", + "options": { + "text": "{{stats.active_users_today}}", + "fontSize": "32px", + "fontWeight": "700", + "color": "#F1F5F9", + "lineHeight": "1.1" + } + }, + { + "widgettype": "Text", + "options": { + "text": "今日活跃用户", + "fontSize": "14px", + "color": "#94A3B8", + "marginTop": "4px" + } + } + ] +} diff --git a/wwwroot/stat_total_orgs.ui b/wwwroot/stat_total_orgs.ui new file mode 100644 index 0000000..109d514 --- /dev/null +++ b/wwwroot/stat_total_orgs.ui @@ -0,0 +1,53 @@ +{% set stats = get_user_stats(request) %} +{ + "widgettype": "VBox", + "options": { + "bgcolor": "#1E293B", + "padding": "20px", + "borderRadius": "12px", + "border": "1px solid #334155", + "flex": "1", + "minHeight": "110px" + }, + "subwidgets": [ + { + "widgettype": "HBox", + "options": { + "alignItems": "center", + "marginBottom": "12px" + }, + "subwidgets": [ + { + "widgettype": "Svg", + "options": { + "svg": "", + "width": "24px", + "height": "24px" + } + }, + { + "widgettype": "Filler" + } + ] + }, + { + "widgettype": "Text", + "options": { + "text": "{{stats.total_orgs}}", + "fontSize": "32px", + "fontWeight": "700", + "color": "#F1F5F9", + "lineHeight": "1.1" + } + }, + { + "widgettype": "Text", + "options": { + "text": "组织机构数", + "fontSize": "14px", + "color": "#94A3B8", + "marginTop": "4px" + } + } + ] +} diff --git a/wwwroot/stat_total_users.ui b/wwwroot/stat_total_users.ui new file mode 100644 index 0000000..4c139c5 --- /dev/null +++ b/wwwroot/stat_total_users.ui @@ -0,0 +1,53 @@ +{% set stats = get_user_stats(request) %} +{ + "widgettype": "VBox", + "options": { + "bgcolor": "#1E293B", + "padding": "20px", + "borderRadius": "12px", + "border": "1px solid #334155", + "flex": "1", + "minHeight": "110px" + }, + "subwidgets": [ + { + "widgettype": "HBox", + "options": { + "alignItems": "center", + "marginBottom": "12px" + }, + "subwidgets": [ + { + "widgettype": "Svg", + "options": { + "svg": "", + "width": "24px", + "height": "24px" + } + }, + { + "widgettype": "Filler" + } + ] + }, + { + "widgettype": "Text", + "options": { + "text": "{{stats.total_users}}", + "fontSize": "32px", + "fontWeight": "700", + "color": "#F1F5F9", + "lineHeight": "1.1" + } + }, + { + "widgettype": "Text", + "options": { + "text": "总用户数", + "fontSize": "14px", + "color": "#94A3B8", + "marginTop": "4px" + } + } + ] +}