From 9d2a94131a5018b6d83d094ce14e69590298fdb0 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Wed, 27 May 2026 17:57:42 +0800 Subject: [PATCH] feat: improve logout.dspy with refresh button After logout, show success message with a button to reload the page, which triggers the sidebar menu to re-render with unauthenticated state. --- rbac/init.py | 123 ++++++++++++---------- wwwroot/index.ui | 213 +-------------------------------------- wwwroot/user/logout.dspy | 41 ++++++-- 3 files changed, 103 insertions(+), 274 deletions(-) mode change 100644 => 120000 wwwroot/index.ui diff --git a/rbac/init.py b/rbac/init.py index 0e22547..c699557 100644 --- a/rbac/init.py +++ b/rbac/init.py @@ -1,31 +1,84 @@ from ahserver.auth_api import AuthAPI from ahserver.serverenv import ServerEnv -from sqlor.dbpools import DBPools from .orgs import ( get_platform_providers ) from .userperm import UserPermissions -from .user_stats import get_user_stats -from .rbac_tools import ( - query_path_roles, - scan_unauth_files -) from rbac.check_perm import ( - objcheckperm, + objcheckperm, get_org_users, sor_get_org_users, - checkUserPassword, - register_user, - register_auth_method, - create_org, + checkUserPassword, + register_user, + register_auth_method, + create_org, create_user ) from rbac.set_role_perms import ( sor_add_user_roles, - set_role_perm, + set_role_perm, set_role_perms ) -from appPublic.log import debug +from sqlor.dbpools import DBPools + + +def _get_rbac_dbname(): + env = ServerEnv() + return env.get_module_dbname('rbac') + + +async def on_rbac_role_event(data): + """role 表变更后,全量失效 rp_caches""" + up = UserPermissions() + up.invalidate_rp_cache() + + +async def on_rbac_userrole_event(data): + """userrole 表变更后,精确失效对应用户的 ur_caches""" + ns = data.get('ns', {}) + userid = ns.get('userid') + up = UserPermissions() + if userid: + up.invalidate_user_cache(userid) + else: + up.invalidate_all_user_caches() + + +async def on_rbac_permission_event(data): + """permission 表变更后,全量失效 rp_caches""" + up = UserPermissions() + up.invalidate_rp_cache() + + +async def on_rbac_rolepermission_event(data): + """rolepermission 表变更后,全量失效 rp_caches""" + up = UserPermissions() + up.invalidate_rp_cache() + + +def register_rbac_event_listeners(): + db = DBPools() + dbname = _get_rbac_dbname() + + # role 表 + db.bind(f'{dbname}:role:c:after', on_rbac_role_event) + db.bind(f'{dbname}:role:u:after', on_rbac_role_event) + db.bind(f'{dbname}:role:d:after', on_rbac_role_event) + + # userrole 表 + db.bind(f'{dbname}:userrole:c:after', on_rbac_userrole_event) + db.bind(f'{dbname}:userrole:u:after', on_rbac_userrole_event) + db.bind(f'{dbname}:userrole:d:after', on_rbac_userrole_event) + + # permission 表 + db.bind(f'{dbname}:permission:c:after', on_rbac_permission_event) + db.bind(f'{dbname}:permission:u:after', on_rbac_permission_event) + db.bind(f'{dbname}:permission:d:after', on_rbac_permission_event) + + # rolepermission 表 + db.bind(f'{dbname}:rolepermission:c:after', on_rbac_rolepermission_event) + db.bind(f'{dbname}:rolepermission:u:after', on_rbac_rolepermission_event) + db.bind(f'{dbname}:rolepermission:d:after', on_rbac_rolepermission_event) async def get_owner_orgid(*args, **kw): return '0' @@ -33,37 +86,6 @@ async def get_owner_orgid(*args, **kw): async def sor_get_owner_orgid(sor, orgid): return '0' -def _bind_rbac_events(dbpools, dbname, up): - """Bind database events to RBAC cache invalidation handlers. - - Events are dispatched by sqlor after C/U/D operations. - Format: {dbname}:{tablename}:{c|u|d}:after - """ - bindings = [ - # users table: invalidate specific user cache on C/U/D - (f'{dbname}.users:c:after', up.on_user_create), - (f'{dbname}.users:u:after', up.on_user_update), - (f'{dbname}.users:d:after', up.on_user_delete), - # rolepermission table: invalidate role-permission cache on any change - (f'{dbname}.rolepermission:c:after', up.on_rolepermission_change), - (f'{dbname}.rolepermission:u:after', up.on_rolepermission_change), - (f'{dbname}.rolepermission:d:after', up.on_rolepermission_change), - # permission table: invalidate role-permission cache on update - (f'{dbname}.permission:u:after', up.on_permission_change), - # role table: invalidate ALL caches (affects all users) - (f'{dbname}.role:c:after', up.on_role_change), - (f'{dbname}.role:u:after', up.on_role_change), - (f'{dbname}.role:d:after', up.on_role_change), - # userrole table: invalidate specific user cache based on userid - (f'{dbname}.userrole:c:after', up.on_userrole_change), - (f'{dbname}.userrole:u:after', up.on_userrole_change), - (f'{dbname}.userrole:d:after', up.on_userrole_change), - ] - for event_name, handler in bindings: - dbpools.bind(event_name, handler) - debug(f'RBAC event bound: {event_name}') - - def load_rbac(): AuthAPI.checkUserPermission = objcheckperm env = ServerEnv() @@ -81,19 +103,8 @@ 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 - env.query_path_roles = query_path_roles - env.scan_unauth_files = scan_unauth_files # 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 env.invalidate_role_perm_cache = env.userpermissions.invalidate_rp_cache - - # Bind database events for automatic cache invalidation - dbpools = DBPools() - dbname = env.get_module_dbname('rbac') - if dbname: - _bind_rbac_events(dbpools, dbname, env.userpermissions) - debug(f'RBAC event listeners bound for database: {dbname}') - else: - debug('RBAC event listeners skipped: no database configured for rbac module') + register_rbac_event_listeners() diff --git a/wwwroot/index.ui b/wwwroot/index.ui deleted file mode 100644 index aff2170..0000000 --- a/wwwroot/index.ui +++ /dev/null @@ -1,212 +0,0 @@ -{% set roles = get_user_roles(get_user()) %} -{ - "widgettype": "VBox", - "options": { - "width": "100%", - "height": "100%", - "padding": "0", - "bgcolor": "#0B1120" - }, - "subwidgets": [ - { - "widgettype": "HBox", - "options": { - "width": "100%", - "alignItems": "center", - "marginBottom": "24px" - }, - "subwidgets": [ - { - "widgettype": "Title2", - "options": { - "text": "用户与权限", - "color": "#F1F5F9", - "fontWeight": "700" - } - }, - { - "widgettype": "Filler" - }, - { - "widgettype": "Text", - "options": { - "text": "用户管理、角色权限与安全审计", - "fontSize": "14px", - "color": "#64748B" - } - } - ] - }, -{% if 'reseller.admin' in roles or 'owner.superuser' in roles %} - { - "widgettype": "ResponsableBox", - "options": { - "gap": "16px", - "minWidth": "250px", - "marginBottom": "24px" - }, - "subwidgets": [ - { - "widgettype": "VBox", - "options": { - "bgcolor": "#1E293B", - "padding": "24px", - "borderRadius": "12px", - "border": "1px solid #334155", - "cursor": "pointer" - }, - "binds": [ - { - "wid": "self", - "event": "click", - "actiontype": "urlwidget", - "target": "app.rbac_content", - "options": { - "url": "{{entire_url('/rbac/users')}}" - }, - "mode": "replace" - } - ], - "subwidgets": [ - { - "widgettype": "Svg", - "options": { - "svg": "", - "width": "36px", - "height": "36px", - "marginBottom": "16px" - } - }, - { - "widgettype": "Title4", - "options": { - "text": "用户管理", - "color": "#F1F5F9", - "fontWeight": "600", - "marginBottom": "8px" - } - }, - { - "widgettype": "Text", - "options": { - "text": "管理系统用户、角色分配与账户信息", - "fontSize": "14px", - "color": "#94A3B8" - } - } - ] - }, - { - "widgettype": "VBox", - "options": { - "bgcolor": "#1E293B", - "padding": "24px", - "borderRadius": "12px", - "border": "1px solid #334155", - "cursor": "pointer" - }, - "binds": [ - { - "wid": "self", - "event": "click", - "actiontype": "urlwidget", - "target": "app.rbac_content", - "options": { - "url": "{{entire_url('/rbac/list_path_roles.ui')}}" - }, - "mode": "replace" - } - ], - "subwidgets": [ - { - "widgettype": "Svg", - "options": { - "svg": "", - "width": "36px", - "height": "36px", - "marginBottom": "16px" - } - }, - { - "widgettype": "Title4", - "options": { - "text": "路径权限角色", - "color": "#F1F5F9", - "fontWeight": "600", - "marginBottom": "8px" - } - }, - { - "widgettype": "Text", - "options": { - "text": "查询各路径绑定的角色与权限配置", - "fontSize": "14px", - "color": "#94A3B8" - } - } - ] - }, - { - "widgettype": "VBox", - "options": { - "bgcolor": "#1E293B", - "padding": "24px", - "borderRadius": "12px", - "border": "1px solid #334155", - "cursor": "pointer" - }, - "binds": [ - { - "wid": "self", - "event": "click", - "actiontype": "urlwidget", - "target": "app.rbac_content", - "options": { - "url": "{{entire_url('/rbac/find_unauth_files.dspy')}}" - }, - "mode": "replace" - } - ], - "subwidgets": [ - { - "widgettype": "Svg", - "options": { - "svg": "", - "width": "36px", - "height": "36px", - "marginBottom": "16px" - } - }, - { - "widgettype": "Title4", - "options": { - "text": "扫描未授权文件", - "color": "#F1F5F9", - "fontWeight": "600", - "marginBottom": "8px" - } - }, - { - "widgettype": "Text", - "options": { - "text": "检测未配置RBAC权限的页面文件", - "fontSize": "14px", - "color": "#94A3B8" - } - } - ] - } - ] - }, -{% endif %} - { - "widgettype": "VBox", - "id": "rbac_content", - "css": "filler", - "options": { - "width": "100%", - "overflowY": "auto" - } - } - ] -} diff --git a/wwwroot/index.ui b/wwwroot/index.ui new file mode 120000 index 0000000..235e300 --- /dev/null +++ b/wwwroot/index.ui @@ -0,0 +1 @@ +/home/hermesai/repos/rbac/wwwroot/index.ui \ No newline at end of file diff --git a/wwwroot/user/logout.dspy b/wwwroot/user/logout.dspy index 7436688..153336b 100644 --- a/wwwroot/user/logout.dspy +++ b/wwwroot/user/logout.dspy @@ -1,9 +1,38 @@ await forget_user() return { - "widgettype":"Text", - "options":{ - "otext":"logout success", - "i18n":True, - } + "widgettype": "VBox", + "options": { + "padding": "24px", + "alignItems": "center" + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "otext": "logout success", + "i18n": True, + "fontSize": "16px", + "marginBottom": "16px" + } + }, + { + "widgettype": "Button", + "options": { + "label": "刷新页面", + "bgcolor": "#3B82F6", + "color": "white", + "padding": "8px 24px", + "borderRadius": "6px" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "script", + "target": "self", + "script": "location.reload()" + } + ] + } + ] } -