Compare commits

..

No commits in common. "main" and "feat/dataviz-users" have entirely different histories.

31 changed files with 773 additions and 1973 deletions

View File

@ -1,133 +0,0 @@
用户管理: User Management
用户名: Username
密码: Password
确认密码: Confirm Password
姓名: Full Name
邮箱: Email
手机号: Phone Number
所属机构: Organization
角色: Role
状态: Status
启用: Enable
停用: Disable
新增用户: Add User
编辑用户: Edit User
删除用户: Delete User
重置密码: Reset Password
角色管理: Role Management
角色名称: Role Name
角色编码: Role Code
角色描述: Role Description
权限: Permission
新增角色: Add Role
编辑角色: Edit Role
删除角色: Delete Role
分配权限: Assign Permission
机构管理: Organization Management
机构名称: Organization Name
机构编码: Organization Code
机构类型: Organization Type
上级机构: Parent Organization
新增机构: Add Organization
编辑机构: Edit Organization
删除机构: Delete Organization
菜单管理: Menu Management
菜单名称: Menu Name
菜单路径: Menu Path
菜单图标: Menu Icon
菜单排序: Menu Sort Order
上级菜单: Parent Menu
新增菜单: Add Menu
编辑菜单: Edit Menu
删除菜单: Delete Menu
权限管理: Permission Management
权限名称: Permission Name
权限编码: Permission Code
权限类型: Permission Type
资源权限: Resource Permission
操作权限: Operation Permission
数据权限: Data Permission
登录日志: Login Log
登录时间: Login Time
登录IP: Login IP
登录状态: Login Status
成功: Success
失败: Failed
操作日志: Operation Log
操作类型: Operation Type
操作时间: Operation Time
操作人: Operator
操作结果: Operation Result
API密钥管理: API Key Management
密钥名称: Key Name
密钥值: Key Value
密钥状态: Key Status
新增密钥: Add Key
编辑密钥: Edit Key
删除密钥: Delete Key
在线用户: Online Users
会话ID: Session ID
最后活动时间: Last Activity Time
强制下线: Force Logout
安全设置: Security Settings
密码策略: Password Policy
密码长度: Password Length
密码复杂度: Password Complexity
密码有效期: Password Expiry
登录失败锁定: Login Failure Lock
最大失败次数: Max Failed Attempts
锁定时长: Lock Duration
双因素认证: Two-Factor Authentication
启用双因素: Enable 2FA
验证码: Verification Code
Token管理: Token Management
Token名称: Token Name
Token过期时间: Token Expiry Time
新增Token: Add Token
编辑Token: Edit Token
删除Token: Delete Token
刷新Token: Refresh Token
用户组: User Group
组名称: Group Name
组描述: Group Description
新增用户组: Add User Group
编辑用户组: Edit User Group
删除用户组: Delete User Group
成员管理: Member Management
添加成员: Add Member
移除成员: Remove Member
搜索: Search
名称或编码: Name or Code
操作: Action
描述: Description
类型: Type
名称: Name
编码: Code
备注: Remarks
创建时间: Created Time
更新时间: Updated Time
新增: Add
保存: Save
取消: Cancel
确认: Confirm
删除: Delete
编辑: Edit
查看: View
导出: Export
打印: Print
刷新: Refresh
返回: Back
提交: Submit
重置: Reset
Conform: Conform
Discard: Discard
Submit: Submit
Reset: Reset
Cancel: Cancel
全部: All
日期: Date
管理员: Administrator
普通用户: Normal User
只读用户: Read-Only User
访问控制: Access Control
最后登录: Last Login

View File

@ -1,133 +0,0 @@
用户管理: ユーザー管理
用户名: ユーザー名
密码: パスワード
确认密码: パスワード確認
姓名: 氏名
邮箱: メールアドレス
手机号: 電話番号
所属机构: 所属組織
角色: ロール
状态: ステータス
启用: 有効
停用: 無効
新增用户: ユーザー追加
编辑用户: ユーザー編集
删除用户: ユーザー削除
重置密码: パスワードリセット
角色管理: ロール管理
角色名称: ロール名
角色编码: ロールコード
角色描述: ロール説明
权限: 権限
新增角色: ロール追加
编辑角色: ロール編集
删除角色: ロール削除
分配权限: 権限割り当て
机构管理: 組織管理
机构名称: 組織名
机构编码: 組織コード
机构类型: 組織タイプ
上级机构: 上位組織
新增机构: 組織追加
编辑机构: 組織編集
删除机构: 組織削除
菜单管理: メニュー管理
菜单名称: メニュー名
菜单路径: メニューパス
菜单图标: メニューアイコン
菜单排序: メニュー並び順
上级菜单: 上位メニュー
新增菜单: メニュー追加
编辑菜单: メニュー編集
删除菜单: メニュー削除
权限管理: 権限管理
权限名称: 権限名
权限编码: 権限コード
权限类型: 権限タイプ
资源权限: リソース権限
操作权限: 操作権限
数据权限: データ権限
登录日志: ログインログ
登录时间: ログイン時間
登录IP: ログインIP
登录状态: ログインステータス
成功: 成功
失败: 失敗
操作日志: 操作ログ
操作类型: 操作タイプ
操作时间: 操作時間
操作人: 操作者
操作结果: 操作結果
API密钥管理: APIキー管理
密钥名称: キー名
密钥值: キー値
密钥状态: キーステータス
新增密钥: キー追加
编辑密钥: キー編集
删除密钥: キー削除
在线用户: オンラインユーザー
会话ID: セッションID
最后活动时间: 最終アクティビティ時間
强制下线: 強制ログアウト
安全设置: セキュリティ設定
密码策略: パスワードポリシー
密码长度: パスワード長さ
密码复杂度: パスワード複雑度
密码有效期: パスワード有効期限
登录失败锁定: ログイン失敗ロック
最大失败次数: 最大失敗回数
锁定时长: ロック期間
双因素认证: 二要素認証
启用双因素: 二要素認証を有効化
验证码: 認証コード
Token管理: トークン管理
Token名称: トークン名
Token过期时间: トークン有効期限
新增Token: トークン追加
编辑Token: トークン編集
删除Token: トークン削除
刷新Token: トークン更新
用户组: ユーザーグループ
组名称: グループ名
组描述: グループ説明
新增用户组: ユーザーグループ追加
编辑用户组: ユーザーグループ編集
删除用户组: ユーザーグループ削除
成员管理: メンバー管理
添加成员: メンバー追加
移除成员: メンバー削除
搜索: 検索
名称或编码: 名前またはコード
操作: 操作
描述: 説明
类型: タイプ
名称: 名前
编码: コード
备注: 備考
创建时间: 作成日時
更新时间: 更新日時
新增: 追加
保存: 保存
取消: キャンセル
确认: 確認
删除: 削除
编辑: 編集
查看: 表示
导出: エクスポート
打印: 印刷
刷新: 更新
返回: 戻る
提交: 送信
重置: リセット
Conform: 確認
Discard: 破棄
Submit: 送信
Reset: リセット
Cancel: キャンセル
全部: すべて
日期: 日付
管理员: 管理者
普通用户: 一般ユーザー
只读用户: 読み取り専用ユーザー
访问控制: アクセス制御
最后登录: 最終ログイン

View File

@ -1,133 +0,0 @@
用户管理: 사용자 관리
用户名: 사용자명
密码: 비밀번호
确认密码: 비밀번호 확인
姓名: 이름
邮箱: 이메일
手机号: 전화번호
所属机构: 소속 기관
角色: 역할
状态: 상태
启用: 활성화
停用: 비활성화
新增用户: 사용자 추가
编辑用户: 사용자 편집
删除用户: 사용자 삭제
重置密码: 비밀번호 재설정
角色管理: 역할 관리
角色名称: 역할명
角色编码: 역할 코드
角色描述: 역할 설명
权限: 권한
新增角色: 역할 추가
编辑角色: 역할 편집
删除角色: 역할 삭제
分配权限: 권한 할당
机构管理: 기관 관리
机构名称: 기관명
机构编码: 기관 코드
机构类型: 기관 유형
上级机构: 상위 기관
新增机构: 기관 추가
编辑机构: 기관 편집
删除机构: 기관 삭제
菜单管理: 메뉴 관리
菜单名称: 메뉴명
菜单路径: 메뉴 경로
菜单图标: 메뉴 아이콘
菜单排序: 메뉴 정렬
上级菜单: 상위 메뉴
新增菜单: 메뉴 추가
编辑菜单: 메뉴 편집
删除菜单: 메뉴 삭제
权限管理: 권한 관리
权限名称: 권한명
权限编码: 권한 코드
权限类型: 권한 유형
资源权限: 리소스 권한
操作权限: 작업 권한
数据权限: 데이터 권한
登录日志: 로그인 로그
登录时间: 로그인 시간
登录IP: 로그인 IP
登录状态: 로그인 상태
成功: 성공
失败: 실패
操作日志: 작업 로그
操作类型: 작업 유형
操作时间: 작업 시간
操作人: 작업자
操作结果: 작업 결과
API密钥管理: API 키 관리
密钥名称: 키 이름
密钥值: 키 값
密钥状态: 키 상태
新增密钥: 키 추가
编辑密钥: 키 편집
删除密钥: 키 삭제
在线用户: 온라인 사용자
会话ID: 세션 ID
最后活动时间: 마지막 활동 시간
强制下线: 강제 로그아웃
安全设置: 보안 설정
密码策略: 비밀번호 정책
密码长度: 비밀번호 길이
密码复杂度: 비밀번호 복잡도
密码有效期: 비밀번호 유효기간
登录失败锁定: 로그인 실패 잠금
最大失败次数: 최대 실패 횟수
锁定时长: 잠금 시간
双因素认证: 이중 인증
启用双因素: 이중 인증 활성화
验证码: 인증 코드
Token管理: 토큰 관리
Token名称: 토큰 이름
Token过期时间: 토큰 만료 시간
新增Token: 토큰 추가
编辑Token: 토큰 편집
删除Token: 토큰 삭제
刷新Token: 토큰 갱신
用户组: 사용자 그룹
组名称: 그룹 이름
组描述: 그룹 설명
新增用户组: 사용자 그룹 추가
编辑用户组: 사용자 그룹 편집
删除用户组: 사용자 그룹 삭제
成员管理: 멤버 관리
添加成员: 멤버 추가
移除成员: 멤버 제거
搜索: 검색
名称或编码: 이름 또는 코드
操作: 작업
描述: 설명
类型: 유형
名称: 이름
编码: 코드
备注: 비고
创建时间: 생성 시간
更新时间: 업데이트 시간
新增: 추가
保存: 저장
取消: 취소
确认: 확인
删除: 삭제
编辑: 편집
查看: 보기
导出: 내보내기
打印: 인쇄
刷新: 새로고침
返回: 뒤로
提交: 제출
重置: 초기화
Conform: 확인
Discard: 폐기
Submit: 제출
Reset: 초기화
Cancel: 취소
全部: 전체
日期: 날짜
管理员: 관리자
普通用户: 일반 사용자
只读用户: 읽기 전용 사용자
访问控制: 접근 제어
最后登录: 마지막 로그인

View File

@ -1,139 +0,0 @@
Account Locked: Account Locked
Account locked due to too many failed login attempts. Please try again in 5 minutes.: Account locked due to too many failed login attempts. Please try again in 5 minutes.
Add Error: Add Error
Add Success: Add Success
Add reseller Error: Add reseller Error
Cancel: Cancel
Conform: Conform
Discard: Discard
Error: Error
Login: Login
Login Error: Login Error
Reset: Reset
Reset Password: Reset Password
Submit: Submit
Success: Success
apikey: apikey
dappid参数必填: dappid参数必填
failed: failed
id: id
id/orgid必填: id/orgid必填
logout success: logout success
no user selected: no user selected
ok: ok
system error: system error
user disabled: user disabled
user enabled: user enabled
user name or password error: user name or password error
user register: user register
user.id和user.orgid必填: user.id和user.orgid必填
users必填: users必填
you are not owner user: you are not owner user
 角色:  角色
两次输入的密码不一致: 两次输入的密码不一致
个人信息已更新: 个人信息已更新
主营业务描述: 主营业务描述
保存成功: 保存成功
允许IP集: 允许IP集
刷新页面: 刷新页面
发送验证码: 发送验证码
发送验证码出错请检查短信模板配置和百度API连接: 发送验证码出错请检查短信模板配置和百度API连接
同步应用id: 同步应用id
名称: 名称
启用日期: 启用日期
图标: 图标
地址: 地址
增加管理员: 增加管理员
失效日期: 失效日期
完善个人信息: 完善个人信息
完善信息: 完善信息
审计日志: 审计日志
密码: 密码
密码登录: 密码登录
应用名称: 应用名称
我的角色: 我的角色
所在地区id: 所在地区id
所在城市id: 所在城市id
所在省id: 所在省id
所属机构: 所属机构
手机: 手机
手机号: 手机号
手机号需短信验证后方可注册: 手机号需短信验证后方可注册
手机登录: 手机登录
手机短信验证码出错: 手机短信验证码出错
扫描未授权文件: 扫描未授权文件
扫码: 扫码
描述: 描述
是否审计: 是否审计
显示名: 显示名
最后登录: 最后登录
最后登录失败时间: 最后登录失败时间
未注册的手机号将自动创建账号: 未注册的手机号将自动创建账号
机构: 机构
机构id: 机构id
机构别名: 机构别名
机构名称: 机构名称
机构拥有角色: 机构拥有角色
机构简称: 机构简称
机构类型: 机构类型
机构编码: 机构编码
权限: 权限
权限id: 权限id
查询路径权限角色: 查询路径权限角色
欢迎登录: 欢迎登录
没有收到手机号: 没有收到手机号
注册失败: 注册失败
注册成功: 注册成功
注册日期: 注册日期
注册账号: 注册账号
父机构id: 父机构id
父权限id: 父权限id
用户: 用户
用户id: 用户id
用户名: 用户名
用户名已被占用: 用户名已被占用
用户应用: 用户应用
用户状态: 用户状态
用户管理: 用户管理
用户角色: 用户角色
用户部门表: 用户部门表
登录成功: 登录成功
短信码已生成: 短信码已生成
短信验证码: 短信验证码
短信验证码错误或已过期,请重新获取: 短信验证码错误或已过期,请重新获取
确认密码: 确认密码
签退: 签退
类型: 类型
系统错误: 系统错误
系统错误,请稍后重试: 系统错误,请稍后重试
组织结构代码: 组织结构代码
网站域名: 网站域名
联系人: 联系人
联系人电话: 联系人电话
营业执照: 营业执照
角色id: 角色id
角色名称: 角色名称
角色权限表: 角色权限表
该手机号已注册,请直接登录: 该手机号已注册,请直接登录
请先发送并输入短信验证码: 请先发送并输入短信验证码
请输入密码: 请输入密码
请输入手机号: 请输入手机号
请输入用户名: 请输入用户名
调用参数: 调用参数
调用日期: 调用日期
调用时间戳: 调用时间戳
账号: 账号
路径: 路径
身份证: 身份证
远程IP: 远程IP
连续失败次数: 连续失败次数
邮件地址: 邮件地址
邮箱: 邮箱
部门id: 部门id
重置密码: 重置密码
错误: 错误
需要短信验证key: 需要短信验证key
需输入手机号: 需输入手机号
需输入验证码: 需输入验证码
验证失败: 验证失败
验证码: 验证码

View File

@ -11,15 +11,11 @@
"alters":{} "alters":{}
}, },
"edit_exclouded_fields":[], "edit_exclouded_fields":[],
"parentField":"parentid" "parentField":"parentid",
}, "toolbar":{
"subtables":[ },
{ "binds":[
"field":"permid", ]
"title":"权限角色", }
"subtable":"rolepermission"
}
]
} }

View File

@ -9,34 +9,19 @@
"exclouded": ["id", "password", "orgid", "nick_name" ], "exclouded": ["id", "password", "orgid", "nick_name" ],
"cwidth": {} "cwidth": {}
}, },
"editexclouded": ["id", "nick_name", "orgid", "last_login_fail", "last_login", "sync_from", "login_fail_count", "created_at"], "editexclouded": [
"record_toolbar": [ "id", "nick_name", "orgid"
{
"label": "启用",
"actiontype": "dspy",
"url": "/rbac/users/enable_user.dspy",
"options": {
"icon": "check",
"cwidth": 16,
"cheight": 9
}
},
{
"label": "禁用",
"actiontype": "dspy",
"url": "/rbac/users/disable_user.dspy",
"options": {
"icon": "block",
"cwidth": 16,
"cheight": 9
}
}
], ],
"subtables": [ "subtables": [
{ {
"field":"userid", "field":"userid",
"title":"用户角色", "title":"用户角色",
"subtable":"userrole" "subtable":"userrole"
},
{
"field":"userid",
"title":"APIKEY",
"subtable":"userapp"
} }
] ]
} }

View File

@ -67,7 +67,7 @@
{ {
"name": "created_at", "name": "created_at",
"title": "注册日期", "title": "注册日期",
"type": "date" "type": "timestamp"
}, },
{ {
"name": "last_login", "name": "last_login",

View File

@ -119,13 +119,7 @@ async def register_user(sor, ns):
ns.login_fail_count = 0 ns.login_fail_count = 0
ns1 = DictObject(id=id, orgname=ns.username) ns1 = DictObject(id=id, orgname=ns.username)
await create_org(sor, ns1) await create_org(sor, ns1)
roles = [ await create_user(sor, ns)
{
'orgtypeid': 'customer',
'roles': ['customer', 'admin']
}
]
await create_user(sor, ns, roles)
return { return {
"status": "ok", "status": "ok",
"data": { "data": {
@ -158,11 +152,6 @@ async def checkUserPassword(request, username, password):
return False return False
user = recs[0] user = recs[0]
# Check user status (disabled)
user_status = getattr(user, 'user_status', '0') or '0'
if user_status != '0':
debug(f'User {username} is disabled (status={user_status})')
return False
fail_count = getattr(user, 'login_fail_count', 0) or 0 fail_count = getattr(user, 'login_fail_count', 0) or 0
last_fail = getattr(user, 'last_login_fail', None) last_fail = getattr(user, 'last_login_fail', None)
@ -214,11 +203,6 @@ async def basic_auth(sor, request):
return None return None
# Check lockout in Python layer (DB-agnostic) # Check lockout in Python layer (DB-agnostic)
user = recs[0] user = recs[0]
# Check user status (disabled)
user_status = getattr(user, 'user_status', '0') or '0'
if user_status != '0':
debug(f'User {username} is disabled (status={user_status}) via basic auth')
return None
fail_count = getattr(user, 'login_fail_count', 0) or 0 fail_count = getattr(user, 'login_fail_count', 0) or 0
last_fail = getattr(user, 'last_login_fail', None) last_fail = getattr(user, 'last_login_fail', None)
if _is_locked(fail_count, last_fail): if _is_locked(fail_count, last_fail):

View File

@ -1,9 +1,15 @@
from ahserver.auth_api import AuthAPI from ahserver.auth_api import AuthAPI
from ahserver.serverenv import ServerEnv from ahserver.serverenv import ServerEnv
from sqlor.dbpools import DBPools
from .orgs import ( from .orgs import (
get_platform_providers get_platform_providers
) )
from .userperm import UserPermissions 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 ( from rbac.check_perm import (
objcheckperm, objcheckperm,
get_org_users, get_org_users,
@ -19,66 +25,8 @@ from rbac.set_role_perms import (
set_role_perm, set_role_perm,
set_role_perms set_role_perms
) )
from sqlor.dbpools import DBPools from appPublic.log import debug
from ahserver.cache_sync import get_cache_sync
def _get_rbac_dbname():
env = ServerEnv()
return env.get_module_dbname('rbac')
async def on_rbac_role_event(data):
"""role 表变更后,全量失效 rp_caches"""
up = ServerEnv().userpermissions
up.invalidate_rp_cache()
async def on_rbac_userrole_event(data):
"""userrole 表变更后,精确失效对应用户的 ur_caches"""
ns = data.get('ns', {})
userid = ns.get('userid')
up = ServerEnv().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 = ServerEnv().userpermissions
up.invalidate_rp_cache()
async def on_rbac_rolepermission_event(data):
"""rolepermission 表变更后,全量失效 rp_caches"""
up = ServerEnv().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): async def get_owner_orgid(*args, **kw):
return '0' return '0'
@ -86,6 +34,58 @@ async def get_owner_orgid(*args, **kw):
async def sor_get_owner_orgid(sor, orgid): async def sor_get_owner_orgid(sor, orgid):
return '0' 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}')
async def start_cache_sync():
"""Start cache_sync and register RBAC reload callbacks."""
env = ServerEnv()
cache_sync = get_cache_sync()
# Get Redis URL from session config
try:
redis_url = env.conf.website.session_redis.url
except AttributeError:
redis_url = "redis://127.0.0.1:6379"
await cache_sync.start(redis_url)
debug(f'RBAC cache_sync started with Redis URL: {redis_url}')
# Register callbacks for cache invalidation messages from other processes
up = env.userpermissions
cache_sync.register('rbac:rp', up.invalidate_rp_cache)
cache_sync.register('rbac:ur:all', up.invalidate_all_user_caches)
# Note: rbac:ur:{userid} callbacks are handled by the invalidate_user_cache method itself
def load_rbac(): def load_rbac():
AuthAPI.checkUserPermission = objcheckperm AuthAPI.checkUserPermission = objcheckperm
env = ServerEnv() env = ServerEnv()
@ -103,11 +103,19 @@ def load_rbac():
env.sor_get_org_users = sor_get_org_users env.sor_get_org_users = sor_get_org_users
env.get_owner_orgid = get_owner_orgid env.get_owner_orgid = get_owner_orgid
env.sor_add_user_roles = sor_add_user_roles 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 # Cache invalidation methods for use after role/permission changes
env.invalidate_user_perm_cache = env.userpermissions.invalidate_user_cache env.invalidate_user_perm_cache = env.userpermissions.invalidate_user_cache
env.invalidate_all_perm_caches = env.userpermissions.invalidate_all_user_caches env.invalidate_all_perm_caches = env.userpermissions.invalidate_all_user_caches
env.invalidate_role_perm_cache = env.userpermissions.invalidate_rp_cache env.invalidate_role_perm_cache = env.userpermissions.invalidate_rp_cache
# Bind hot_reload event — instance method, WeakMethod safe (stored on env)
if hasattr(env, 'event_dispatcher'): # Bind database events for automatic cache invalidation
env.event_dispatcher.bind('hot_reload', env.userpermissions.on_hot_reload) dbpools = DBPools()
register_rbac_event_listeners() 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')

View File

@ -114,6 +114,31 @@ async def set_role_perms(dbname, module, orgtype, role, items):
for tblname in items: for tblname in items:
await set_role_perm(dbname, module, orgtype, role, tblname) await set_role_perm(dbname, module, orgtype, role, tblname)
async def send_rbac_invalidation():
"""Send cache invalidation message to all processes via Redis Pub/Sub."""
try:
from ahserver.cache_sync import get_cache_sync
cache_sync = get_cache_sync()
# Use default Redis URL for CLI scripts
try:
from ahserver.serverenv import ServerEnv
env = ServerEnv()
redis_url = env.conf.website.session_redis.url
except (AttributeError, Exception):
redis_url = "redis://127.0.0.1:6379"
await cache_sync.start(redis_url)
# Invalidate both role-permission and all user caches
# (CLI scripts typically change permissions/roles)
await cache_sync.invalidate('rbac:rp')
await cache_sync.invalidate('rbac:ur:all')
debug('RBAC CLI: sent cache invalidation messages')
# Give a moment for the message to be published
await asyncio.sleep(0.1)
await cache_sync.stop()
except Exception as e:
print(f'Warning: Failed to send cache invalidation: {e}')
if __name__ == '__main__': if __name__ == '__main__':
async def main(): async def main():
if len(sys.argv) < 6: if len(sys.argv) < 6:
@ -124,6 +149,8 @@ if __name__ == '__main__':
orgtype = sys.argv[3] orgtype = sys.argv[3]
role = sys.argv[4] role = sys.argv[4]
await set_role_perms(dbname, module, orgtype, role, sys.argv[5:]) await set_role_perms(dbname, module, orgtype, role, sys.argv[5:])
# Send invalidation message to all running Sage processes
await send_rbac_invalidation()
def run(coro): def run(coro):
p = '.' p = '.'

View File

@ -4,20 +4,9 @@ from sqlor.dbpools import get_sor_context
from ahserver.serverenv import ServerEnv from ahserver.serverenv import ServerEnv
from appPublic.Singleton import SingletonDecorator from appPublic.Singleton import SingletonDecorator
from appPublic.log import debug, error from appPublic.log import debug, error
from appPublic.jsonConfig import getConfig from ahserver.cache_sync import get_cache_sync
def _cache_enabled(module_name='rbac'):
"""Check if cache is enabled for the given module in config.json"""
try:
config = getConfig()
module_cache = config.module_cache
if module_cache is None:
return True # Default to enabled if not configured
return getattr(module_cache, module_name, True)
except Exception:
return True # Default to enabled on error
class LRUCache: class LRUCache:
"""Async-safe LRU cache with TTL support. """Async-safe LRU cache with TTL support.
@ -38,8 +27,6 @@ class LRUCache:
def get(self, key): def get(self, key):
import time import time
if not _cache_enabled('rbac'):
return None
if key not in self._cache: if key not in self._cache:
return None return None
value, expire_at = self._cache[key] value, expire_at = self._cache[key]
@ -51,8 +38,6 @@ class LRUCache:
def set(self, key, value): def set(self, key, value):
import time import time
if not _cache_enabled('rbac'):
return
if key in self._cache: if key in self._cache:
self._cache.move_to_end(key) self._cache.move_to_end(key)
self._cache[key] = (value, time.time() + self.ttl) self._cache[key] = (value, time.time() + self.ttl)
@ -98,89 +83,82 @@ class UserPermissions:
# Async lock for rp_caches initialization (lazy init) # Async lock for rp_caches initialization (lazy init)
self._rp_lock = None self._rp_lock = None
def on_hot_reload(self, data=None): async def on_user_update(self, data):
"""Event handler for hot_reload event. Clears all caches."""
from appPublic.log import debug
debug(f'[rbac] on_hot_reload called, clearing caches (data={data})')
self.ur_caches.clear()
self.invalidate_rp_cache()
def on_user_update(self, data):
"""Event handler for users table update. """Event handler for users table update.
Clears the specific user's permission cache. Clears the specific user's permission cache.
""" """
try: try:
userid = getattr(data, 'id', None) userid = getattr(data, 'id', None)
if userid: if userid:
self.invalidate_user_cache(userid) await self.invalidate_user_cache(userid)
debug(f'RBAC cache invalidated for user id={userid} (users update)') debug(f'RBAC cache invalidated for user id={userid} (users update)')
except Exception as e: except Exception as e:
error(f'RBAC on_user_update handler error: {e}') error(f'RBAC on_user_update handler error: {e}')
def on_user_create(self, data): async def on_user_create(self, data):
"""Event handler for users table insert. """Event handler for users table insert.
Clears the specific user's permission cache. Clears the specific user's permission cache.
""" """
try: try:
userid = getattr(data, 'id', None) userid = getattr(data, 'id', None)
if userid: if userid:
self.invalidate_user_cache(userid) await self.invalidate_user_cache(userid)
debug(f'RBAC cache invalidated for user id={userid} (users create)') debug(f'RBAC cache invalidated for user id={userid} (users create)')
except Exception as e: except Exception as e:
error(f'RBAC on_user_create handler error: {e}') error(f'RBAC on_user_create handler error: {e}')
def on_user_delete(self, data): async def on_user_delete(self, data):
"""Event handler for users table delete. """Event handler for users table delete.
Clears the specific user's permission cache. Clears the specific user's permission cache.
""" """
try: try:
userid = getattr(data, 'id', None) userid = getattr(data, 'id', None)
if userid: if userid:
self.invalidate_user_cache(userid) await self.invalidate_user_cache(userid)
debug(f'RBAC cache invalidated for user id={userid} (users delete)') debug(f'RBAC cache invalidated for user id={userid} (users delete)')
except Exception as e: except Exception as e:
error(f'RBAC on_user_delete handler error: {e}') error(f'RBAC on_user_delete handler error: {e}')
def on_rolepermission_change(self, data): async def on_rolepermission_change(self, data):
"""Event handler for rolepermission table C/U/D. """Event handler for rolepermission table C/U/D.
Clears the role-permission cache. Clears the role-permission cache.
""" """
try: try:
self.invalidate_rp_cache() await self.invalidate_rp_cache()
debug('RBAC role-permission cache invalidated (rolepermission change)') debug('RBAC role-permission cache invalidated (rolepermission change)')
except Exception as e: except Exception as e:
error(f'RBAC on_rolepermission_change handler error: {e}') error(f'RBAC on_rolepermission_change handler error: {e}')
def on_permission_change(self, data): async def on_permission_change(self, data):
"""Event handler for permission table update. """Event handler for permission table update.
Clears the role-permission cache. Clears the role-permission cache.
""" """
try: try:
self.invalidate_rp_cache() await self.invalidate_rp_cache()
debug('RBAC role-permission cache invalidated (permission change)') debug('RBAC role-permission cache invalidated (permission change)')
except Exception as e: except Exception as e:
error(f'RBAC on_permission_change handler error: {e}') error(f'RBAC on_permission_change handler error: {e}')
def on_role_change(self, data): async def on_role_change(self, data):
"""Event handler for role table C/U/D. """Event handler for role table C/U/D.
Clears all user caches and role-permission cache, Clears all user caches and role-permission cache,
since role changes may affect any user. since role changes may affect any user.
""" """
try: try:
self.invalidate_all_user_caches() await self.invalidate_all_user_caches()
self.invalidate_rp_cache() await self.invalidate_rp_cache()
debug('RBAC all caches invalidated (role change)') debug('RBAC all caches invalidated (role change)')
except Exception as e: except Exception as e:
error(f'RBAC on_role_change handler error: {e}') error(f'RBAC on_role_change handler error: {e}')
def on_userrole_change(self, data): async def on_userrole_change(self, data):
"""Event handler for userrole table C/U/D. """Event handler for userrole table C/U/D.
Clears the specific user's permission cache based on userid. Clears the specific user's permission cache based on userid.
""" """
try: try:
userid = getattr(data, 'userid', None) userid = getattr(data, 'userid', None)
if userid: if userid:
self.invalidate_user_cache(userid) await self.invalidate_user_cache(userid)
debug(f'RBAC cache invalidated for user id={userid} (userrole change)') debug(f'RBAC cache invalidated for user id={userid} (userrole change)')
except Exception as e: except Exception as e:
error(f'RBAC on_userrole_change handler error: {e}') error(f'RBAC on_userrole_change handler error: {e}')
@ -200,28 +178,41 @@ class UserPermissions:
return roles return roles
async with get_sor_context(ServerEnv(), 'rbac') as sor: async with get_sor_context(ServerEnv(), 'rbac') as sor:
roles = await self.get_userroles(sor, userid) await self.get_userroles(sor, userid)
# When cache is enabled, get_userroles stored it and we can read back;
# when cache is disabled, get_userroles returns the list directly.
if roles is not None:
return roles
return self.ur_caches.get(userid) return self.ur_caches.get(userid)
return ['any', 'logined'] return None
def invalidate_user_cache(self, userid): async def invalidate_user_cache(self, userid):
"""Invalidate cache for a specific user. """Invalidate cache for a specific user.
Call this after role changes, user creation, etc. Call this after role changes, user creation, etc.
Also broadcasts invalidation to all other processes via Redis Pub/Sub.
""" """
self.ur_caches.invalidate(userid) self.ur_caches.invalidate(userid)
# Broadcast to other processes
cache_sync = get_cache_sync()
if cache_sync.is_running:
await cache_sync.invalidate(f'rbac:ur:{userid}')
def invalidate_all_user_caches(self): async def invalidate_all_user_caches(self):
"""Invalidate all user role caches.""" """Invalidate all user role caches.
Also broadcasts invalidation to all other processes via Redis Pub/Sub.
"""
self.ur_caches.clear() self.ur_caches.clear()
# Broadcast to other processes
cache_sync = get_cache_sync()
if cache_sync.is_running:
await cache_sync.invalidate('rbac:ur:all')
def invalidate_rp_cache(self): async def invalidate_rp_cache(self):
"""Invalidate role-permission cache (after permission changes).""" """Invalidate role-permission cache (after permission changes).
Also broadcasts invalidation to all other processes via Redis Pub/Sub.
"""
self.rp_caches = None self.rp_caches = None
self.rp_cache_loaded_at = 0 self.rp_cache_loaded_at = 0
# Broadcast to other processes
cache_sync = get_cache_sync()
if cache_sync.is_running:
await cache_sync.invalidate('rbac:rp')
async def load_roleperms(self, sor): async def load_roleperms(self, sor):
"""Load all role-permission mappings into cache. """Load all role-permission mappings into cache.
@ -235,28 +226,22 @@ class UserPermissions:
now = time.time() now = time.time()
# Fast path: cache valid, no lock needed # Fast path: cache valid, no lock needed
if _cache_enabled('rbac') and self.rp_caches is not None and (now - self.rp_cache_loaded_at) < self.rp_cache_ttl: if self.rp_caches is not None and (now - self.rp_cache_loaded_at) < self.rp_cache_ttl:
return return
# Slow path: acquire lock and double-check # Slow path: acquire lock and double-check
async with self._get_rp_lock(): async with self._get_rp_lock():
# Double-check after lock acquisition # Double-check after lock acquisition
if _cache_enabled('rbac') and self.rp_caches is not None and (now - self.rp_cache_loaded_at) < self.rp_cache_ttl: if self.rp_caches is not None and (now - self.rp_cache_loaded_at) < self.rp_cache_ttl:
return return
# Build in local dict first, assign atomically when complete. self.rp_caches = {}
# Otherwise other coroutines see {} during the await and get 403.
new_caches = {}
sql_all = """select c.id, c.orgtypeid, c.name, b.path sql_all = """select c.id, c.orgtypeid, c.name, b.path
from rolepermission a, permission b, role c from rolepermission a, permission b, role c
where a.permid = b.id where a.permid = b.id
and c.id = a.roleid and c.id = a.roleid
order by c.orgtypeid, c.name""" order by c.orgtypeid, c.name"""
recs = await sor.sqlExe(sql_all, {}) recs = await sor.sqlExe(sql_all, {})
if len(recs) == 0 and self.rp_caches:
# DB returned empty — likely a bad connection. Keep previous valid cache.
debug(f'load_roleperms: got 0 records, keeping previous cache ({sum(len(v) for v in self.rp_caches.values())} paths)')
return
for r in recs: for r in recs:
if r.id == 'anonymous': if r.id == 'anonymous':
k = 'anonymous' k = 'anonymous'
@ -266,17 +251,13 @@ order by c.orgtypeid, c.name"""
k = 'logined' k = 'logined'
else: else:
k = f'{r.orgtypeid}.{r.name}' k = f'{r.orgtypeid}.{r.name}'
arr = new_caches.get(k, []) arr = self.rp_caches.get(k, [])
arr.append(r.path) arr.append(r.path)
new_caches[k] = arr self.rp_caches[k] = arr
# Atomic swap: other coroutines see old cache or fully-loaded new cache, never {}
self.rp_caches = new_caches
self.rp_cache_loaded_at = now self.rp_cache_loaded_at = now
async def get_userroles(self, sor, userid): async def get_userroles(self, sor, userid):
"""Load user roles from database and cache them. """Load user roles from database and cache them."""
Returns the roles list directly (needed when cache is disabled).
"""
recs = await sor.sqlExe('''select b.id, b.orgtypeid, b.name recs = await sor.sqlExe('''select b.id, b.orgtypeid, b.name
from users a, role b, userrole c from users a, role b, userrole c
where a.id = c.userid where a.id = c.userid
@ -287,17 +268,14 @@ where a.id = c.userid
roles.append(f'{r.orgtypeid}.{r.name}') roles.append(f'{r.orgtypeid}.{r.name}')
roles.append(f'{r.orgtypeid}.*') roles.append(f'{r.orgtypeid}.*')
roles.append(f'*.{r.name}') roles.append(f'*.{r.name}')
roles = sorted(list(set(roles))) self.ur_caches.set(userid, sorted(list(set(roles))))
self.ur_caches.set(userid, roles)
return roles
def check_roles_path(self, roles, path): def check_roles_path(self, roles, path):
"""Check if any of the roles has access to the given path. """Check if any of the roles has access to the given path.
Supports: Supports:
- Exact match: '/customer_management/index.ui' or '/main/login.ui' - Exact match: '/customer_management/index.ui' or '/main/login.ui'
- Wildcard prefix match: '/customer_management/**' or '/customer_management/%' - Wildcard prefix match: '/customer_management/**' matches any path starting with '/customer_management/'
matches any path starting with '/customer_management/'
- Path normalization: tries both the raw path and path with /main stripped - Path normalization: tries both the raw path and path with /main stripped
""" """
for role in roles: for role in roles:
@ -314,22 +292,16 @@ where a.id = c.userid
return True return True
# Also try wildcard match with normalized path # Also try wildcard match with normalized path
for perm_path in paths: for perm_path in paths:
prefix = None
if perm_path.endswith('**'): if perm_path.endswith('**'):
prefix = perm_path[:-2] prefix = perm_path[:-2]
elif perm_path.endswith('%'): if normalized.startswith(prefix) or path.startswith(prefix):
prefix = perm_path[:-1] return True
if prefix and (normalized.startswith(prefix) or path.startswith(prefix)):
return True
# Wildcard prefix match with raw path # Wildcard prefix match with raw path
for perm_path in paths: for perm_path in paths:
prefix = None
if perm_path.endswith('**'): if perm_path.endswith('**'):
prefix = perm_path[:-2] prefix = perm_path[:-2]
elif perm_path.endswith('%'): if path.startswith(prefix):
prefix = perm_path[:-1] return True
if prefix and path.startswith(prefix):
return True
return False return False
async def is_user_has_path_perm(self, userid, path): async def is_user_has_path_perm(self, userid, path):
@ -344,19 +316,13 @@ where a.id = c.userid
if userid is None: if userid is None:
roles = ['any', 'anonymous'] roles = ['any', 'anonymous']
if not _cache_enabled('rbac') or self.rp_caches is None or not roles: if self.rp_caches is None or not roles:
env = ServerEnv() env = ServerEnv()
async with get_sor_context(env, 'rbac') as sor: async with get_sor_context(env, 'rbac') as sor:
if not _cache_enabled('rbac') or self.rp_caches is None: if self.rp_caches is None:
await self.load_roleperms(sor) await self.load_roleperms(sor)
if not roles: if not roles:
roles = await self.get_userroles(sor, userid) await self.get_userroles(sor, userid)
# When cache is enabled, fall back to cache read roles = self.ur_caches.get(userid)
if roles is None:
roles = self.ur_caches.get(userid)
# Safety fallback: if roles is still None (shouldn't happen), deny access
if not roles:
return False
return self.check_roles_path(roles, path) return self.check_roles_path(roles, path)

View File

@ -1,96 +0,0 @@
#!/usr/bin/env python3
"""rbac 模块 RBAC 权限注册。"""
import subprocess
MOD = "rbac"
# any — 无需登录(登录页、注册、验证码、公共资源等)
PATHS_ANY = [
f"/{MOD}/admin_menu.ui",
f"/{MOD}/gen_sms_code.dspy",
f"/{MOD}/login.css",
f"/{MOD}/phone_login.dspy",
f"/{MOD}/qr_scan.ui",
f"/{MOD}/user/code_login.dspy",
f"/{MOD}/user/login.ui",
f"/{MOD}/user/logout.dspy",
f"/{MOD}/user/register.dspy",
f"/{MOD}/user/register.ui",
f"/{MOD}/user/sms_register.dspy",
f"/{MOD}/user/reset_password/index.ui",
f"/{MOD}/user/reset_password/reset_password.dspy",
f"/{MOD}/user/up_login.dspy",
f"/{MOD}/usermenu.ui",
f"/{MOD}/userpassword_login.dspy",
f"/{MOD}/userpassword_login.ui",
# 公共资源
"/favicon.ico",
"/i18n_getmsgs",
]
# logined — 需要认证的页面和 API
PATHS_LOGINED = [
f"/{MOD}",
f"/{MOD}/add_adminuser.dspy",
f"/{MOD}/add_adminuser.ui",
f"/{MOD}/add_provider.dspy",
f"/{MOD}/add_provider.ui",
f"/{MOD}/add_reseller.dspy",
f"/{MOD}/add_superuser.dspy",
f"/{MOD}/find_unauth_files.dspy",
f"/{MOD}/get_all_roles.dspy",
f"/{MOD}/get_normal_roles.dspy",
f"/{MOD}/get_provider.dspy",
f"/{MOD}/get_reseller.dspy",
f"/{MOD}/index.ui",
f"/{MOD}/list_path_roles.dspy",
f"/{MOD}/list_path_roles.ui",
f"/{MOD}/organization",
f"/{MOD}/orgtypes",
f"/{MOD}/permission",
f"/{MOD}/provider",
f"/{MOD}/refresh_userperm.dspy",
f"/{MOD}/reseller",
f"/{MOD}/role",
f"/{MOD}/rolepermission",
f"/{MOD}/stat_active_users.ui",
f"/{MOD}/stat_total_orgs.ui",
f"/{MOD}/stat_total_users.ui",
f"/{MOD}/user",
f"/{MOD}/user/myrole.ui",
f"/{MOD}/user/user.ui",
f"/{MOD}/user/user_panel.ui",
f"/{MOD}/user/userapikey",
f"/{MOD}/user/userapikey/add_userapikey.dspy",
f"/{MOD}/user/userapikey/delete_userapikey.dspy",
f"/{MOD}/user/userapikey/get_userapikey.dspy",
f"/{MOD}/user/userapikey/index.ui",
f"/{MOD}/user/userapikey/update_userapikey.dspy",
f"/{MOD}/user/userinfo.ui",
f"/{MOD}/user/edit_profile.dspy",
f"/{MOD}/user/save_profile.dspy",
f"/{MOD}/user/wechat_login.ui",
f"/{MOD}/userapp",
f"/{MOD}/userdepartment",
f"/{MOD}/userrole",
f"/{MOD}/users",
f"/{MOD}/usersync",
f"/{MOD}/usersync/index.dspy",
]
def register_paths():
for path in PATHS_ANY:
subprocess.run(["py3/bin/python", "set_role_perm.py", "any", path])
print(f" any: {path}")
for path in PATHS_LOGINED:
subprocess.run(["py3/bin/python", "set_role_perm.py", "logined", path])
print(f" logined: {path}")
if __name__ == "__main__":
print(f"=== {MOD} RBAC registration ===")
register_paths()
print("Done.")

View File

@ -7,22 +7,12 @@ if phone is None:
} }
} }
# 使用短信模块发布的sms_engine实例生成验证码参数手机号 # 使用短信模块发布的sms_engine实例生成验证码参数手机号
try: xx = await sms_engine.generate_sms_code(phone)
xx = await sms_engine.generate_sms_code(phone)
except Exception as e:
debug(f'gen_sms_code error: {e}')
exception(f'gen_sms_code error for {phone}: {e}')
return {
"status": "error",
"data": {
"message": f"发送验证码出错: {e}"
}
}
if xx is None: if xx is None:
return { return {
"status": "error", "status": "error",
"data": { "data": {
"message": "发送验证码出错请检查短信模板配置和百度API连接" "message": "发送验证码出错"
} }
} }
id, code = xx id, code = xx

212
wwwroot/index.ui Normal file
View File

@ -0,0 +1,212 @@
{% 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": "<svg width=\"36\" height=\"36\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#60A5FA\" stroke-width=\"1.5\"><path d=\"M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.953 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z\"/></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": "<svg width=\"36\" height=\"36\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#A78BFA\" stroke-width=\"1.5\"><path d=\"M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z\"/></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": "<svg width=\"36\" height=\"36\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#EF4444\" stroke-width=\"1.5\"><path d=\"M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z\"/></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"
}
}
]
}

View File

@ -1,242 +0,0 @@
/* ===== Modern Login Popup Styling ===== */
/* Popup window card */
.login-window {
border-radius: 16px !important;
box-shadow: 0 25px 60px -12px rgba(0, 0, 0, 0.35) !important;
overflow: hidden !important;
border: none !important;
}
/* Title bar - gradient brand header */
.login-window .titlebar {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%) !important;
padding: 8px 16px !important;
border: none !important;
min-height: 48px !important;
}
.login-window .titlebar .text-w,
.login-window .titlebar .bricks-text {
color: #ffffff !important;
font-size: 16px !important;
font-weight: 600 !important;
}
/* Content area padding */
.login-window .flexbox {
padding: 0 !important;
}
/* Tab panel modern styling */
.login-window .tabpanel {
background: transparent !important;
border: none !important;
display: flex !important;
flex-direction: column !important;
}
.login-window .tabpanel-tabs {
display: flex !important;
border-bottom: 2px solid #e2e8f0 !important;
background: #f8fafc !important;
padding: 0 !important;
gap: 0 !important;
}
.login-window .tabpanel-tab {
padding: 14px 24px !important;
cursor: pointer !important;
border-bottom: 3px solid transparent !important;
margin-bottom: -2px !important;
transition: all 0.2s ease !important;
color: #64748b !important;
font-weight: 500 !important;
font-size: 14px !important;
background: transparent !important;
}
.login-window .tabpanel-tab:hover {
color: #6366f1 !important;
background: rgba(99, 102, 241, 0.04) !important;
}
.login-window .tabpanel-tab-active,
.login-window .tabpanel-tab-selected {
color: #6366f1 !important;
border-bottom-color: #6366f1 !important;
background: transparent !important;
}
.login-window .tabpanel-content {
padding: 0 !important;
background: #ffffff !important;
overflow: hidden !important;
flex: 1 !important;
min-height: 0 !important;
}
.login-window .tabpanel-content .scrollpanel {
height: 100% !important;
}
.login-window .tabpanel-content .scrollpanel .vcontainer {
padding: 20px 24px !important;
}
/* Form styling */
.login-window .vcontainer {
gap: 4px !important;
}
.login-window .inputbox {
border-radius: 10px !important;
border: 1.5px solid #e2e8f0 !important;
padding: 10px 14px !important;
transition: border-color 0.2s ease, box-shadow 0.2s ease !important;
background: #f8fafc !important;
font-size: 14px !important;
}
.login-window .inputbox:focus,
.login-window .inputbox:focus-within {
border-color: #6366f1 !important;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.12) !important;
background: #ffffff !important;
outline: none !important;
}
.login-window input.inputbox {
height: 42px !important;
}
/* Form labels */
.login-window .field-label,
.login-window .bricks-form label {
font-weight: 500 !important;
color: #374151 !important;
font-size: 13px !important;
margin-bottom: 4px !important;
}
/* Toolbar buttons */
.login-window .htoolbar {
padding: 8px 0 !important;
gap: 8px !important;
justify-content: center !important;
}
.login-window .submit_btn {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%) !important;
color: #ffffff !important;
border: none !important;
border-radius: 10px !important;
padding: 10px 32px !important;
font-weight: 600 !important;
font-size: 14px !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3) !important;
}
.login-window .submit_btn:hover {
opacity: 0.92 !important;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4) !important;
transform: translateY(-1px) !important;
}
.login-window .reset_btn,
.login-window .clear_btn {
border-radius: 10px !important;
border: 1.5px solid #e2e8f0 !important;
background: #ffffff !important;
color: #64748b !important;
padding: 10px 20px !important;
font-weight: 500 !important;
transition: all 0.2s ease !important;
}
.login-window .reset_btn:hover,
.login-window .clear_btn:hover {
border-color: #cbd5e1 !important;
background: #f8fafc !important;
}
/* SMS send button */
.sms-send-btn {
border-radius: 10px !important;
border: 1.5px solid #6366f1 !important;
background: rgba(99, 102, 241, 0.06) !important;
color: #6366f1 !important;
padding: 8px 20px !important;
font-weight: 600 !important;
font-size: 13px !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
}
.sms-send-btn:hover {
background: rgba(99, 102, 241, 0.12) !important;
}
.sms-send-btn:disabled {
opacity: 0.5 !important;
cursor: not-allowed !important;
}
/* Description text */
.login-window .bricks-text {
color: #64748b;
}
/* Form description */
.login-desc {
color: #94a3b8 !important;
font-size: 12px !important;
text-align: center !important;
margin-top: 4px !important;
}
/* Section separator */
.login-separator {
display: flex !important;
align-items: center !important;
gap: 12px !important;
margin: 8px 0 !important;
color: #cbd5e1 !important;
font-size: 12px !important;
}
.login-separator::before,
.login-separator::after {
content: '' !important;
flex: 1 !important;
height: 1px !important;
background: #e2e8f0 !important;
}
/* Dark theme support */
[data-theme="dark"] .login-window {
background: #1e293b !important;
}
[data-theme="dark"] .login-window .tabpanel-content {
background: #1e293b !important;
}
[data-theme="dark"] .login-window .tabpanel-tabs {
background: #0f172a !important;
border-bottom-color: #334155 !important;
}
[data-theme="dark"] .login-window .tabpanel-tab {
color: #94a3b8 !important;
}
[data-theme="dark"] .login-window .tabpanel-tab-active,
[data-theme="dark"] .login-window .tabpanel-tab-selected {
color: #818cf8 !important;
border-bottom-color: #818cf8 !important;
}
[data-theme="dark"] .login-window .inputbox {
background: #0F172A !important;
border-color: #475569 !important;
color: #e2e8f0 !important;
}
[data-theme="dark"] .login-window .inputbox:focus,
[data-theme="dark"] .login-window .inputbox:focus-within {
border-color: #818cf8 !important;
box-shadow: 0 0 0 3px rgba(129, 140, 248, 0.15) !important;
}
[data-theme="dark"] .login-window .field-label {
color: #cbd5e1 !important;
}
[data-theme="dark"] .login-window .reset_btn,
[data-theme="dark"] .login-window .clear_btn {
background: #334155 !important;
border-color: #475569 !important;
color: #cbd5e1 !important;
}
[data-theme="dark"] .sms-send-btn {
border-color: #818cf8 !important;
color: #818cf8 !important;
background: rgba(129, 140, 248, 0.1) !important;
}

View File

@ -1,257 +0,0 @@
# 手机验证码登录 - 接收前端表单参数(cell_no, codeid, check_code)
# 调用sms_engine验证后完成登录或自动注册
debug(f'code_login.dspy: {params_kw=}')
cellphone = params_kw.cell_no
key = params_kw.codeid
sms_code = params_kw.check_code
if not cellphone:
return {
"widgettype": "Error",
"options": {
"timeout": 3,
"title": "错误",
"message": "需输入手机号"
}
}
if not sms_code:
return {
"widgettype": "Error",
"options": {
"timeout": 3,
"title": "错误",
"message": "需输入验证码"
}
}
if not key:
return {
"widgettype": "Error",
"options": {
"timeout": 3,
"title": "错误",
"message": "需要短信验证key"
}
}
# 验证短信码
ok = await sms_engine.check_sms_code(key, sms_code)
if not ok:
return {
"widgettype": "Error",
"options": {
"timeout": 3,
"title": "验证失败",
"message": "手机短信验证码出错"
}
}
# 验证通过,查找或注册用户
ns = {
"username": cellphone,
"password": "^&%UHI",
"cfm_password": "^&%UHI",
"mobile": cellphone,
"user_status": "0"
}
udata = DictObject(**ns)
try:
async with get_sor_context(request._run_ns, 'rbac') as sor:
recs = await sor.R('users', {'mobile': cellphone})
if recs:
if len(recs) == 1:
r = recs[0]
now_str = timestampstr()
await sor.sqlExe("""
UPDATE users
SET last_login = ${now}$, login_fail_count = 0,
last_login_fail = NULL
WHERE id = ${id}$
""", {'id': r.id, 'now': now_str})
await remember_user(r.id, username=r.username, userorgid=r.orgid)
return {
"widgettype": "Message",
"options": {
"timeout": 3,
"auto_open": True,
"title": "登录成功",
"message": f"{r.username} 欢迎回来"
},
"binds": [
{
"wid": "self",
"event": "dismissed",
"actiontype": "urlwidget",
"target": "window.user_container",
"options": {
"url": entire_url('/rbac/user/userinfo.ui')
}
},
{
"wid": "self",
"event": "dismissed",
"actiontype": "script",
"target": "body.login_window",
"script": "this.destroy()"
},
{
"wid": "self",
"event": "dismissed",
"actiontype": "script",
"target": "self",
"script": "if(bricks.app && bricks.app.dispatch) bricks.app.dispatch('user_logined')"
}
]
}
# 多个用户绑定同一手机号
if params_kw.selected_id:
for r in recs:
if r.id == params_kw.selected_id:
now_str = timestampstr()
await sor.sqlExe("""
UPDATE users
SET last_login = ${now}$, login_fail_count = 0,
last_login_fail = NULL
WHERE id = ${id}$
""", {'id': r.id, 'now': now_str})
await remember_user(r.id, username=r.username, userorgid=r.orgid)
return {
"widgettype": "Message",
"options": {
"timeout": 3,
"auto_open": True,
"title": "登录成功",
"message": f"{r.username} 欢迎回来"
},
"binds": [
{
"wid": "self",
"event": "dismissed",
"actiontype": "urlwidget",
"target": "window.user_container",
"options": {
"url": entire_url('/rbac/user/userinfo.ui')
}
},
{
"wid": "self",
"event": "dismissed",
"actiontype": "script",
"target": "body.login_window",
"script": "this.destroy()"
},
{
"wid": "self",
"event": "dismissed",
"actiontype": "script",
"target": "self",
"script": "if(bricks.app && bricks.app.dispatch) bricks.app.dispatch('user_logined')"
}
]
}
else:
# 返回用户选择列表
buttons = []
for r in recs:
buttons.append({
"widgettype": "Button",
"options": {
"label": f"{r.username} ({r.id})",
"width": "100%",
"margin": "4px 0"
},
"binds": [{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "self",
"options": {
"url": entire_url('/rbac/user/code_login.dspy'),
"params": {
"cell_no": cellphone,
"codeid": key,
"check_code": sms_code,
"selected_id": r.id
}
}
}]
})
return {
"widgettype": "VBox",
"options": {"padding": "12px"},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "该手机号关联多个账号,请选择:",
"fontSize": "14px",
"margin": "0 0 8px 0"
}
}
] + buttons
}
# 新用户自动注册
d = await register_user(sor, udata)
if d['status'] == 'error':
return {
"widgettype": "Error",
"options": {
"timeout": 5,
"title": "注册失败",
"message": d['data']['message']
}
}
try:
ownerid = await get_owner_orgid(sor, orgid)
await openCustomerAccounts(sor, ownerid, orgid)
except Exception as e:
exception(f'{e}')
r = d['data']['user']
await remember_user(r.id, username=r.username, userorgid=r.orgid)
return {
"widgettype": "Message",
"options": {
"timeout": 3,
"auto_open": True,
"title": "登录成功",
"message": f"{r.username} 欢迎"
},
"binds": [
{
"wid": "self",
"event": "dismissed",
"actiontype": "urlwidget",
"target": "window.user_container",
"options": {
"url": entire_url('/rbac/user/userinfo.ui')
}
},
{
"wid": "self",
"event": "dismissed",
"actiontype": "script",
"target": "body.login_window",
"script": "this.destroy()"
},
{
"wid": "self",
"event": "dismissed",
"actiontype": "script",
"target": "self",
"script": "if(bricks.app && bricks.app.dispatch) bricks.app.dispatch('user_logined')"
}
]
}
except Exception as e:
exception(f'code_login error: {e}')
return {
"widgettype": "Error",
"options": {
"timeout": 5,
"title": "系统错误",
"message": f"{e}"
}
}

View File

@ -1,79 +0,0 @@
debug(f'edit_profile.dspy: {params_kw=}')
userid = await get_user()
if userid is None:
return UiError(title='Error', message='You need login first')
db = DBPools()
dbname = get_module_dbname('rbac')
async with db.sqlorContext(dbname) as sor:
recs = await sor.R('users', {'id': userid})
if not recs:
return UiError(title='Error', message='User not found')
user = recs[0]
nick_name = user.nick_name or ''
email = user.email or ''
mobile = user.mobile or ''
address = user.address or ''
return {
"widgettype": "Form",
"options": {
"title": "完善个人信息",
"description": "请填写或更新您的个人信息",
"fields": [
{
"name": "nick_name",
"type": "str",
"length": 255,
"uitype": "str",
"datatype": "str",
"required": False,
"label": "显示名",
"value": nick_name
},
{
"name": "email",
"type": "str",
"length": 255,
"uitype": "email",
"datatype": "str",
"required": False,
"label": "邮件地址",
"value": email
},
{
"name": "mobile",
"type": "str",
"length": 255,
"uitype": "tel",
"datatype": "str",
"required": False,
"label": "手机号",
"value": mobile
},
{
"name": "address",
"type": "str",
"length": 255,
"uitype": "str",
"datatype": "str",
"required": False,
"label": "地址",
"value": address
}
]
},
"binds": [
{
"wid": "self",
"event": "submit",
"actiontype": "urlwidget",
"target": "self",
"options": {
"method": "POST",
"url": entire_url('save_profile.dspy')
}
}
]
}

View File

@ -1,22 +1,16 @@
{% set sms_code_url = entire_url('/rbac/gen_sms_code.dspy') %}
{% set code_login_url = entire_url('/rbac/user/code_login.dspy') %}
{% set sms_register_url = entire_url('/rbac/user/sms_register.dspy') %}
{ {
"id": "login_window", "id": "login_window",
"widgettype": "PopupWindow", "widgettype": "PopupWindow",
"options": { "options": {
"title": "欢迎登录", "title": "登录/注册",
"i18n": true,
"css": "login-window",
"auto_open": true, "auto_open": true,
"archor": "cc", "anthor": "cc",
"cwidth": 26, "cwidth": 22,
"cheight": 30 "cheight": 19
}, },
"subwidgets": [ "subwidgets": [
{ {
"widgettype": "TabPanel", "widgettype": "TabPanel",
"id": "login_tabs",
"options": { "options": {
"tab_wide": "auto", "tab_wide": "auto",
"height": "100%", "height": "100%",
@ -25,159 +19,90 @@
"items": [ "items": [
{ {
"name": "userpasswd", "name": "userpasswd",
"label": "密码登录", "label": "用户密码",
"content": { "content": {
"widgettype": "VScrollPanel", "widgettype": "Form",
"options": {"height": "100%"}, "options": {
"subwidgets": [ "cols": 1,
{ "fields": [
"widgettype": "Form", {
"options": { "name": "username",
"cols": 1, "label": "用户名",
"fields": [ "uitype": "str"
{"name": "username", "label": "用户名", "uitype": "str"},
{"name": "password", "label": "密码", "uitype": "password"}
]
}, },
"binds": [ {
{ "name": "password",
"wid": "self", "label": "密码",
"event": "submit", "uitype": "password"
"actiontype": "urlwidget", }
"target": "self", ]
"options": { },
"method": "POST", "binds": [
"url": "{{entire_url('up_login.dspy')}}"
}
}
]
}
]
}
},
{
"name": "phonecode",
"label": "手机登录",
"content": {
"widgettype": "VScrollPanel",
"options": {"height": "100%"},
"subwidgets": [
{ {
"widgettype": "VBox", "wid": "self",
"options": {"gap": "8px"}, "event": "submit",
"subwidgets": [ "actiontype": "urlwidget",
{ "target": "self",
"widgettype": "Text",
"options": {
"otext": "未注册的手机号将自动创建账号",
"i18n": true,
"css": "login-desc"
}
},
{
"widgettype": "Form",
"id": "phone_form",
"options": {
"cols": 1,
"fields": [
{"name": "cell_no", "label": "手机号", "uitype": "str"},
{"name": "codeid", "uitype": "hide", "value": ""},
{"name": "check_code", "label": "验证码", "uitype": "str"}
]
},
"binds": [
{
"wid": "self",
"event": "submit",
"actiontype": "urlwidget",
"target": "self",
"options": {
"method": "POST",
"url": "{{code_login_url}}"
}
}
]
},
{
"widgettype": "Button",
"id": "gen_code_btn",
"options": {"label": "发送验证码", "i18n": true, "css": "sms-send-btn"},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "script",
"target": "self",
"script": "var form=bricks.getWidgetById('phone_form',bricks.app);if(!form)return;var cell=form._getValue().cell_no;if(!cell||cell.length<11){alert('请输入正确的手机号');return;}var btn=this;btn.disabled=true;btn.text_w&&btn.text_w.set_otext('发送中...');fetch('{{sms_code_url}}?_webbricks_=1&cellphone='+encodeURIComponent(cell)).then(function(r){return r.json()}).then(function(d){if(d.status==='ok'){var w=form.name_inputs['codeid'];if(w)w.setValue(d.data.key);btn.text_w&&btn.text_w.set_otext('已发送');var s=60;var t=setInterval(function(){s--;if(s<=0){clearInterval(t);btn.disabled=false;btn.text_w&&btn.text_w.set_otext('重新发送')}else btn.text_w&&btn.text_w.set_otext(s+'s')},1000)}else{alert(d.data.message||'发送验证码出错');btn.disabled=false;btn.text_w&&btn.text_w.set_otext('发送验证码')}}).catch(function(e){alert('网络错误: '+e);btn.disabled=false;btn.text_w&&btn.text_w.set_otext('发送验证码')})"
}
]
}
]
}
]
}
},
{
"name": "register",
"label": "注册账号",
"content": {
"widgettype": "VBox",
"options": {"gap": "8px", "height": "100%"},
"subwidgets": [
{
"widgettype": "Text",
"options": { "options": {
"otext": "手机号需短信验证后方可注册", "method": "POST",
"i18n": true, "url": "{{entire_url('up_login.dspy')}}"
"css": "login-desc" }
}
]
}
},
{
"name": "checkcode",
"label": "手机验证码",
"content": {
"widgettype": "Form",
"options": {
"toolbar": {
"tools": [
{
"name": "gen_code",
"label": "发送验证码"
}
]
},
"description": "限中国国内手机",
"fields": [
{
"name": "cell_no",
"label": "手机号",
"uitype": "str"
},
{
"name": "codeid",
"uitype": "hide",
"value": "{{uuid()}}"
},
{
"name": "check_code",
"uitype": "str"
}
]
},
"binds": [
{
"wid": "self",
"event": "gen_code",
"actiontype": "urlwidget",
"datawidget": "self",
"datamethod": "getValue",
"target": "self",
"options": {
"url": "{{entire_url('../gen_sms_code.dspy')}}"
} }
}, },
{ {
"widgettype": "VScrollPanel", "wid": "self",
"options": {"height": "100%", "flex": "1"}, "event": "submit",
"subwidgets": [ "actiontype": "urlwidget",
{ "target": "self",
"widgettype": "Form", "options": {
"id": "register_form", "url": "{{entire_url('code_login.dspy')}}"
"options": { }
"cols": 1,
"fields": [
{"name": "username", "label": "用户名", "uitype": "str"},
{"name": "mobile", "label": "手机号", "uitype": "str"},
{"name": "codeid", "uitype": "hide", "value": ""},
{"name": "check_code", "label": "短信验证码", "uitype": "str"},
{"name": "password", "label": "密码", "uitype": "password"},
{"name": "cfm_password", "label": "确认密码", "uitype": "password"}
]
},
"binds": [
{
"wid": "self",
"event": "submit",
"actiontype": "urlwidget",
"target": "self",
"options": {
"method": "POST",
"url": "{{sms_register_url}}"
}
}
]
}
]
},
{
"widgettype": "Button",
"id": "reg_sms_btn",
"options": {"label": "发送验证码", "i18n": true, "css": "sms-send-btn"},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "script",
"target": "self",
"script": "var form=bricks.getWidgetById('register_form',bricks.app);if(!form)return;var cell=form._getValue().mobile;if(!cell||cell.length<11){alert('请输入正确的手机号');return;}var btn=this;btn.disabled=true;btn.text_w&&btn.text_w.set_otext('发送中...');fetch('{{sms_code_url}}?_webbricks_=1&cellphone='+encodeURIComponent(cell)).then(function(r){return r.json()}).then(function(d){if(d.status==='ok'){var w=form.name_inputs['codeid'];if(w)w.setValue(d.data.key);btn.text_w&&btn.text_w.set_otext('已发送');var s=60;var t=setInterval(function(){s--;if(s<=0){clearInterval(t);btn.disabled=false;btn.text_w&&btn.text_w.set_otext('重新发送')}else btn.text_w&&btn.text_w.set_otext(s+'s')},1000)}else{alert(d.data.message||'发送验证码出错');btn.disabled=false;btn.text_w&&btn.text_w.set_otext('发送验证码')}}).catch(function(e){alert('网络错误: '+e);btn.disabled=false;btn.text_w&&btn.text_w.set_otext('发送验证码')})"
}
]
} }
] ]
} }

View File

@ -1,38 +1,9 @@
await forget_user() await forget_user()
return { return {
"widgettype": "VBox", "widgettype":"Text",
"options": { "options":{
"padding": "24px", "otext":"logout success",
"alignItems": "center" "i18n":True,
}, }
"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()"
}
]
}
]
} }

View File

@ -1,69 +1,18 @@
debug(f'register.dspy: {params_kw=}') debug(f'{params_kw=}')
db = DBPools() db = DBPools()
dbname = get_module_dbname('rbac') dbname = get_module_dbname('rbac')
async with db.sqlorContext(dbname) as sor: async with db.sqlorContext(dbname) as sor:
data = await register_user(sor, params_kw) data = await register_user(sor, params_kw)
data = DictObject(**data) data = DictObject(**data)
if data.status == 'error':
debug(f"register error: {data.data.message}")
return {
"widgettype": "Error",
"options": {
"timeout": 5,
"title": "注册失败",
"message": data.data.message
}
}
user = data.data.user
orgid = user.orgid
try: try:
if data['status'] == 'error':
debug(f"{data.data.message}")
return UiError(title='Error', message=data.data.message)
orgid = data.data.user.orgid
await openCustomerAccounts(sor, '0', orgid) await openCustomerAccounts(sor, '0', orgid)
debug(f'{orgid} accounts opened') debug(f'{orgid} accounts opened')
except Exception as e: except Exception as e:
exception(f'{e},{orgid=}') exception(f'{e},{orgid=}')
# 注册成功后自动登录 return UiMessage(title="Success", message=f"register success {orgid}")
await remember_user(user.id, username=user.username, userorgid=user.orgid) return UiError(title='Error', message="register failed")
return {
"widgettype": "Message",
"options": {
"timeout": 3,
"auto_open": True,
"title": "注册成功",
"message": f"{user.username} 注册成功,已自动登录"
},
"binds": [
{
"wid": "self",
"event": "dismissed",
"actiontype": "urlwidget",
"target": "window.user_container",
"options": {
"url": entire_url('/rbac/user/userinfo.ui')
}
},
{
"wid": "self",
"event": "dismissed",
"actiontype": "script",
"target": "body.login_window",
"script": "if(this.destroy) this.destroy()"
},
{
"wid": "self",
"event": "dismissed",
"actiontype": "script",
"target": "self",
"script": "if(bricks.app && bricks.app.dispatch) bricks.app.dispatch('user_logined')"
}
]
}
return {
"widgettype": "Error",
"options": {
"timeout": 5,
"title": "注册失败",
"message": "系统错误,请稍后重试"
}
}

View File

@ -1,40 +0,0 @@
debug(f'save_profile.dspy: {params_kw=}')
userid = await get_user()
if userid is None:
return UiError(title='Error', message='You need login first')
data = {
'id': userid,
'nick_name': params_kw.nick_name or '',
'email': params_kw.email or '',
'mobile': params_kw.mobile or '',
'address': params_kw.address or ''
}
db = DBPools()
dbname = get_module_dbname('rbac')
try:
async with db.sqlorContext(dbname) as sor:
await sor.U('users', data)
return {
"widgettype": "Message",
"options": {
"timeout": 3,
"auto_open": True,
"title": "保存成功",
"message": "个人信息已更新"
},
"binds": [
{
"wid": "self",
"event": "dismissed",
"actiontype": "script",
"target": "self",
"script": "if(bricks.app && bricks.app.dispatch) bricks.app.dispatch('profile_updated')"
}
]
}
except Exception as e:
exception(f'save_profile error: {e}')
return UiError(title='Error', message=f'保存失败: {e}')

View File

@ -1,187 +0,0 @@
# 短信验证注册 - 接收前端表单参数(username, mobile, codeid, check_code, password, cfm_password)
# 先验证短信码,通过后注册并自动登录
debug(f'sms_register.dspy: {params_kw=}')
username = params_kw.username
mobile = params_kw.mobile
key = params_kw.codeid
sms_code = params_kw.check_code
password = params_kw.password
cfm_password = params_kw.cfm_password
# 基本参数校验
if not username:
return {
"widgettype": "Error",
"options": {
"timeout": 3,
"title": "注册失败",
"message": "请输入用户名"
}
}
if not mobile:
return {
"widgettype": "Error",
"options": {
"timeout": 3,
"title": "注册失败",
"message": "请输入手机号"
}
}
if not password:
return {
"widgettype": "Error",
"options": {
"timeout": 3,
"title": "注册失败",
"message": "请输入密码"
}
}
if password != cfm_password:
return {
"widgettype": "Error",
"options": {
"timeout": 3,
"title": "注册失败",
"message": "两次输入的密码不一致"
}
}
if not key or not sms_code:
return {
"widgettype": "Error",
"options": {
"timeout": 3,
"title": "注册失败",
"message": "请先发送并输入短信验证码"
}
}
# 验证短信码
try:
ok = await sms_engine.check_sms_code(key, sms_code)
except Exception as e:
exception(f'sms_register sms check error: {e}')
ok = False
if not ok:
return {
"widgettype": "Error",
"options": {
"timeout": 3,
"title": "验证失败",
"message": "短信验证码错误或已过期,请重新获取"
}
}
# 短信验证通过,注册用户
db = DBPools()
dbname = get_module_dbname('rbac')
try:
async with db.sqlorContext(dbname) as sor:
# 检查手机号是否已注册
existing = await sor.R('users', {'mobile': mobile})
if existing:
return {
"widgettype": "Error",
"options": {
"timeout": 5,
"title": "注册失败",
"message": "该手机号已注册,请直接登录"
}
}
# 检查用户名是否已存在
existing_user = await sor.R('users', {'username': username})
if existing_user:
return {
"widgettype": "Error",
"options": {
"timeout": 5,
"title": "注册失败",
"message": "用户名已被占用"
}
}
# 调用注册函数
reg_params = DictObject(
username=username,
mobile=mobile,
password=password,
cfm_password=cfm_password
)
data = await register_user(sor, reg_params)
data = DictObject(**data)
if data.status == 'error':
debug(f"sms_register error: {data.data.message}")
return {
"widgettype": "Error",
"options": {
"timeout": 5,
"title": "注册失败",
"message": data.data.message
}
}
user = data.data.user
orgid = user.orgid
try:
await openCustomerAccounts(sor, '0', orgid)
debug(f'{orgid} accounts opened')
except Exception as e:
exception(f'{e},{orgid=}')
# 注册成功后自动登录
await remember_user(user.id, username=user.username, userorgid=user.orgid)
return {
"widgettype": "Message",
"options": {
"timeout": 3,
"auto_open": True,
"title": "注册成功",
"message": f"{user.username} 注册成功,已自动登录"
},
"binds": [
{
"wid": "self",
"event": "dismissed",
"actiontype": "urlwidget",
"target": "window.user_container",
"options": {
"url": entire_url('/rbac/user/userinfo.ui')
}
},
{
"wid": "self",
"event": "dismissed",
"actiontype": "script",
"target": "body.login_window",
"script": "this.destroy()"
},
{
"wid": "self",
"event": "dismissed",
"actiontype": "script",
"target": "self",
"script": "if(bricks.app && bricks.app.dispatch) bricks.app.dispatch('user_logined')"
}
]
}
except Exception as e:
exception(f'sms_register error: {e}')
return {
"widgettype": "Error",
"options": {
"timeout": 5,
"title": "系统错误",
"message": f"注册失败: {e}"
}
}
return {
"widgettype": "Error",
"options": {
"timeout": 5,
"title": "注册失败",
"message": "系统错误,请稍后重试"
}
}

View File

@ -1,3 +1,4 @@
debug(f'{params_kw=}') debug(f'{params_kw=}')
ns = { ns = {
"username":params_kw.username, "username":params_kw.username,
@ -12,10 +13,6 @@ async with db.sqlorContext(dbname) as sor:
r = await sor.sqlExe('select * from users where username=${username}$', ns.copy()) r = await sor.sqlExe('select * from users where username=${username}$', ns.copy())
if len(r) == 0: if len(r) == 0:
return { return {
"status": "error",
"data": {
"message": "user name or password error"
},
"widgettype":"Error", "widgettype":"Error",
"options":{ "options":{
"timeout":3, "timeout":3,
@ -34,10 +31,6 @@ async with db.sqlorContext(dbname) as sor:
elapsed = (datetime.datetime.now() - stored).total_seconds() elapsed = (datetime.datetime.now() - stored).total_seconds()
if elapsed < 300: if elapsed < 300:
return { return {
"status": "error",
"data": {
"message": "Account locked due to too many failed login attempts. Please try again in 5 minutes."
},
"widgettype":"Error", "widgettype":"Error",
"options":{ "options":{
"timeout":5, "timeout":5,
@ -72,10 +65,6 @@ async with db.sqlorContext(dbname) as sor:
else: else:
msg = f"user name or password error ({3 - new_fail_count} attempts remaining)" msg = f"user name or password error ({3 - new_fail_count} attempts remaining)"
return { return {
"status": "error",
"data": {
"message": msg
},
"widgettype":"Error", "widgettype":"Error",
"options":{ "options":{
"timeout":3, "timeout":3,
@ -93,41 +82,33 @@ async with db.sqlorContext(dbname) as sor:
""", {'id': user.id, 'now': now_str}) """, {'id': user.id, 'now': now_str})
await remember_user(r[0].id, username=r[0].username, userorgid=r[0].orgid) await remember_user(r[0].id, username=r[0].username, userorgid=r[0].orgid)
return { return {
"status": "ok",
"data": { "user": r[0]},
"widgettype":"Message", "widgettype":"Message",
"options":{ "options":{
"timeout":3, "timeout":3,
"anchor": "cc",
"auto_open":True, "auto_open":True,
"title":"Login", "title":"Login",
"message":f"{r[0].username} Welcome back" "message":f"{r[0].username} Welcome back"
}, },
"binds":[ "binds":[
{ {
"wid":"self", "wid":"self",
"event":"dismissed", "event":"dismissed",
"actiontype":"urlwidget", "actiontype":"urlwidget",
"target":"window", "target":"window.user_container",
"options":{ "options":{
"url":entire_url('/index.ui') "url":entire_url('/rbac/user/userinfo.ui')
}
},
{
"wid":"self",
"event":"dismissed",
"actiontype":"script",
"target": f'body.login_window',
"script":"this.destroy()"
} }
},
{
"wid":"self",
"event":"opened",
"actiontype":"script",
"target":"window.login_window",
"script":"this.destroy()"
}
] ]
} }
return { return {
"status": "error",
"data": {
"message": "system error"
},
"widgettype":"Error", "widgettype":"Error",
"options":{ "options":{
"timeout":3, "timeout":3,

View File

@ -0,0 +1,25 @@
ns = params_kw.copy()
id = params_kw.id
if not id or len(id) > 32:
id = uuid()
ns['id'] = id
db = DBPools()
async with db.sqlorContext('sage') as sor:
r = await sor.C('userapikey', ns.copy())
return {
"widgettype":"Message",
"options":{
"user_data":ns,
"title":"Add Success",
"message":"ok"
}
}
return {
"widgettype":"Error",
"options":{
"title":"Add Error",
"message":"failed"
}
}

View File

@ -0,0 +1,24 @@
ns = {
'id':params_kw['id'],
}
db = DBPools()
async with db.sqlorContext('sage') as sor:
r = await sor.D('userapikey', ns)
print('delete success');
return {
"widgettype":"Message",
"options":{
"title":"Delete Success",
"message":"ok"
}
}
print('Delete failed');
return {
"widgettype":"Error",
"options":{
"title":"Delete Error",
"message":"failed"
}
}

View File

@ -0,0 +1,74 @@
ns = params_kw.copy()
print(f'get_userapikey.dspy:{ns=}')
if not ns.get('page'):
ns['page'] = 1
if not ns.get('sort'):
ns['sort'] = 'id'
filterjson = params_kw.get('data_filter')
userid = await get_user()
ns['userid'] = userid
if not filterjson:
fields = [ f['name'] for f in [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "providerid",
"title": "\u4f9b\u5e94\u5546id",
"type": "str",
"length": 200
},
{
"name": "customerid",
"title": "\u7528\u6237id",
"type": "str",
"length": 32,
"default": "0"
},
{
"name": "apikey",
"title": "api\u5bc6\u94a5",
"type": "str",
"length": 4000,
"default": "0"
},
{
"name": "secretkey",
"title": "\u9644\u5c5e\u5bc6\u94a5",
"type": "str",
"length": 4000
},
{
"name": "rfname",
"title": "\u51fd\u6570\u540d",
"type": "str",
"length": 400
}
] ]
filterjson = default_filterjson(fields, ns)
sql = '''select a.*, b.providerid_text, c.customerid_text
from (select y.* from users x, userapikey y where x.id=${userid}$ and x.orgid = y.customerid) a left join (select id as providerid,
orgname as providerid_text from organization where 1 = 1) b on a.providerid = b.providerid left join (select id as customerid,
orgname as customerid_text from organization where 1 = 1) c on a.customerid = c.customerid'''
if filterjson:
dbf = DBFilter(filterjson)
conds = dbf.gen(ns)
if conds:
ns.update(dbf.consts)
sql += ' and ' + conds
info(f'{ns=},{sql=}')
db = DBPools()
async with db.sqlorContext('sage') as sor:
r = await sor.sqlPaging(sql, ns)
return r
return {
"total":0,
"rows":[]
}

View File

@ -0,0 +1,126 @@
{
"widgettype":"Tabular",
"options":{
"editable":{
"new_data_url":"{{entire_url('add_userapikey.dspy')}}",
"delete_data_url":"{{entire_url('delete_userapikey.dspy')}}",
"update_data_url":"{{entire_url('update_userapikey.dspy')}}"
},
"data_url":"{{entire_url('./get_userapikey.dspy')}}",
"data_method":"GET",
"data_params":{{json.dumps(params_kw, indent=4)}},
"row_options":{
"browserfields":{
"excloud": [],
"cwidth": {}
},
"editexclouded":[
"id"
],
"fields":[
{
"name": "id",
"title": "id",
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "id"
},
{
"name": "providerid",
"title": "\u4f9b\u5e94\u5546id",
"type": "str",
"length": 200,
"label": "\u4f9b\u5e94\u5546id",
"cwidth":16,
"uitype": "code",
"cwidth":18,
"valueField": "providerid",
"textField": "providerid_text",
"params": {
"dbname": "sage",
"table": "organization",
"tblvalue": "id",
"tbltext": "orgname",
"valueField": "providerid",
"textField": "providerid_text"
},
"dataurl": "\/get_code.dspy"
},
{
"name": "customerid",
"title": "\u7528\u6237id",
"type": "str",
"length": 32,
"cwidth":18,
"default": "0",
"label": "\u7528\u6237id",
"cwidth":16,
"uitype": "code",
"valueField": "customerid",
"textField": "customerid_text",
"params": {
"dbname": "sage",
"table": "organization",
"tblvalue": "id",
"tbltext": "orgname",
"valueField": "customerid",
"textField": "customerid_text"
},
"dataurl": "\/get_code.dspy"
},
{
"name": "apikey",
"title": "api\u5bc6\u94a5",
"type": "str",
"length": 4000,
"default": "0",
"cwidth": 18,
"uitype": "text",
"rows": 4,
"datatype": "str",
"label": "api\u5bc6\u94a5"
},
{
"name": "secretkey",
"title": "\u9644\u5c5e\u5bc6\u94a5",
"type": "str",
"length": 4000,
"cwidth": 18,
"uitype": "text",
"rows": 4,
"datatype": "str",
"label": "\u9644\u5c5e\u5bc6\u94a5"
},
{
"name": "rfname",
"title": "\u51fd\u6570\u540d",
"type": "str",
"length": 400,
"cwidth": 18,
"uitype": "text",
"rows": 4,
"datatype": "str",
"label": "\u51fd\u6570\u540d"
}
]
},
"page_rows":160,
"cache_limit":5
}
}

View File

@ -0,0 +1,22 @@
ns = params_kw.copy()
db = DBPools()
async with db.sqlorContext('sage') as sor:
r = await sor.U('userapikey', ns)
print('update success');
return {
"widgettype":"Message",
"options":{
"title":"Update Success",
"message":"ok"
}
}
print('update failed');
return {
"widgettype":"Error",
"options":{
"title":"Update Error",
"message":"failed"
}
}

View File

@ -32,16 +32,6 @@
"label":"我的角色", "label":"我的角色",
"url":"{{entire_url('user/myrole.ui')}}" "url":"{{entire_url('user/myrole.ui')}}"
}, },
{
"name":"editprofile",
"label":"完善信息",
"url":"{{entire_url('user/edit_profile.dspy')}}",
"popup_options":{
"title":"完善个人信息",
"cwidth":22,
"height":"75%"
}
},
{ {
"name":"logout", "name":"logout",
"label":"签退", "label":"签退",

View File

@ -1,8 +0,0 @@
if not params_kw.get('id'):
return {"widgettype":"Error","options":{"title":"Error","message":"no user selected","cwidth":16,"cheight":9,"timeout":3}}
dbname = get_module_dbname('rbac')
db = DBPools()
async with db.sqlorContext(dbname) as sor:
await sor.U('users', {'id': params_kw.id, 'user_status': '1'})
return {"widgettype":"Message","options":{"title":"Success","message":"user disabled","cwidth":16,"cheight":9,"timeout":3}}

View File

@ -1,8 +0,0 @@
if not params_kw.get('id'):
return {"widgettype":"Error","options":{"title":"Error","message":"no user selected","cwidth":16,"cheight":9,"timeout":3}}
dbname = get_module_dbname('rbac')
db = DBPools()
async with db.sqlorContext(dbname) as sor:
await sor.U('users', {'id': params_kw.id, 'user_status': '0', 'login_fail_count': 0})
return {"widgettype":"Message","options":{"title":"Success","message":"user enabled","cwidth":16,"cheight":9,"timeout":3}}