Compare commits

...

54 Commits

Author SHA1 Message Date
Hermes Agent
600fddced3 feat: add i18n translations (zh/en/jp/ko) for all modules 2026-06-19 15:01:37 +08:00
Hermes Agent
2ea45cd380 rbac: 完善信息弹窗修复双窗口+添加标题 2026-06-18 11:09:46 +08:00
Hermes Agent
947638853f rbac: 完善信息Form使用value回填用户数据 2026-06-18 10:48:17 +08:00
Hermes Agent
cd29b276f7 fix: add login.css to any permissions 2026-06-15 13:55:09 +08:00
18251e3eae fix: add favicon.ico and i18n_getmsgs to any permissions 2026-06-11 23:54:17 +08:00
d1b81c9a9f fix: remove Sage dependency from load_path.py, use standard pattern 2026-06-11 23:41:10 +08:00
52cd71f861 feat: add user_status check on login, enable/disable toolbar, fix editexclouded for add user 2026-06-11 16:57:04 +08:00
cb9f8bbb4b bugfix 2026-06-10 11:16:23 +08:00
12663a37e0 bugfix 2026-06-10 11:08:33 +08:00
f6027788b2 fix: login redirect to index.ui instead of userinfo.ui
After login success, redirect to the main page (/index.ui)
instead of just loading userinfo.ui into user_container.
This fixes the white screen after login.
2026-06-09 23:18:39 +08:00
587241b6a9 bugfix 2026-06-09 17:01:27 +08:00
62e1985ecb bugfix 2026-06-09 16:58:15 +08:00
df212ea40d bugfix 2026-06-09 16:52:12 +08:00
e453883b61 chore: exclude last_login_fail, last_login, sync_from from users edit form 2026-06-05 11:14:14 +08:00
002226d17c fix: change created_at type from timestamp to date for proper Date control 2026-06-04 16:50:20 +08:00
5809a81646 debug: add hot_reload handler logging 2026-06-01 22:53:07 +08:00
b72a487df7 fix: use ServerEnv().userpermissions instead of new UserPermissions() in event handlers 2026-06-01 18:15:37 +08:00
ce7eb2a193 refactor: bind hot_reload event via EventDispatcher, add on_hot_reload to UserPermissions 2026-06-01 18:10:29 +08:00
c949a51f2e fix: gen_sms_code.dspy handle both None return and exception from generate_sms_code 2026-05-31 12:26:55 +08:00
4038b7d0b9 fix: gen_sms_code.dspy catch exception and return actual error message 2026-05-31 11:56:46 +08:00
7f2c3d25dd feat: 用户菜单添加完善信息功能 - 新增 edit_profile.dspy/save_profile.dspy - usermenu.ui 添加完善信息入口 - load_path.py 注册权限路径 2026-05-31 10:52:13 +08:00
26d1fd1447 feat: register_user 添加 customer.admin 角色,注册时同时分配 customer 和 admin 权限 2026-05-31 10:38:32 +08:00
c9dd80a484 fix: 暗色主题输入框颜色全局优化
- bricks.css: inputbox背景 #1E293B → #0F172A
- login.css: 同步更新保持一致
2026-05-30 14:32:11 +08:00
b0d6b59613 fix: 注册表单仅输入区域滚动,暗色主题输入框颜色优化
- 注册Tab结构: Text(固定) + VScrollPanel(仅Form) + Button(固定)
- 暗色主题输入框背景 #0f172a -> #1e293b,更协调
2026-05-30 14:27:33 +08:00
d1b3ca0914 fix: 修复登录弹窗三个显示问题
- 注册表单超出无滚动: 三个Tab内容均包裹VScrollPanel(scrollpanel)
- Form toolbar不可见: 滚动容器让submit/reset按钮可达
- 文本未国际化: title/label/otext均加i18n:true, archor纠正
- CSS: tabpanel flex布局, tabpanel-content flex:1+min-height:0
- PopupWindow cheight 28->30 给更多显示空间
2026-05-30 14:16:00 +08:00
36569c0e41 feat: 现代化登录/注册界面改造
- login.css: 全新现代化样式,支持亮/暗主题
- login.ui: 三Tab布局(密码登录/手机登录/注册),手机登录支持短信验证码
- sms_register.dspy: 短信验证注册后端,验证通过后自动注册并登录
- load_path.py: 添加 sms_register.dspy 到 any 权限
- 修复手机登录 setValue 调用 (上一轮已提交)
- 注册流程: 手机号+短信验证码+用户名+密码,短信验证通过后才允许注册
2026-05-30 14:08:11 +08:00
019e9702fc fix: use setValue() on codeid field widget instead of direct value assignment 2026-05-30 13:36:54 +08:00
cec79caf88 fix: phone login form.setValue is not a function - use name_inputs direct value set 2026-05-30 13:35:07 +08:00
b532548d19 fix: keep previous rp_caches when DB returns empty result
sqlExe can return [] without raising an exception (bad connection,
cursor issue). When load_roleperms gets 0 records but had valid
cache before, keep the old cache instead of replacing with {}.
Prevents intermittent 403 from transient DB issues.
2026-05-30 11:34:13 +08:00
fbbe011a8d fix: rp_caches race condition causing intermittent 403
load_roleperms() was setting self.rp_caches = {} before the async
DB query. During the await, other coroutines saw {} (not None),
skipped the load, and checked permissions against an empty dict,
causing intermittent 403 on random paths.

Fix: build in local dict first, assign atomically when complete.
2026-05-30 11:19:32 +08:00
c776c0b3b5 fix: rp_cache should reload from DB when cache is disabled 2026-05-30 10:19:00 +08:00
04e9b718db fix: check_roles_path supports % wildcard alongside **
load_path.py scripts across modules register paths like '/module/api/%'
using SQL LIKE wildcard, but check_roles_path() only recognized '**' as
wildcard suffix. This caused all %-terminated paths to be treated as
exact matches, resulting in 403 for any sub-path.

Now both '/module/api/%' and '/module/api/**' work as prefix wildcards.
2026-05-29 23:12:22 +08:00
67687883ff fix: RBAC crash when cache disabled - return roles directly from get_userroles
When module_cache.rbac=false in config.json, LRUCache.get() always returns
None and LRUCache.set() is a no-op. This caused get_userroles() to store
roles into a disabled cache, then callers read back None, leading to
TypeError in check_roles_path() when iterating over None.

Fix: get_userroles() now returns the roles list directly. Callers use the
return value instead of relying solely on cache reads. Added safety
fallback to deny access if roles is somehow still None.
2026-05-29 22:43:39 +08:00
fa9f7f5146 fix: wrap Tabular in VBox with cheight for proper scrolling 2026-05-29 22:09:56 +08:00
cf18e592c7 feat: respect module_cache config for RBAC LRU cache 2026-05-29 17:59:04 +08:00
342fc6652a refactor: rewrite SMS login UI using pure bricks patterns
- login.ui: use actiontype:urlwidget for Form submit → code_login.dspy
- Removed custom JavaScript (phone_login.js)
- Send code button: minimal script action to fetch and set form value
- Login flow: bricks returns Message/Error/VBox widgets directly
- Multi-account selection: code_login.dspy returns VBox with Buttons
2026-05-29 11:58:30 +08:00
8528eebf10 revert: restore gen_sms_code.dspy (API used by other systems) 2026-05-29 11:38:35 +08:00
e65fddbe61 revert: restore phone_login.dspy to original (API used by other systems) 2026-05-29 11:37:39 +08:00
cf186a632b fix: revert uitype to 'hide' (bricks framework uses 'hide' not 'hidden') 2026-05-29 11:36:05 +08:00
567513789e feat: rewrite SMS login UI with fetch-based flow matching phone_login.dspy API
- login.ui: SMS tab now uses fetch for gen_sms_code.dspy and phone_login.dspy
- Added _webbricks_=1 to fetch URLs (prevents HTML wrapping)
- Added 60s countdown timer on send-code button
- Added multi-account selection UI (status=choose response)
- Fixed uitype 'hide' -> 'hidden' for codeid field
- Dispatches user_logined event after successful phone login
- gen_sms_code.dspy: improved error message for SMS service config issues
- phone_login.dspy: added mark_used parameter for multi-account flow
- phone_login.js: sageSelectAccount handler for account selection
2026-05-29 11:31:39 +08:00
cfd3810a0a fix: remove _webbricks_=1 from SMS code generation API call
The _webbricks_=1 parameter was causing the response to be wrapped as a widget instead of plain JSON, which prevented the frontend from correctly parsing the response and setting the codeid field.

Now the API returns plain JSON {status: 'ok', data: {key: '...'}} and the form's hidden codeid field gets properly set after successful SMS generation.
2026-05-28 18:39:25 +08:00
969a72b528 fix: SMS verification code URL - replace undefined bricks.app.baseUrl with entire_url template
The send verification code button was sending requests to https://token.opencomputing.cn/undefined/rbac/gen_sms_code.dspy because bricks.app.baseUrl doesn't exist in the bricks framework (it uses baseURI on widgets).

Fix: Use Jinja2 entire_url() template function like all other URLs in the file.
2026-05-28 18:24:39 +08:00
ff03efb50a fix: Button label更新改用set_otext()支持国际化
set_text()直接设置innerHTML跳过i18n翻译,
set_otext()会先调用i18n._()翻译再set_text,
与Button初始化时i18n:true的配置保持一致。
2026-05-28 17:00:57 +08:00
f44104b8a5 fix: Button label更新使用text_w.set_text()替代options.label赋值
根因:
1. script上下文中this为undefined,无法通过this.options.label访问
2. btn.options.label只修改属性不更新DOM,需调用text_w.set_text()
3. Button内部label是bricks.Text子组件(text_w),需通过其API更新
2026-05-28 16:52:26 +08:00
53111aa2fd fix: Form.getValue()返回FormData对象,改用_getValue()获取普通对象
根因:bricks Form的getValue()内部调用get_formdata()返回浏览器
原生FormData对象,不支持.cell_no属性访问。
改用_getValue()返回{name:value}普通JS对象。
2026-05-28 16:27:56 +08:00
92627c9c96 fix: 手机验证码tab改用VBox+独立Button,避免toolbar tool触发submit导致tab跳转
根因:Form toolbar tool点击会同时触发Form的submit事件,
submit的urlwidget替换Form后TabPanel重置到第一个tab。
改为VBox包裹Form和独立Button,Button通过bricks.getWidgetById
读取Form值,fetch加_webbricks_=1确保返回纯JSON。
2026-05-28 15:03:50 +08:00
7200ee43a0 fix: 手机验证码gen_code按钮用datawidget/datamethod传表单值给script
原script中this.getValue()在toolbar tool事件上下文中拿不到表单数据,
改用datawidget/datamethod机制将表单值通过params传入script。
回写codeid用.bind(this)保持Form引用。
2026-05-28 14:10:37 +08:00
de21b9fd38 feat: 手机验证码登录对接 + 注册tab + user_logined事件派发
- 新增 code_login.dspy: 接收前端表单(cell_no/codeid/check_code)
  映射到sms_engine验证,返回UI widget含自动登录binds
- 修复 login.ui 手机验证码tab: gen_code按钮改用script调用
  gen_sms_code.dspy并回写key到隐藏字段,submit指向code_login.dspy
- login.ui 新增注册tab: 用户名/手机号/密码/确认密码表单
- register.dspy: 注册成功后自动remember_user并返回含binds的
  Message widget(加载userinfo、销毁登录窗、派发user_logined)
- up_login.dspy: 补充user_logined事件派发bind
- load_path.py: code_login.dspy加入any权限,gen_sms_code.dspy
  从logined移至any(验证码发送在登录前)
2026-05-28 13:50:17 +08:00
54b0f3d7b6 fix: dispatch user_logined event after successful login
After login message is dismissed, dispatches 'user_logined' on bricks.app
so the sidebar menu reloads without requiring manual page reload.
2026-05-28 13:42:03 +08:00
9d2a94131a 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.
2026-05-27 17:58:15 +08:00
0a5bfa4c64 feat: add load_path.py RBAC permission registration script 2026-05-27 13:16:09 +08:00
39f8eb7d94 Revert "feat: add cross-process cache invalidation via Redis Pub/Sub"
This reverts commit 8cec17c04295665eb4b750e2070c17fa3b06a939.
2026-05-26 18:31:04 +08:00
8fdb31a850 Revert "fix: add app parameter to start_cache_sync for aiohttp on_startup hook"
This reverts commit c0bbe63845e1f5ca255a0e2fe821fcf4f88786aa.
2026-05-26 18:31:04 +08:00
c0bbe63845 fix: add app parameter to start_cache_sync for aiohttp on_startup hook 2026-05-26 17:20:52 +08:00
31 changed files with 1966 additions and 766 deletions

133
i18n/en/msg.txt Normal file
View File

@ -0,0 +1,133 @@
用户管理: 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

133
i18n/jp/msg.txt Normal file
View File

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

133
i18n/ko/msg.txt Normal file
View File

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

139
i18n/zh/msg.txt Normal file
View File

@ -0,0 +1,139 @@
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,11 +11,15 @@
"alters":{} "alters":{}
}, },
"edit_exclouded_fields":[], "edit_exclouded_fields":[],
"parentField":"parentid", "parentField":"parentid"
"toolbar":{ },
}, "subtables":[
"binds":[ {
] "field":"permid",
} "title":"权限角色",
"subtable":"rolepermission"
}
]
} }

View File

@ -9,19 +9,34 @@
"exclouded": ["id", "password", "orgid", "nick_name" ], "exclouded": ["id", "password", "orgid", "nick_name" ],
"cwidth": {} "cwidth": {}
}, },
"editexclouded": [ "editexclouded": ["id", "nick_name", "orgid", "last_login_fail", "last_login", "sync_from", "login_fail_count", "created_at"],
"id", "nick_name", "orgid" "record_toolbar": [
{
"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": "timestamp" "type": "date"
}, },
{ {
"name": "last_login", "name": "last_login",

View File

@ -119,7 +119,13 @@ 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)
await create_user(sor, ns) roles = [
{
'orgtypeid': 'customer',
'roles': ['customer', 'admin']
}
]
await create_user(sor, ns, roles)
return { return {
"status": "ok", "status": "ok",
"data": { "data": {
@ -152,6 +158,11 @@ 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)
@ -203,6 +214,11 @@ 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,15 +1,9 @@
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,
@ -25,8 +19,66 @@ from rbac.set_role_perms import (
set_role_perm, set_role_perm,
set_role_perms set_role_perms
) )
from appPublic.log import debug from sqlor.dbpools import DBPools
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'
@ -34,58 +86,6 @@ 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,19 +103,11 @@ 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)
# Bind database events for automatic cache invalidation if hasattr(env, 'event_dispatcher'):
dbpools = DBPools() env.event_dispatcher.bind('hot_reload', env.userpermissions.on_hot_reload)
dbname = env.get_module_dbname('rbac') register_rbac_event_listeners()
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,31 +114,6 @@ 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:
@ -149,8 +124,6 @@ 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,9 +4,20 @@ 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 ahserver.cache_sync import get_cache_sync from appPublic.jsonConfig import getConfig
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.
@ -27,6 +38,8 @@ 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]
@ -38,6 +51,8 @@ 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)
@ -83,82 +98,89 @@ 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
async def on_user_update(self, data): def on_hot_reload(self, data=None):
"""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:
await self.invalidate_user_cache(userid) 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}')
async def on_user_create(self, data): 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:
await self.invalidate_user_cache(userid) 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}')
async def on_user_delete(self, data): 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:
await self.invalidate_user_cache(userid) 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}')
async def on_rolepermission_change(self, data): 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:
await self.invalidate_rp_cache() 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}')
async def on_permission_change(self, data): 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:
await self.invalidate_rp_cache() 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}')
async def on_role_change(self, data): 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:
await self.invalidate_all_user_caches() self.invalidate_all_user_caches()
await self.invalidate_rp_cache() 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}')
async def on_userrole_change(self, data): 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:
await self.invalidate_user_cache(userid) 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}')
@ -178,41 +200,28 @@ class UserPermissions:
return roles return roles
async with get_sor_context(ServerEnv(), 'rbac') as sor: async with get_sor_context(ServerEnv(), 'rbac') as sor:
await self.get_userroles(sor, userid) roles = 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 None return ['any', 'logined']
async def invalidate_user_cache(self, userid): 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}')
async def invalidate_all_user_caches(self): 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')
async def invalidate_rp_cache(self): 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.
@ -226,22 +235,28 @@ class UserPermissions:
now = time.time() now = time.time()
# Fast path: cache valid, no lock needed # Fast path: cache valid, no lock needed
if self.rp_caches is not None and (now - self.rp_cache_loaded_at) < self.rp_cache_ttl: if _cache_enabled('rbac') and 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 self.rp_caches is not None and (now - self.rp_cache_loaded_at) < self.rp_cache_ttl: if _cache_enabled('rbac') and self.rp_caches is not None and (now - self.rp_cache_loaded_at) < self.rp_cache_ttl:
return return
self.rp_caches = {} # Build in local dict first, assign atomically when complete.
# 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'
@ -251,13 +266,17 @@ 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 = self.rp_caches.get(k, []) arr = new_caches.get(k, [])
arr.append(r.path) arr.append(r.path)
self.rp_caches[k] = arr new_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
@ -268,14 +287,17 @@ 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}')
self.ur_caches.set(userid, sorted(list(set(roles)))) roles = 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/**' matches any path starting with '/customer_management/' - Wildcard prefix match: '/customer_management/**' or '/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:
@ -292,16 +314,22 @@ 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]
if normalized.startswith(prefix) or path.startswith(prefix): elif perm_path.endswith('%'):
return True prefix = perm_path[:-1]
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]
if path.startswith(prefix): elif perm_path.endswith('%'):
return True prefix = perm_path[:-1]
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):
@ -316,13 +344,19 @@ where a.id = c.userid
if userid is None: if userid is None:
roles = ['any', 'anonymous'] roles = ['any', 'anonymous']
if self.rp_caches is None or not roles: if not _cache_enabled('rbac') or 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 self.rp_caches is None: if not _cache_enabled('rbac') or self.rp_caches is None:
await self.load_roleperms(sor) await self.load_roleperms(sor)
if not roles: if not roles:
await self.get_userroles(sor, userid) roles = await self.get_userroles(sor, userid)
roles = self.ur_caches.get(userid) # When cache is enabled, fall back to cache read
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)

96
scripts/load_path.py Normal file
View File

@ -0,0 +1,96 @@
#!/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,12 +7,22 @@ if phone is None:
} }
} }
# 使用短信模块发布的sms_engine实例生成验证码参数手机号 # 使用短信模块发布的sms_engine实例生成验证码参数手机号
xx = await sms_engine.generate_sms_code(phone) try:
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": "发送验证码出错" "message": "发送验证码出错请检查短信模板配置和百度API连接"
} }
} }
id, code = xx id, code = xx

View File

@ -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": "<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"
}
}
]
}

242
wwwroot/login.css Normal file
View File

@ -0,0 +1,242 @@
/* ===== 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

@ -0,0 +1,257 @@
# 手机验证码登录 - 接收前端表单参数(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

@ -0,0 +1,79 @@
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,16 +1,22 @@
{% 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,
"anthor": "cc", "archor": "cc",
"cwidth": 22, "cwidth": 26,
"cheight": 19 "cheight": 30
}, },
"subwidgets": [ "subwidgets": [
{ {
"widgettype": "TabPanel", "widgettype": "TabPanel",
"id": "login_tabs",
"options": { "options": {
"tab_wide": "auto", "tab_wide": "auto",
"height": "100%", "height": "100%",
@ -19,90 +25,159 @@
"items": [ "items": [
{ {
"name": "userpasswd", "name": "userpasswd",
"label": "用户密码", "label": "密码登录",
"content": { "content": {
"widgettype": "Form", "widgettype": "VScrollPanel",
"options": { "options": {"height": "100%"},
"cols": 1, "subwidgets": [
"fields": [
{
"name": "username",
"label": "用户名",
"uitype": "str"
},
{
"name": "password",
"label": "密码",
"uitype": "password"
}
]
},
"binds": [
{ {
"wid": "self", "widgettype": "Form",
"event": "submit",
"actiontype": "urlwidget",
"target": "self",
"options": { "options": {
"method": "POST", "cols": 1,
"url": "{{entire_url('up_login.dspy')}}" "fields": [
} {"name": "username", "label": "用户名", "uitype": "str"},
{"name": "password", "label": "密码", "uitype": "password"}
]
},
"binds": [
{
"wid": "self",
"event": "submit",
"actiontype": "urlwidget",
"target": "self",
"options": {
"method": "POST",
"url": "{{entire_url('up_login.dspy')}}"
}
}
]
} }
] ]
} }
}, },
{ {
"name": "checkcode", "name": "phonecode",
"label": "手机验证码", "label": "手机登录",
"content": { "content": {
"widgettype": "Form", "widgettype": "VScrollPanel",
"options": { "options": {"height": "100%"},
"toolbar": { "subwidgets": [
"tools": [ {
"widgettype": "VBox",
"options": {"gap": "8px"},
"subwidgets": [
{ {
"name": "gen_code", "widgettype": "Text",
"label": "发送验证码" "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": {
"otext": "手机号需短信验证后方可注册",
"i18n": true,
"css": "login-desc"
}
},
{
"widgettype": "VScrollPanel",
"options": {"height": "100%", "flex": "1"},
"subwidgets": [
{
"widgettype": "Form",
"id": "register_form",
"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}}"
}
}
]
} }
] ]
}, },
"description": "限中国国内手机",
"fields": [
{
"name": "cell_no",
"label": "手机号",
"uitype": "str"
},
{
"name": "codeid",
"uitype": "hide",
"value": "{{uuid()}}"
},
{
"name": "check_code",
"uitype": "str"
}
]
},
"binds": [
{ {
"wid": "self", "widgettype": "Button",
"event": "gen_code", "id": "reg_sms_btn",
"actiontype": "urlwidget", "options": {"label": "发送验证码", "i18n": true, "css": "sms-send-btn"},
"datawidget": "self", "binds": [
"datamethod": "getValue", {
"target": "self", "wid": "self",
"options": { "event": "click",
"url": "{{entire_url('../gen_sms_code.dspy')}}" "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('发送验证码')})"
{ }
"wid": "self", ]
"event": "submit",
"actiontype": "urlwidget",
"target": "self",
"options": {
"url": "{{entire_url('code_login.dspy')}}"
}
} }
] ]
} }

View File

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

View File

@ -1,18 +1,69 @@
debug(f'{params_kw=}') debug(f'register.dspy: {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}") # 注册成功后自动登录
return UiError(title='Error', message="register failed") 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": "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

@ -0,0 +1,40 @@
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

@ -0,0 +1,187 @@
# 短信验证注册 - 接收前端表单参数(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,4 +1,3 @@
debug(f'{params_kw=}') debug(f'{params_kw=}')
ns = { ns = {
"username":params_kw.username, "username":params_kw.username,
@ -13,6 +12,10 @@ 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,
@ -31,6 +34,10 @@ 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,
@ -65,6 +72,10 @@ 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,
@ -82,33 +93,41 @@ 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.user_container", "target":"window",
"options":{ "options":{
"url":entire_url('/rbac/user/userinfo.ui') "url":entire_url('/index.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

@ -1,25 +0,0 @@
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

@ -1,24 +0,0 @@
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

@ -1,74 +0,0 @@
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

@ -1,126 +0,0 @@
{
"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

@ -1,22 +0,0 @@
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,6 +32,16 @@
"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

@ -0,0 +1,8 @@
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

@ -0,0 +1,8 @@
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}}