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":{}
},
"edit_exclouded_fields":[],
"parentField":"parentid"
},
"subtables":[
{
"field":"permid",
"title":"权限角色",
"subtable":"rolepermission"
}
]
"parentField":"parentid",
"toolbar":{
},
"binds":[
]
}
}

View File

@ -9,34 +9,19 @@
"exclouded": ["id", "password", "orgid", "nick_name" ],
"cwidth": {}
},
"editexclouded": ["id", "nick_name", "orgid", "last_login_fail", "last_login", "sync_from", "login_fail_count", "created_at"],
"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
}
}
"editexclouded": [
"id", "nick_name", "orgid"
],
"subtables": [
{
"field":"userid",
"title":"用户角色",
"subtable":"userrole"
},
{
"field":"userid",
"title":"APIKEY",
"subtable":"userapp"
}
]
}

View File

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

View File

@ -119,13 +119,7 @@ async def register_user(sor, ns):
ns.login_fail_count = 0
ns1 = DictObject(id=id, orgname=ns.username)
await create_org(sor, ns1)
roles = [
{
'orgtypeid': 'customer',
'roles': ['customer', 'admin']
}
]
await create_user(sor, ns, roles)
await create_user(sor, ns)
return {
"status": "ok",
"data": {
@ -158,11 +152,6 @@ async def checkUserPassword(request, username, password):
return False
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
last_fail = getattr(user, 'last_login_fail', None)
@ -214,11 +203,6 @@ async def basic_auth(sor, request):
return None
# Check lockout in Python layer (DB-agnostic)
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
last_fail = getattr(user, 'last_login_fail', None)
if _is_locked(fail_count, last_fail):

View File

@ -1,9 +1,15 @@
from ahserver.auth_api import AuthAPI
from ahserver.serverenv import ServerEnv
from sqlor.dbpools import DBPools
from .orgs import (
get_platform_providers
)
from .userperm import UserPermissions
from .user_stats import get_user_stats
from .rbac_tools import (
query_path_roles,
scan_unauth_files
)
from rbac.check_perm import (
objcheckperm,
get_org_users,
@ -19,66 +25,8 @@ from rbac.set_role_perms import (
set_role_perm,
set_role_perms
)
from sqlor.dbpools import DBPools
def _get_rbac_dbname():
env = ServerEnv()
return env.get_module_dbname('rbac')
async def on_rbac_role_event(data):
"""role 表变更后,全量失效 rp_caches"""
up = 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)
from appPublic.log import debug
from ahserver.cache_sync import get_cache_sync
async def get_owner_orgid(*args, **kw):
return '0'
@ -86,6 +34,58 @@ async def get_owner_orgid(*args, **kw):
async def sor_get_owner_orgid(sor, orgid):
return '0'
def _bind_rbac_events(dbpools, dbname, up):
"""Bind database events to RBAC cache invalidation handlers.
Events are dispatched by sqlor after C/U/D operations.
Format: {dbname}:{tablename}:{c|u|d}:after
"""
bindings = [
# users table: invalidate specific user cache on C/U/D
(f'{dbname}.users:c:after', up.on_user_create),
(f'{dbname}.users:u:after', up.on_user_update),
(f'{dbname}.users:d:after', up.on_user_delete),
# rolepermission table: invalidate role-permission cache on any change
(f'{dbname}.rolepermission:c:after', up.on_rolepermission_change),
(f'{dbname}.rolepermission:u:after', up.on_rolepermission_change),
(f'{dbname}.rolepermission:d:after', up.on_rolepermission_change),
# permission table: invalidate role-permission cache on update
(f'{dbname}.permission:u:after', up.on_permission_change),
# role table: invalidate ALL caches (affects all users)
(f'{dbname}.role:c:after', up.on_role_change),
(f'{dbname}.role:u:after', up.on_role_change),
(f'{dbname}.role:d:after', up.on_role_change),
# userrole table: invalidate specific user cache based on userid
(f'{dbname}.userrole:c:after', up.on_userrole_change),
(f'{dbname}.userrole:u:after', up.on_userrole_change),
(f'{dbname}.userrole:d:after', up.on_userrole_change),
]
for event_name, handler in bindings:
dbpools.bind(event_name, handler)
debug(f'RBAC event bound: {event_name}')
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():
AuthAPI.checkUserPermission = objcheckperm
env = ServerEnv()
@ -103,11 +103,19 @@ def load_rbac():
env.sor_get_org_users = sor_get_org_users
env.get_owner_orgid = get_owner_orgid
env.sor_add_user_roles = sor_add_user_roles
env.get_user_stats = get_user_stats
env.query_path_roles = query_path_roles
env.scan_unauth_files = scan_unauth_files
# Cache invalidation methods for use after role/permission changes
env.invalidate_user_perm_cache = env.userpermissions.invalidate_user_cache
env.invalidate_all_perm_caches = env.userpermissions.invalidate_all_user_caches
env.invalidate_role_perm_cache = env.userpermissions.invalidate_rp_cache
# Bind hot_reload event — instance method, WeakMethod safe (stored on env)
if hasattr(env, 'event_dispatcher'):
env.event_dispatcher.bind('hot_reload', env.userpermissions.on_hot_reload)
register_rbac_event_listeners()
# Bind database events for automatic cache invalidation
dbpools = DBPools()
dbname = env.get_module_dbname('rbac')
if dbname:
_bind_rbac_events(dbpools, dbname, env.userpermissions)
debug(f'RBAC event listeners bound for database: {dbname}')
else:
debug('RBAC event listeners skipped: no database configured for rbac module')

View File

@ -114,6 +114,31 @@ async def set_role_perms(dbname, module, orgtype, role, items):
for tblname in items:
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__':
async def main():
if len(sys.argv) < 6:
@ -124,6 +149,8 @@ if __name__ == '__main__':
orgtype = sys.argv[3]
role = sys.argv[4]
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):
p = '.'

View File

@ -4,20 +4,9 @@ from sqlor.dbpools import get_sor_context
from ahserver.serverenv import ServerEnv
from appPublic.Singleton import SingletonDecorator
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:
"""Async-safe LRU cache with TTL support.
@ -38,8 +27,6 @@ class LRUCache:
def get(self, key):
import time
if not _cache_enabled('rbac'):
return None
if key not in self._cache:
return None
value, expire_at = self._cache[key]
@ -51,8 +38,6 @@ class LRUCache:
def set(self, key, value):
import time
if not _cache_enabled('rbac'):
return
if key in self._cache:
self._cache.move_to_end(key)
self._cache[key] = (value, time.time() + self.ttl)
@ -98,89 +83,82 @@ class UserPermissions:
# Async lock for rp_caches initialization (lazy init)
self._rp_lock = None
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):
async def on_user_update(self, data):
"""Event handler for users table update.
Clears the specific user's permission cache.
"""
try:
userid = getattr(data, 'id', None)
if userid:
self.invalidate_user_cache(userid)
await self.invalidate_user_cache(userid)
debug(f'RBAC cache invalidated for user id={userid} (users update)')
except Exception as 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.
Clears the specific user's permission cache.
"""
try:
userid = getattr(data, 'id', None)
if userid:
self.invalidate_user_cache(userid)
await self.invalidate_user_cache(userid)
debug(f'RBAC cache invalidated for user id={userid} (users create)')
except Exception as 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.
Clears the specific user's permission cache.
"""
try:
userid = getattr(data, 'id', None)
if userid:
self.invalidate_user_cache(userid)
await self.invalidate_user_cache(userid)
debug(f'RBAC cache invalidated for user id={userid} (users delete)')
except Exception as 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.
Clears the role-permission cache.
"""
try:
self.invalidate_rp_cache()
await self.invalidate_rp_cache()
debug('RBAC role-permission cache invalidated (rolepermission change)')
except Exception as 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.
Clears the role-permission cache.
"""
try:
self.invalidate_rp_cache()
await self.invalidate_rp_cache()
debug('RBAC role-permission cache invalidated (permission change)')
except Exception as 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.
Clears all user caches and role-permission cache,
since role changes may affect any user.
"""
try:
self.invalidate_all_user_caches()
self.invalidate_rp_cache()
await self.invalidate_all_user_caches()
await self.invalidate_rp_cache()
debug('RBAC all caches invalidated (role change)')
except Exception as 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.
Clears the specific user's permission cache based on userid.
"""
try:
userid = getattr(data, 'userid', None)
if userid:
self.invalidate_user_cache(userid)
await self.invalidate_user_cache(userid)
debug(f'RBAC cache invalidated for user id={userid} (userrole change)')
except Exception as e:
error(f'RBAC on_userrole_change handler error: {e}')
@ -200,28 +178,41 @@ class UserPermissions:
return roles
async with get_sor_context(ServerEnv(), 'rbac') as sor:
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
await self.get_userroles(sor, 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.
Call this after role changes, user creation, etc.
Also broadcasts invalidation to all other processes via Redis Pub/Sub.
"""
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):
"""Invalidate all user role caches."""
async def invalidate_all_user_caches(self):
"""Invalidate all user role caches.
Also broadcasts invalidation to all other processes via Redis Pub/Sub.
"""
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):
"""Invalidate role-permission cache (after permission changes)."""
async def invalidate_rp_cache(self):
"""Invalidate role-permission cache (after permission changes).
Also broadcasts invalidation to all other processes via Redis Pub/Sub.
"""
self.rp_caches = None
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):
"""Load all role-permission mappings into cache.
@ -235,28 +226,22 @@ class UserPermissions:
now = time.time()
# 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
# Slow path: acquire lock and double-check
async with self._get_rp_lock():
# 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
# Build in local dict first, assign atomically when complete.
# Otherwise other coroutines see {} during the await and get 403.
new_caches = {}
self.rp_caches = {}
sql_all = """select c.id, c.orgtypeid, c.name, b.path
from rolepermission a, permission b, role c
where a.permid = b.id
and c.id = a.roleid
order by c.orgtypeid, c.name"""
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:
if r.id == 'anonymous':
k = 'anonymous'
@ -266,17 +251,13 @@ order by c.orgtypeid, c.name"""
k = 'logined'
else:
k = f'{r.orgtypeid}.{r.name}'
arr = new_caches.get(k, [])
arr = self.rp_caches.get(k, [])
arr.append(r.path)
new_caches[k] = arr
# Atomic swap: other coroutines see old cache or fully-loaded new cache, never {}
self.rp_caches = new_caches
self.rp_caches[k] = arr
self.rp_cache_loaded_at = now
async def get_userroles(self, sor, userid):
"""Load user roles from database and cache them.
Returns the roles list directly (needed when cache is disabled).
"""
"""Load user roles from database and cache them."""
recs = await sor.sqlExe('''select b.id, b.orgtypeid, b.name
from users a, role b, userrole c
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}.*')
roles.append(f'*.{r.name}')
roles = sorted(list(set(roles)))
self.ur_caches.set(userid, roles)
return roles
self.ur_caches.set(userid, sorted(list(set(roles))))
def check_roles_path(self, roles, path):
"""Check if any of the roles has access to the given path.
Supports:
- Exact match: '/customer_management/index.ui' or '/main/login.ui'
- Wildcard prefix match: '/customer_management/**' or '/customer_management/%'
matches any path starting with '/customer_management/'
- Wildcard prefix match: '/customer_management/**' matches any path starting with '/customer_management/'
- Path normalization: tries both the raw path and path with /main stripped
"""
for role in roles:
@ -314,22 +292,16 @@ where a.id = c.userid
return True
# Also try wildcard match with normalized path
for perm_path in paths:
prefix = None
if perm_path.endswith('**'):
prefix = perm_path[:-2]
elif perm_path.endswith('%'):
prefix = perm_path[:-1]
if prefix and (normalized.startswith(prefix) or path.startswith(prefix)):
return True
if normalized.startswith(prefix) or path.startswith(prefix):
return True
# Wildcard prefix match with raw path
for perm_path in paths:
prefix = None
if perm_path.endswith('**'):
prefix = perm_path[:-2]
elif perm_path.endswith('%'):
prefix = perm_path[:-1]
if prefix and path.startswith(prefix):
return True
if path.startswith(prefix):
return True
return False
async def is_user_has_path_perm(self, userid, path):
@ -344,19 +316,13 @@ where a.id = c.userid
if userid is None:
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()
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)
if not roles:
roles = await self.get_userroles(sor, 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
await self.get_userroles(sor, userid)
roles = self.ur_caches.get(userid)
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实例生成验证码参数手机号
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}"
}
}
xx = await sms_engine.generate_sms_code(phone)
if xx is None:
return {
"status": "error",
"data": {
"message": "发送验证码出错请检查短信模板配置和百度API连接"
"message": "发送验证码出错"
}
}
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",
"widgettype": "PopupWindow",
"options": {
"title": "欢迎登录",
"i18n": true,
"css": "login-window",
"title": "登录/注册",
"auto_open": true,
"archor": "cc",
"cwidth": 26,
"cheight": 30
"anthor": "cc",
"cwidth": 22,
"cheight": 19
},
"subwidgets": [
{
"widgettype": "TabPanel",
"id": "login_tabs",
"options": {
"tab_wide": "auto",
"height": "100%",
@ -25,159 +19,90 @@
"items": [
{
"name": "userpasswd",
"label": "密码登录",
"label": "用户密码",
"content": {
"widgettype": "VScrollPanel",
"options": {"height": "100%"},
"subwidgets": [
{
"widgettype": "Form",
"options": {
"cols": 1,
"fields": [
{"name": "username", "label": "用户名", "uitype": "str"},
{"name": "password", "label": "密码", "uitype": "password"}
]
"widgettype": "Form",
"options": {
"cols": 1,
"fields": [
{
"name": "username",
"label": "用户名",
"uitype": "str"
},
"binds": [
{
"wid": "self",
"event": "submit",
"actiontype": "urlwidget",
"target": "self",
"options": {
"method": "POST",
"url": "{{entire_url('up_login.dspy')}}"
}
}
]
}
]
}
},
{
"name": "phonecode",
"label": "手机登录",
"content": {
"widgettype": "VScrollPanel",
"options": {"height": "100%"},
"subwidgets": [
{
"name": "password",
"label": "密码",
"uitype": "password"
}
]
},
"binds": [
{
"widgettype": "VBox",
"options": {"gap": "8px"},
"subwidgets": [
{
"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",
"wid": "self",
"event": "submit",
"actiontype": "urlwidget",
"target": "self",
"options": {
"otext": "手机号需短信验证后方可注册",
"i18n": true,
"css": "login-desc"
"method": "POST",
"url": "{{entire_url('up_login.dspy')}}"
}
}
]
}
},
{
"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",
"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}}"
}
}
]
}
]
},
{
"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('发送验证码')})"
}
]
"wid": "self",
"event": "submit",
"actiontype": "urlwidget",
"target": "self",
"options": {
"url": "{{entire_url('code_login.dspy')}}"
}
}
]
}

View File

@ -1,38 +1,9 @@
await forget_user()
return {
"widgettype": "VBox",
"options": {
"padding": "24px",
"alignItems": "center"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"otext": "logout success",
"i18n": True,
"fontSize": "16px",
"marginBottom": "16px"
}
},
{
"widgettype": "Button",
"options": {
"label": "刷新页面",
"bgcolor": "#3B82F6",
"color": "white",
"padding": "8px 24px",
"borderRadius": "6px"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "script",
"target": "self",
"script": "location.reload()"
}
]
}
]
"widgettype":"Text",
"options":{
"otext":"logout success",
"i18n":True,
}
}

View File

@ -1,69 +1,18 @@
debug(f'register.dspy: {params_kw=}')
debug(f'{params_kw=}')
db = DBPools()
dbname = get_module_dbname('rbac')
async with db.sqlorContext(dbname) as sor:
data = await register_user(sor, params_kw)
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:
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)
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": "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": "系统错误,请稍后重试"
}
}
return UiMessage(title="Success", message=f"register success {orgid}")
return UiError(title='Error', message="register failed")

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