bugfix
This commit is contained in:
parent
de8ce23fcb
commit
f932bfb088
@ -12,11 +12,87 @@ Usage:
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import fnmatch
|
||||||
from appPublic.uniqueID import getID
|
from appPublic.uniqueID import getID
|
||||||
from appPublic.log import debug, info
|
from appPublic.log import debug, info
|
||||||
|
from appPublic.jsonConfig import getConfig
|
||||||
from sqlor.dbpools import DBPools
|
from sqlor.dbpools import DBPools
|
||||||
|
|
||||||
|
|
||||||
|
def _scan_wwwroot_paths(wwwroot_dir, module_dir):
|
||||||
|
"""Scan wwwroot directory to get all actual file paths for a module.
|
||||||
|
|
||||||
|
Returns list of relative paths like '/customer_management/customer_list.ui'
|
||||||
|
"""
|
||||||
|
full_dir = os.path.join(wwwroot_dir, module_dir)
|
||||||
|
if not os.path.isdir(full_dir):
|
||||||
|
return []
|
||||||
|
|
||||||
|
paths = []
|
||||||
|
for root, dirs, files in os.walk(full_dir):
|
||||||
|
for f in files:
|
||||||
|
abs_path = os.path.join(root, f)
|
||||||
|
rel_path = os.path.relpath(abs_path, wwwroot_dir)
|
||||||
|
# Convert to URL path (forward slashes, leading /)
|
||||||
|
url_path = '/' + rel_path.replace(os.sep, '/')
|
||||||
|
paths.append(url_path)
|
||||||
|
return sorted(paths)
|
||||||
|
|
||||||
|
|
||||||
|
def _expand_wildcard_pattern(pattern, wwwroot_dir, all_paths):
|
||||||
|
"""Expand a wildcard pattern like '/customer_management/**' to actual paths.
|
||||||
|
|
||||||
|
Since rbac.check_roles_path() only does exact match, we need to expand
|
||||||
|
** patterns into individual file paths.
|
||||||
|
"""
|
||||||
|
if '**' not in pattern:
|
||||||
|
return [pattern]
|
||||||
|
|
||||||
|
# Pattern like '/customer_management/**' or '/financial_management/invoice**'
|
||||||
|
prefix = pattern.replace('**', '').rstrip('/')
|
||||||
|
|
||||||
|
matched = []
|
||||||
|
for p in all_paths:
|
||||||
|
if p.startswith(prefix):
|
||||||
|
matched.append(p)
|
||||||
|
|
||||||
|
return matched if matched else [pattern]
|
||||||
|
|
||||||
|
|
||||||
|
def _expand_permissions(PERMISSION_MATRIX, wwwroot_dir):
|
||||||
|
"""Expand PERMISSION_MATRIX with wildcard patterns into actual file paths.
|
||||||
|
|
||||||
|
Returns dict: {role_id: set_of_paths}
|
||||||
|
"""
|
||||||
|
# Build master list of all actual file paths
|
||||||
|
all_paths = []
|
||||||
|
if os.path.isdir(wwwroot_dir):
|
||||||
|
for item in os.listdir(wwwroot_dir):
|
||||||
|
full_item = os.path.join(wwwroot_dir, item)
|
||||||
|
if os.path.isdir(full_item):
|
||||||
|
all_paths.extend(_scan_wwwroot_paths(wwwroot_dir, item))
|
||||||
|
|
||||||
|
info(f'Scanned {len(all_paths)} actual file paths from wwwroot')
|
||||||
|
|
||||||
|
# Expand patterns
|
||||||
|
role_paths = {}
|
||||||
|
for module, patterns in PERMISSION_MATRIX.items():
|
||||||
|
for pattern, role_list in patterns.items():
|
||||||
|
if '**' in pattern:
|
||||||
|
expanded = _expand_wildcard_pattern(pattern, wwwroot_dir, all_paths)
|
||||||
|
info(f' Expanded "{pattern}" -> {len(expanded)} paths')
|
||||||
|
else:
|
||||||
|
expanded = [pattern]
|
||||||
|
|
||||||
|
for path in expanded:
|
||||||
|
if path not in role_paths:
|
||||||
|
role_paths[path] = set()
|
||||||
|
for role in role_list:
|
||||||
|
role_paths[path].add(role)
|
||||||
|
|
||||||
|
return role_paths
|
||||||
|
|
||||||
|
|
||||||
async def ensure_permission(sor, path, name='', permtype='page'):
|
async def ensure_permission(sor, path, name='', permtype='page'):
|
||||||
"""Ensure a permission exists in the database."""
|
"""Ensure a permission exists in the database."""
|
||||||
recs = await sor.R('permission', {'path': path})
|
recs = await sor.R('permission', {'path': path})
|
||||||
@ -33,10 +109,24 @@ async def ensure_permission(sor, path, name='', permtype='page'):
|
|||||||
|
|
||||||
|
|
||||||
async def ensure_role(sor, roleid, name, desc=''):
|
async def ensure_role(sor, roleid, name, desc=''):
|
||||||
"""Ensure a role exists with the given ID."""
|
"""Ensure a role exists with the given ID.
|
||||||
|
|
||||||
|
If a role with the same name or roleid-as-name already exists, reuse its ID.
|
||||||
|
This handles the case where roles were pre-created with UUID IDs but we define
|
||||||
|
them with string IDs in perm_config.py.
|
||||||
|
"""
|
||||||
|
# Try matching by name (supports both Chinese and English names)
|
||||||
|
for match_name in [name, roleid]:
|
||||||
|
recs = await sor.R('role', {'name': match_name})
|
||||||
|
if recs:
|
||||||
|
return recs[0].id
|
||||||
|
|
||||||
|
# Try matching by ID
|
||||||
recs = await sor.R('role', {'id': roleid})
|
recs = await sor.R('role', {'id': roleid})
|
||||||
if recs:
|
if recs:
|
||||||
return recs[0].id
|
return recs[0].id
|
||||||
|
|
||||||
|
# Create new role
|
||||||
await sor.C('role', {
|
await sor.C('role', {
|
||||||
'id': roleid,
|
'id': roleid,
|
||||||
'orgtypeid': '*',
|
'orgtypeid': '*',
|
||||||
@ -62,8 +152,9 @@ async def init_permissions_from_config(dbname, config_module=None):
|
|||||||
CRM是单业主机构系统,所有角色属于同一业主机构。
|
CRM是单业主机构系统,所有角色属于同一业主机构。
|
||||||
流程:
|
流程:
|
||||||
1. 创建约定角色(any/logined/anonymous)和定义的角色
|
1. 创建约定角色(any/logined/anonymous)和定义的角色
|
||||||
2. 注册 PERMISSION_MATRIX 中的路径权限
|
2. 扫描 wwwroot 目录,展开 ** 通配符为实际文件路径
|
||||||
3. 注册 CRUD 路径并授权
|
3. 注册权限并授权
|
||||||
|
4. 注册 CRUD 路径并授权
|
||||||
"""
|
"""
|
||||||
if config_module is None:
|
if config_module is None:
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||||
@ -75,13 +166,20 @@ async def init_permissions_from_config(dbname, config_module=None):
|
|||||||
PERMISSION_MATRIX = config_module.PERMISSION_MATRIX
|
PERMISSION_MATRIX = config_module.PERMISSION_MATRIX
|
||||||
CRUD_TABLES = config_module.CRUD_TABLES
|
CRUD_TABLES = config_module.CRUD_TABLES
|
||||||
|
|
||||||
|
# Get wwwroot directory from config
|
||||||
|
config = getConfig()
|
||||||
|
workdir = os.path.dirname(os.path.dirname(__file__))
|
||||||
|
wwwroot_dir = os.path.join(workdir, 'wwwroot')
|
||||||
|
|
||||||
|
# Expand wildcards into actual file paths
|
||||||
|
role_paths = _expand_permissions(PERMISSION_MATRIX, wwwroot_dir)
|
||||||
|
|
||||||
db = DBPools()
|
db = DBPools()
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
# Step 1: 创建约定角色(固定ID,rbac/userperm.py硬编码检查)
|
# Step 1: 创建约定角色(固定ID,rbac/userperm.py硬编码检查)
|
||||||
info('Creating convention roles...')
|
info('Creating convention roles...')
|
||||||
role_ids = {}
|
role_ids = {}
|
||||||
|
|
||||||
# 约定角色必须用固定ID
|
|
||||||
for fixed_id in ['any', 'logined', 'anonymous']:
|
for fixed_id in ['any', 'logined', 'anonymous']:
|
||||||
role_ids[fixed_id] = await ensure_role(sor, fixed_id, fixed_id)
|
role_ids[fixed_id] = await ensure_role(sor, fixed_id, fixed_id)
|
||||||
|
|
||||||
@ -92,16 +190,22 @@ async def init_permissions_from_config(dbname, config_module=None):
|
|||||||
sor, role['id'], role['name'], role.get('desc', '')
|
sor, role['id'], role['name'], role.get('desc', '')
|
||||||
)
|
)
|
||||||
|
|
||||||
# Step 3: 注册权限矩阵中的路径并授权
|
# Step 3: 注册展开后的路径权限并授权
|
||||||
info('Registering permissions from matrix...')
|
info('Registering expanded permissions...')
|
||||||
perm_count = 0
|
perm_count = 0
|
||||||
|
|
||||||
for module, paths in PERMISSION_MATRIX.items():
|
for path, roles in role_paths.items():
|
||||||
for path_pattern, role_list in paths.items():
|
# Register both /xxx and /main/xxx variants
|
||||||
permid = await ensure_permission(sor, path_pattern,
|
# The auth middleware passes request.path (e.g. /main/customer_management/xxx.ui)
|
||||||
name=f'{module}: {path_pattern}', permtype='module')
|
# but perm_config.py defines paths without /main prefix
|
||||||
|
variants = [path]
|
||||||
|
if not path.startswith('/main/'):
|
||||||
|
variants.append('/main' + path)
|
||||||
|
|
||||||
for role_name in role_list:
|
for variant in variants:
|
||||||
|
permid = await ensure_permission(sor, variant, permtype='page')
|
||||||
|
|
||||||
|
for role_name in roles:
|
||||||
if role_name in role_ids:
|
if role_name in role_ids:
|
||||||
await grant_permission(sor, role_ids[role_name], permid)
|
await grant_permission(sor, role_ids[role_name], permid)
|
||||||
perm_count += 1
|
perm_count += 1
|
||||||
@ -114,7 +218,7 @@ async def init_permissions_from_config(dbname, config_module=None):
|
|||||||
permid = await ensure_permission(sor, crud_path,
|
permid = await ensure_permission(sor, crud_path,
|
||||||
name=f'{module}/{table} CRUD', permtype='crud')
|
name=f'{module}/{table} CRUD', permtype='crud')
|
||||||
|
|
||||||
# 根据模块权限矩阵授予权限
|
# Grant to roles that have module-level access
|
||||||
if module in PERMISSION_MATRIX:
|
if module in PERMISSION_MATRIX:
|
||||||
for path_pattern, role_list in PERMISSION_MATRIX[module].items():
|
for path_pattern, role_list in PERMISSION_MATRIX[module].items():
|
||||||
if path_pattern.startswith(f'/{module}'):
|
if path_pattern.startswith(f'/{module}'):
|
||||||
@ -125,11 +229,41 @@ async def init_permissions_from_config(dbname, config_module=None):
|
|||||||
|
|
||||||
info(f'Permission initialization complete: {perm_count} grants created')
|
info(f'Permission initialization complete: {perm_count} grants created')
|
||||||
|
|
||||||
|
# Step 5: 同步 admin 和 superuser 角色权限(兼容已有数据库)
|
||||||
|
# 关键:superadmin 用户的角色是 UUID-based (orgtypeid=customer),需要额外同步
|
||||||
|
info('Syncing admin and superuser role permissions...')
|
||||||
|
admin_role_id = role_ids.get('admin')
|
||||||
|
superuser_role_id = role_ids.get('superuser')
|
||||||
|
admin_super_id = role_ids.get('admin_superuser')
|
||||||
|
|
||||||
|
# Get admin_superuser grants (source of truth)
|
||||||
|
admin_super_perms = []
|
||||||
|
if admin_super_id:
|
||||||
|
admin_super_perms = await sor.R('rolepermission', {'roleid': admin_super_id})
|
||||||
|
for g in admin_super_perms:
|
||||||
|
if admin_role_id:
|
||||||
|
await grant_permission(sor, admin_role_id, g.permid)
|
||||||
|
perm_count += 1
|
||||||
|
if superuser_role_id:
|
||||||
|
await grant_permission(sor, superuser_role_id, g.permid)
|
||||||
|
perm_count += 1
|
||||||
|
|
||||||
|
# 额外同步:找到所有 orgtypeid=customer 的 admin/superuser 角色并授权
|
||||||
|
info('Syncing customer-org admin/superuser roles...')
|
||||||
|
all_roles = await sor.R('role', {'orgtypeid': 'customer'})
|
||||||
|
for r in all_roles:
|
||||||
|
if r.name in ('admin', 'superuser'):
|
||||||
|
for g in admin_super_perms:
|
||||||
|
await grant_permission(sor, r.id, g.permid)
|
||||||
|
perm_count += 1
|
||||||
|
info(f' Synced {len(admin_super_perms)} grants to role {r.id} (name={r.name})')
|
||||||
|
|
||||||
|
info(f'After sync: {perm_count} total grants created')
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Run permission initialization."""
|
"""Run permission initialization."""
|
||||||
import json
|
import json
|
||||||
from appPublic.jsonConfig import getConfig
|
|
||||||
from appPublic.dictObject import DictObject
|
from appPublic.dictObject import DictObject
|
||||||
|
|
||||||
config_path = os.path.dirname(os.path.dirname(__file__))
|
config_path = os.path.dirname(os.path.dirname(__file__))
|
||||||
|
|||||||
@ -25,7 +25,9 @@ ROLES = [
|
|||||||
# 财务部
|
# 财务部
|
||||||
{'id': 'finance_admin', 'name': '财务管理员', 'desc': '所有财务操作、报表'},
|
{'id': 'finance_admin', 'name': '财务管理员', 'desc': '所有财务操作、报表'},
|
||||||
{'id': 'finance_clerk', 'name': '财务出纳', 'desc': '收款登记、付款处理'},
|
{'id': 'finance_clerk', 'name': '财务出纳', 'desc': '收款登记、付款处理'},
|
||||||
# 管理员
|
# 管理员(兼容已有数据库中的角色名)
|
||||||
|
{'id': 'admin', 'name': '管理员', 'desc': '全部权限'},
|
||||||
|
{'id': 'superuser', 'name': '超级用户', 'desc': '全部权限'},
|
||||||
{'id': 'admin_superuser', 'name': '超级用户', 'desc': '全部权限,初始化用'},
|
{'id': 'admin_superuser', 'name': '超级用户', 'desc': '全部权限,初始化用'},
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -35,13 +37,38 @@ ROLES = [
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
PERMISSION_MATRIX = {
|
PERMISSION_MATRIX = {
|
||||||
# ----------------------------------------------------------
|
# ----------------------------------------------------------
|
||||||
# 公共路径(登录等)
|
# 静态资源(bricks 框架)—— 任何人可访问
|
||||||
# ----------------------------------------------------------
|
# ----------------------------------------------------------
|
||||||
'main': {
|
'bricks': {
|
||||||
|
'/bricks/**': ['any'],
|
||||||
|
},
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# 公共路径(登录等)—— 任何人可访问
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
'public': {
|
||||||
|
'/main/rbac/user/login.ui': ['any'],
|
||||||
|
'/main/rbac/user/login.dspy': ['any'],
|
||||||
|
'/main/rbac/user/up_login.dspy': ['any'],
|
||||||
|
'/main/rbac/user/logout.dspy': ['any'],
|
||||||
|
'/main/rbac/user/register.dspy': ['any'],
|
||||||
|
'/main/rbac/user/register.ui': ['any'],
|
||||||
'/main/login.ui': ['any'],
|
'/main/login.ui': ['any'],
|
||||||
'/main/login.dspy': ['any'],
|
'/main/login.dspy': ['any'],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# 根路径 —— 已登录用户可访问(/main/ 指向 base.ui)
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
'root': {
|
||||||
|
'/main/': ['logined'],
|
||||||
|
'/main/index.html': ['logined'],
|
||||||
|
'/main/base.ui': ['logined'],
|
||||||
|
'/base.ui': ['logined'],
|
||||||
|
'/login.ui': ['any'],
|
||||||
|
'/login.dspy': ['any'],
|
||||||
|
},
|
||||||
|
|
||||||
# ----------------------------------------------------------
|
# ----------------------------------------------------------
|
||||||
# 客户管理模块 - 销售+财务+管理员
|
# 客户管理模块 - 销售+财务+管理员
|
||||||
# ----------------------------------------------------------
|
# ----------------------------------------------------------
|
||||||
|
|||||||
115
wwwroot/login.ui
115
wwwroot/login.ui
@ -1,29 +1,100 @@
|
|||||||
{
|
{
|
||||||
"type": "Page",
|
"id": "login_page",
|
||||||
"title": "CRM Login",
|
"widgettype": "VBox",
|
||||||
"style": {"background": "linear-gradient(135deg, #667eea 0%, #764ba2 100%)"},
|
"options": {
|
||||||
"content": {
|
"width": "100%",
|
||||||
"type": "VBox",
|
"height": "100vh",
|
||||||
"align": "center",
|
"style": {
|
||||||
"justify": "center",
|
"background": "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
|
||||||
"style": {"minHeight": "100vh", "padding": "20px"},
|
"alignItems": "center",
|
||||||
"children": [
|
"justifyContent": "center"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
{
|
{
|
||||||
"type": "Card",
|
"widgettype": "VBox",
|
||||||
"style": {"width": "400px", "maxWidth": "95vw", "borderRadius": "16px", "boxShadow": "0 8px 32px rgba(0,0,0,0.15)", "padding": "32px"},
|
"options": {
|
||||||
"content": {
|
"width": "400px",
|
||||||
"type": "VBox",
|
"style": {
|
||||||
"gap": 16,
|
"background": "white",
|
||||||
"children": [
|
"borderRadius": "12px",
|
||||||
{"type": "Text", "content": "CRM System", "style": {"fontSize": "24px", "fontWeight": "bold", "textAlign": "center", "color": "#333"}},
|
"boxShadow": "0 8px 32px rgba(0,0,0,0.15)",
|
||||||
{"type": "Text", "content": "Please login to continue", "style": {"fontSize": "14px", "textAlign": "center", "color": "#666"}},
|
"padding": "40px 32px"
|
||||||
{"type": "TextField", "id": "username", "placeholder": "Username", "prefixIcon": "person", "style": {"width": "100%"}},
|
}
|
||||||
{"type": "TextField", "id": "password", "placeholder": "Password", "prefixIcon": "lock", "password": true, "style": {"width": "100%"}},
|
},
|
||||||
{"type": "Button", "id": "loginBtn", "text": "Login", "variant": "primary", "fullWidth": true, "size": "large"},
|
"subwidgets": [
|
||||||
{"type": "Text", "id": "errorMsg", "content": "", "style": {"color": "#d32f2f", "fontSize": "12px", "textAlign": "center"}}
|
{
|
||||||
]
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"label": "CRM系统登录",
|
||||||
|
"style": {
|
||||||
|
"fontSize": "24px",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"textAlign": "center",
|
||||||
|
"marginBottom": "24px",
|
||||||
|
"color": "#333"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Form",
|
||||||
|
"id": "login_form",
|
||||||
|
"options": {
|
||||||
|
"url": "{{entire_url('api/login.dspy')}}",
|
||||||
|
"method": "POST",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "username",
|
||||||
|
"label": "用户名",
|
||||||
|
"uitype": "str",
|
||||||
|
"required": true,
|
||||||
|
"placeholder": "请输入用户名"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "password",
|
||||||
|
"label": "密码",
|
||||||
|
"uitype": "password",
|
||||||
|
"required": true,
|
||||||
|
"placeholder": "请输入密码"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"submitLabel": "登录",
|
||||||
|
"submitVariant": "primary",
|
||||||
|
"submitFullWidth": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "HBox",
|
||||||
|
"options": {
|
||||||
|
"style": {
|
||||||
|
"justifyContent": "center",
|
||||||
|
"marginTop": "16px",
|
||||||
|
"gap": "16px"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Button",
|
||||||
|
"id": "btn_register",
|
||||||
|
"options": {
|
||||||
|
"label": "注册账号",
|
||||||
|
"variant": "text",
|
||||||
|
"url": "/rbac/user/register.ui"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"binds": [
|
||||||
|
{
|
||||||
|
"wid": "login_form",
|
||||||
|
"event": "submit",
|
||||||
|
"actiontype": "formsubmit",
|
||||||
|
"callback": {
|
||||||
|
"success": "navigate('/index.ui')"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user