Compare commits
No commits in common. "main" and "feat/dataviz-users" have entirely different histories.
main
...
feat/datav
133
i18n/en/msg.txt
133
i18n/en/msg.txt
@ -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
|
||||
133
i18n/jp/msg.txt
133
i18n/jp/msg.txt
@ -1,133 +0,0 @@
|
||||
用户管理: ユーザー管理
|
||||
用户名: ユーザー名
|
||||
密码: パスワード
|
||||
确认密码: パスワード確認
|
||||
姓名: 氏名
|
||||
邮箱: メールアドレス
|
||||
手机号: 電話番号
|
||||
所属机构: 所属組織
|
||||
角色: ロール
|
||||
状态: ステータス
|
||||
启用: 有効
|
||||
停用: 無効
|
||||
新增用户: ユーザー追加
|
||||
编辑用户: ユーザー編集
|
||||
删除用户: ユーザー削除
|
||||
重置密码: パスワードリセット
|
||||
角色管理: ロール管理
|
||||
角色名称: ロール名
|
||||
角色编码: ロールコード
|
||||
角色描述: ロール説明
|
||||
权限: 権限
|
||||
新增角色: ロール追加
|
||||
编辑角色: ロール編集
|
||||
删除角色: ロール削除
|
||||
分配权限: 権限割り当て
|
||||
机构管理: 組織管理
|
||||
机构名称: 組織名
|
||||
机构编码: 組織コード
|
||||
机构类型: 組織タイプ
|
||||
上级机构: 上位組織
|
||||
新增机构: 組織追加
|
||||
编辑机构: 組織編集
|
||||
删除机构: 組織削除
|
||||
菜单管理: メニュー管理
|
||||
菜单名称: メニュー名
|
||||
菜单路径: メニューパス
|
||||
菜单图标: メニューアイコン
|
||||
菜单排序: メニュー並び順
|
||||
上级菜单: 上位メニュー
|
||||
新增菜单: メニュー追加
|
||||
编辑菜单: メニュー編集
|
||||
删除菜单: メニュー削除
|
||||
权限管理: 権限管理
|
||||
权限名称: 権限名
|
||||
权限编码: 権限コード
|
||||
权限类型: 権限タイプ
|
||||
资源权限: リソース権限
|
||||
操作权限: 操作権限
|
||||
数据权限: データ権限
|
||||
登录日志: ログインログ
|
||||
登录时间: ログイン時間
|
||||
登录IP: ログインIP
|
||||
登录状态: ログインステータス
|
||||
成功: 成功
|
||||
失败: 失敗
|
||||
操作日志: 操作ログ
|
||||
操作类型: 操作タイプ
|
||||
操作时间: 操作時間
|
||||
操作人: 操作者
|
||||
操作结果: 操作結果
|
||||
API密钥管理: APIキー管理
|
||||
密钥名称: キー名
|
||||
密钥值: キー値
|
||||
密钥状态: キーステータス
|
||||
新增密钥: キー追加
|
||||
编辑密钥: キー編集
|
||||
删除密钥: キー削除
|
||||
在线用户: オンラインユーザー
|
||||
会话ID: セッションID
|
||||
最后活动时间: 最終アクティビティ時間
|
||||
强制下线: 強制ログアウト
|
||||
安全设置: セキュリティ設定
|
||||
密码策略: パスワードポリシー
|
||||
密码长度: パスワード長さ
|
||||
密码复杂度: パスワード複雑度
|
||||
密码有效期: パスワード有効期限
|
||||
登录失败锁定: ログイン失敗ロック
|
||||
最大失败次数: 最大失敗回数
|
||||
锁定时长: ロック期間
|
||||
双因素认证: 二要素認証
|
||||
启用双因素: 二要素認証を有効化
|
||||
验证码: 認証コード
|
||||
Token管理: トークン管理
|
||||
Token名称: トークン名
|
||||
Token过期时间: トークン有効期限
|
||||
新增Token: トークン追加
|
||||
编辑Token: トークン編集
|
||||
删除Token: トークン削除
|
||||
刷新Token: トークン更新
|
||||
用户组: ユーザーグループ
|
||||
组名称: グループ名
|
||||
组描述: グループ説明
|
||||
新增用户组: ユーザーグループ追加
|
||||
编辑用户组: ユーザーグループ編集
|
||||
删除用户组: ユーザーグループ削除
|
||||
成员管理: メンバー管理
|
||||
添加成员: メンバー追加
|
||||
移除成员: メンバー削除
|
||||
搜索: 検索
|
||||
名称或编码: 名前またはコード
|
||||
操作: 操作
|
||||
描述: 説明
|
||||
类型: タイプ
|
||||
名称: 名前
|
||||
编码: コード
|
||||
备注: 備考
|
||||
创建时间: 作成日時
|
||||
更新时间: 更新日時
|
||||
新增: 追加
|
||||
保存: 保存
|
||||
取消: キャンセル
|
||||
确认: 確認
|
||||
删除: 削除
|
||||
编辑: 編集
|
||||
查看: 表示
|
||||
导出: エクスポート
|
||||
打印: 印刷
|
||||
刷新: 更新
|
||||
返回: 戻る
|
||||
提交: 送信
|
||||
重置: リセット
|
||||
Conform: 確認
|
||||
Discard: 破棄
|
||||
Submit: 送信
|
||||
Reset: リセット
|
||||
Cancel: キャンセル
|
||||
全部: すべて
|
||||
日期: 日付
|
||||
管理员: 管理者
|
||||
普通用户: 一般ユーザー
|
||||
只读用户: 読み取り専用ユーザー
|
||||
访问控制: アクセス制御
|
||||
最后登录: 最終ログイン
|
||||
133
i18n/ko/msg.txt
133
i18n/ko/msg.txt
@ -1,133 +0,0 @@
|
||||
用户管理: 사용자 관리
|
||||
用户名: 사용자명
|
||||
密码: 비밀번호
|
||||
确认密码: 비밀번호 확인
|
||||
姓名: 이름
|
||||
邮箱: 이메일
|
||||
手机号: 전화번호
|
||||
所属机构: 소속 기관
|
||||
角色: 역할
|
||||
状态: 상태
|
||||
启用: 활성화
|
||||
停用: 비활성화
|
||||
新增用户: 사용자 추가
|
||||
编辑用户: 사용자 편집
|
||||
删除用户: 사용자 삭제
|
||||
重置密码: 비밀번호 재설정
|
||||
角色管理: 역할 관리
|
||||
角色名称: 역할명
|
||||
角色编码: 역할 코드
|
||||
角色描述: 역할 설명
|
||||
权限: 권한
|
||||
新增角色: 역할 추가
|
||||
编辑角色: 역할 편집
|
||||
删除角色: 역할 삭제
|
||||
分配权限: 권한 할당
|
||||
机构管理: 기관 관리
|
||||
机构名称: 기관명
|
||||
机构编码: 기관 코드
|
||||
机构类型: 기관 유형
|
||||
上级机构: 상위 기관
|
||||
新增机构: 기관 추가
|
||||
编辑机构: 기관 편집
|
||||
删除机构: 기관 삭제
|
||||
菜单管理: 메뉴 관리
|
||||
菜单名称: 메뉴명
|
||||
菜单路径: 메뉴 경로
|
||||
菜单图标: 메뉴 아이콘
|
||||
菜单排序: 메뉴 정렬
|
||||
上级菜单: 상위 메뉴
|
||||
新增菜单: 메뉴 추가
|
||||
编辑菜单: 메뉴 편집
|
||||
删除菜单: 메뉴 삭제
|
||||
权限管理: 권한 관리
|
||||
权限名称: 권한명
|
||||
权限编码: 권한 코드
|
||||
权限类型: 권한 유형
|
||||
资源权限: 리소스 권한
|
||||
操作权限: 작업 권한
|
||||
数据权限: 데이터 권한
|
||||
登录日志: 로그인 로그
|
||||
登录时间: 로그인 시간
|
||||
登录IP: 로그인 IP
|
||||
登录状态: 로그인 상태
|
||||
成功: 성공
|
||||
失败: 실패
|
||||
操作日志: 작업 로그
|
||||
操作类型: 작업 유형
|
||||
操作时间: 작업 시간
|
||||
操作人: 작업자
|
||||
操作结果: 작업 결과
|
||||
API密钥管理: API 키 관리
|
||||
密钥名称: 키 이름
|
||||
密钥值: 키 값
|
||||
密钥状态: 키 상태
|
||||
新增密钥: 키 추가
|
||||
编辑密钥: 키 편집
|
||||
删除密钥: 키 삭제
|
||||
在线用户: 온라인 사용자
|
||||
会话ID: 세션 ID
|
||||
最后活动时间: 마지막 활동 시간
|
||||
强制下线: 강제 로그아웃
|
||||
安全设置: 보안 설정
|
||||
密码策略: 비밀번호 정책
|
||||
密码长度: 비밀번호 길이
|
||||
密码复杂度: 비밀번호 복잡도
|
||||
密码有效期: 비밀번호 유효기간
|
||||
登录失败锁定: 로그인 실패 잠금
|
||||
最大失败次数: 최대 실패 횟수
|
||||
锁定时长: 잠금 시간
|
||||
双因素认证: 이중 인증
|
||||
启用双因素: 이중 인증 활성화
|
||||
验证码: 인증 코드
|
||||
Token管理: 토큰 관리
|
||||
Token名称: 토큰 이름
|
||||
Token过期时间: 토큰 만료 시간
|
||||
新增Token: 토큰 추가
|
||||
编辑Token: 토큰 편집
|
||||
删除Token: 토큰 삭제
|
||||
刷新Token: 토큰 갱신
|
||||
用户组: 사용자 그룹
|
||||
组名称: 그룹 이름
|
||||
组描述: 그룹 설명
|
||||
新增用户组: 사용자 그룹 추가
|
||||
编辑用户组: 사용자 그룹 편집
|
||||
删除用户组: 사용자 그룹 삭제
|
||||
成员管理: 멤버 관리
|
||||
添加成员: 멤버 추가
|
||||
移除成员: 멤버 제거
|
||||
搜索: 검색
|
||||
名称或编码: 이름 또는 코드
|
||||
操作: 작업
|
||||
描述: 설명
|
||||
类型: 유형
|
||||
名称: 이름
|
||||
编码: 코드
|
||||
备注: 비고
|
||||
创建时间: 생성 시간
|
||||
更新时间: 업데이트 시간
|
||||
新增: 추가
|
||||
保存: 저장
|
||||
取消: 취소
|
||||
确认: 확인
|
||||
删除: 삭제
|
||||
编辑: 편집
|
||||
查看: 보기
|
||||
导出: 내보내기
|
||||
打印: 인쇄
|
||||
刷新: 새로고침
|
||||
返回: 뒤로
|
||||
提交: 제출
|
||||
重置: 초기화
|
||||
Conform: 확인
|
||||
Discard: 폐기
|
||||
Submit: 제출
|
||||
Reset: 초기화
|
||||
Cancel: 취소
|
||||
全部: 전체
|
||||
日期: 날짜
|
||||
管理员: 관리자
|
||||
普通用户: 일반 사용자
|
||||
只读用户: 읽기 전용 사용자
|
||||
访问控制: 접근 제어
|
||||
最后登录: 마지막 로그인
|
||||
139
i18n/zh/msg.txt
139
i18n/zh/msg.txt
@ -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
|
||||
需输入手机号: 需输入手机号
|
||||
需输入验证码: 需输入验证码
|
||||
验证失败: 验证失败
|
||||
验证码: 验证码
|
||||
@ -11,15 +11,11 @@
|
||||
"alters":{}
|
||||
},
|
||||
"edit_exclouded_fields":[],
|
||||
"parentField":"parentid"
|
||||
"parentField":"parentid",
|
||||
"toolbar":{
|
||||
},
|
||||
"subtables":[
|
||||
{
|
||||
"field":"permid",
|
||||
"title":"权限角色",
|
||||
"subtable":"rolepermission"
|
||||
}
|
||||
"binds":[
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -67,7 +67,7 @@
|
||||
{
|
||||
"name": "created_at",
|
||||
"title": "注册日期",
|
||||
"type": "date"
|
||||
"type": "timestamp"
|
||||
},
|
||||
{
|
||||
"name": "last_login",
|
||||
|
||||
@ -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):
|
||||
|
||||
136
rbac/init.py
136
rbac/init.py
@ -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')
|
||||
|
||||
@ -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 = '.'
|
||||
|
||||
140
rbac/userperm.py
140
rbac/userperm.py
@ -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,21 +292,15 @@ 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)):
|
||||
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):
|
||||
if path.startswith(prefix):
|
||||
return True
|
||||
return False
|
||||
|
||||
@ -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:
|
||||
await self.get_userroles(sor, userid)
|
||||
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)
|
||||
|
||||
@ -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.")
|
||||
@ -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
212
wwwroot/index.ui
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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}"
|
||||
}
|
||||
}
|
||||
@ -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')
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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,18 +19,22 @@
|
||||
"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"}
|
||||
{
|
||||
"name": "username",
|
||||
"label": "用户名",
|
||||
"uitype": "str"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"label": "密码",
|
||||
"uitype": "password"
|
||||
}
|
||||
]
|
||||
},
|
||||
"binds": [
|
||||
@ -52,134 +50,61 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "phonecode",
|
||||
"label": "手机登录",
|
||||
"name": "checkcode",
|
||||
"label": "手机验证码",
|
||||
"content": {
|
||||
"widgettype": "VScrollPanel",
|
||||
"options": {"height": "100%"},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"gap": "8px"},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"widgettype": "Form",
|
||||
"options": {
|
||||
"otext": "未注册的手机号将自动创建账号",
|
||||
"i18n": true,
|
||||
"css": "login-desc"
|
||||
"toolbar": {
|
||||
"tools": [
|
||||
{
|
||||
"name": "gen_code",
|
||||
"label": "发送验证码"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "限中国国内手机",
|
||||
"fields": [
|
||||
{
|
||||
"name": "cell_no",
|
||||
"label": "手机号",
|
||||
"uitype": "str"
|
||||
},
|
||||
{
|
||||
"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"}
|
||||
"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')}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "submit",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "self",
|
||||
"options": {
|
||||
"method": "POST",
|
||||
"url": "{{code_login_url}}"
|
||||
"url": "{{entire_url('code_login.dspy')}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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('发送验证码')})"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@ -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":"Text",
|
||||
"options":{
|
||||
"otext":"logout success",
|
||||
"i18n":True,
|
||||
}
|
||||
},
|
||||
{
|
||||
"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()"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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}')
|
||||
@ -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": "系统错误,请稍后重试"
|
||||
}
|
||||
}
|
||||
@ -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,12 +82,9 @@ 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"
|
||||
@ -108,26 +94,21 @@ async with db.sqlorContext(dbname) as sor:
|
||||
"wid":"self",
|
||||
"event":"dismissed",
|
||||
"actiontype":"urlwidget",
|
||||
"target":"window",
|
||||
"target":"window.user_container",
|
||||
"options":{
|
||||
"url":entire_url('/index.ui')
|
||||
"url":entire_url('/rbac/user/userinfo.ui')
|
||||
}
|
||||
},
|
||||
{
|
||||
"wid":"self",
|
||||
"event":"opened",
|
||||
"event":"dismissed",
|
||||
"actiontype":"script",
|
||||
"target":"window.login_window",
|
||||
"target": f'body.login_window',
|
||||
"script":"this.destroy()"
|
||||
}
|
||||
]
|
||||
}
|
||||
return {
|
||||
"status": "error",
|
||||
"data": {
|
||||
"message": "system error"
|
||||
},
|
||||
|
||||
"widgettype":"Error",
|
||||
"options":{
|
||||
"timeout":3,
|
||||
|
||||
25
wwwroot/user/userapikey/add_userapikey.dspy
Normal file
25
wwwroot/user/userapikey/add_userapikey.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
24
wwwroot/user/userapikey/delete_userapikey.dspy
Normal file
24
wwwroot/user/userapikey/delete_userapikey.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
74
wwwroot/user/userapikey/get_userapikey.dspy
Normal file
74
wwwroot/user/userapikey/get_userapikey.dspy
Normal 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":[]
|
||||
}
|
||||
126
wwwroot/user/userapikey/index.ui
Normal file
126
wwwroot/user/userapikey/index.ui
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
22
wwwroot/user/userapikey/update_userapikey.dspy
Normal file
22
wwwroot/user/userapikey/update_userapikey.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
@ -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":"签退",
|
||||
|
||||
@ -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}}
|
||||
@ -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}}
|
||||
Loading…
x
Reference in New Issue
Block a user