first commit

This commit is contained in:
yumoqing 2025-07-16 14:19:12 +08:00
commit b46426abe0
59 changed files with 2017 additions and 0 deletions

2
README.md Normal file
View File

@ -0,0 +1,2 @@
# rbac

3
json/build.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
xls2ui -m ../models -o ../wwwroot rbac *.json

51
json/organization.json Normal file
View File

@ -0,0 +1,51 @@
{
"models_dir": "${HOME}$/py/rbac/models",
"output_dir": "${HOME}$/py/sage/wwwroot/_a/organization",
"dbname": "sage",
"tblname": "organization",
"title":"Organization",
"params": {
"sortby":"orgname",
"editor":{
"binds":[
{
"wid":"province_id",
"event":"changed",
"actiontype":"script",
"target":"city_id",
"script":"this.loadData({cond:'parentid=\\''+params.province_id+'\\''})"
},
{
"wid":"city_id",
"event":"changed",
"actiontype":"script",
"target":"distinct_id",
"script":"this.loadData({cond:'parentid=\\'' + params.city_id+ '\\''})"
}
]
},
"browserfields": {
"exclouded": ["id"],
"alters": {
}
},
"editexclouded": [
"id"
],
"subtables":[
{
"field":"orgid",
"title":"Org. type",
"url":"../orgtypes",
"subtable":"orgtypes"
},
{
"field":"orgid",
"title":"Users",
"url":"../users",
"subtable":"users"
}
],
"record_toolbar": null
}
}

18
json/orgtypes.json Normal file
View File

@ -0,0 +1,18 @@
{
"models_dir": "${HOME}$/py/rbac/models",
"output_dir": "${HOME}$/py/sage/wwwroot/_a/orgtypes",
"dbname": "sage",
"tblname": "orgtypes",
"title":"Org. type",
"params": {
"browserfields": {
"exclouded": ["id", "orgid"],
"cwidth": {}
},
"editexclouded": [
"id",
"orgid"
],
"record_toolbar": null
}
}

21
json/permission.json Normal file
View File

@ -0,0 +1,21 @@
{
"tblname": "permission",
"uitype":"tree",
"title":"权限",
"params":{
"idField":"id",
"textField":"path",
"sortby":"path",
"editable":true,
"browserfields":{
"alters":{}
},
"edit_exclouded_fields":[],
"parentField":"parentid",
"toolbar":{
},
"binds":[
]
}
}

31
json/role.json Normal file
View File

@ -0,0 +1,31 @@
{
"models_dir": "${HOME}$/py/rbac/models",
"output_dir": "${HOME}$/py/sage/wwwroot/_a/role",
"dbname": "sage",
"tblname": "role",
"title":"角色",
"params": {
"sortby":"name",
"browserfields": {
"exclouded": ["id"],
"cwidth": {}
},
"editexclouded": [
"id"
],
"subtables":[
{
"field":"roleid",
"title":"角色权限",
"url":"../rolepermission",
"subtable":"rolepermission"
},
{
"field":"roleid",
"title":"用户",
"url":"../users",
"subtable":"users"
}
]
}
}

26
json/rolepermission.json Normal file
View File

@ -0,0 +1,26 @@
{
"models_dir": "${HOME}$/py/rbac/models",
"output_dir": "${HOME}$/py/sage/wwwroot/_a/rolepermission",
"dbname": "sage",
"tblname": "rolepermission",
"title":"用户",
"params": {
"relation":{
"outter_field":"permid",
"param_field":"roleid"
},
"noedit":true,
"browserfields": {
"exclouded": ["id", "roleid"],
"alters":{
"permid":{
"cwidth":60
}
}
},
"editexclouded": [
"id", "roleid"
],
"record_toolbar": null
}
}

16
json/userapp.json Normal file
View File

@ -0,0 +1,16 @@
{
"tblname": "userapp",
"title":"用户",
"params": {
"confidential_fields":["apikey"],
"sortby":"appname",
"browserfields": {
"exclouded": ["id", "userid"],
"alters": {}
},
"editexclouded": [
"id", "userid"
],
"record_toolbar": null
}
}

17
json/userdepartment.json Normal file
View File

@ -0,0 +1,17 @@
{
"models_dir": "${HOME}$/py/rbac/models",
"output_dir": "${HOME}$/py/sage/wwwroot/_a/userdepartment",
"dbname": "sage",
"tblname": "userdepartment",
"title":"用户",
"params": {
"browserfields": {
"exclouded": ["id", "userid"],
"cwidth": {}
},
"editexclouded": [
"id", "userid"
],
"record_toolbar": null
}
}

14
json/userrole.json Normal file
View File

@ -0,0 +1,14 @@
{
"tblname": "userrole",
"title":"用户角色管理",
"params": {
"browserfields": {
"exclouded": ["id", "orgid"],
"alters": {}
},
"editexclouded": [
"id",
"userid"
]
}
}

28
json/users.json Normal file
View File

@ -0,0 +1,28 @@
{
"tblname": "users",
"title":"用户",
"params": {
"sortby":"username",
"confidential_fields":["password"],
"logined_userorgid":"orgid",
"browserfields": {
"exclouded": ["id", "password", "orgid", "nick_name" ],
"cwidth": {}
},
"editexclouded": [
"id", "nick_name", "orgid"
],
"subtables": [
{
"field":"userid",
"title":"用户角色",
"subtable":"userrole"
},
{
"field":"userid",
"title":"APIKEY",
"subtable":"userapp"
}
]
}
}

BIN
models/audit_log.xlsx Normal file

Binary file not shown.

BIN
models/organization.xlsx Normal file

Binary file not shown.

BIN
models/orgtypes.xlsx Normal file

Binary file not shown.

BIN
models/permission.xlsx Normal file

Binary file not shown.

BIN
models/role.xlsx Normal file

Binary file not shown.

BIN
models/rolepermission.xlsx Normal file

Binary file not shown.

BIN
models/userapp.xlsx Normal file

Binary file not shown.

BIN
models/userdepartment.xlsx Normal file

Binary file not shown.

BIN
models/userrole.xlsx Normal file

Binary file not shown.

BIN
models/users.xlsx Normal file

Binary file not shown.

4
pyproject.toml Normal file
View File

@ -0,0 +1,4 @@
[build-system]
requires = ["setuptools>=61", "wheel"]
build-backend = "setuptools.build_meta"

0
rbac/README.md Normal file
View File

0
rbac/__init__.py Normal file
View File

9
rbac/audit_log.py Normal file
View File

@ -0,0 +1,9 @@
from sqlor.dbpools import DBPools
from appPublic.dictObject import DictObject
from appPublic.uniqueID import getID
import json
def write_audit_log(sor, request):
id = getID()
params_kw = get_params_kw(request)

176
rbac/check_perm.py Normal file
View File

@ -0,0 +1,176 @@
import time
from aiohttp import BasicAuth
from sqlor.dbpools import DBPools
from appPublic.registerfunction import RegisterFunction
from appPublic.rc4 import password, unpassword
from appPublic.jsonConfig import getConfig
from appPublic.log import debug, exception
from appPublic.dictObject import DictObject
from appPublic.timeUtils import curDateString
from appPublic.uniqueID import getID
from ahserver.auth_api import AuthAPI, user_login
from ahserver.globalEnv import password_encode
from ahserver.serverenv import ServerEnv, get_serverenv, set_serverenv
async def get_user_roles(userid):
sql = "select concat(b.orgtypeid, '.', b.name) as name from userrole a, role b where a.userid=${userid}$ and a.roleid = b.id"
db = DBPools()
roles = []
dbname = await get_dbname()
async with db.sqlorContext(dbname) as sor:
recs = await sor.sqlExe(sql, {'userid':userid})
if len(recs) < 1:
return roles
for r in recs:
roles.append(r.name)
return roles
async def create_org(sor, ns, orgtypes=[]):
await sor.C('organization', ns)
if orgtypes == []:
orgtypes = ['customer']
if 'customer' not in orgtypes:
orgtypes.append('customer')
for ot in orgtypes:
otns = {
'id':getID(),
'orgid':ns.id,
'orgtypeid':ot
}
await sor.C('orgtypes', otns)
async def create_user(sor, ns, roles=[]):
"""
role format:
{
orgtypeid: rr,
roles: ['ee', 'bb']
}
"""
await sor.C('users', ns)
if roles == []:
roles = [
{
'orgtypeid': 'customer',
'roles': [ 'customer']
}
]
for rt in roles:
sql = "select * from role where orgtypeid = ${otid}$ and name in ${roles}$)"
recs = await sor.sqlExe(sql, {
'otid': rt['orgtypeid'],
'roles': rt['roles']
})
for r in recs:
await sor.C('userrole', {
'id':getID(),
'userid':ns.id,
'roleid':r.id
})
async def register_user(sor, ns):
if ns.password != ns.cfm_password:
debug('password not match')
return False
ns.password = password_encode(ns.password)
id = getID()
ns.id = id
ns.orgid = id
ns1 = DictObject(id=id, orgname=ns.username)
await create_org(sor, ns1)
await create_user(sor, ns)
return id
async def get_dbname():
rf = RegisterFunction()
dbname = await rf.exe('get_module_dbname', 'rbac')
return dbname
async def checkUserPassword(request, username, password):
db = DBPools()
dbname = await get_dbname()
async with db.sqlorContext(dbname) as sor:
sql = "select * from users where username=${username}$ and password=${password}$"
recs = await sor.sqlExe(sql, {'username':username, 'password':password})
if len(recs) < 1:
return False
await user_login(request, recs[0].id,
username=recs[0].username,
userorgid=recs[0].orgid)
return True
return False
async def basic_auth(sor, auth):
auther = BasicAuth('x')
m = auther.decode(auth)
username = m.login
password = password_encode(m.password)
sql = "select * from users where username=${username}$ and password=${password}$"
recs = await sor.sqlExe(sql, {'username':username,'password':password})
if len(recs) < 1:
return None
return recs[0].id
async def bearer_auth(sor, auth):
# apikey = get_apikey_from_token(auth[7:])
apikey = auth[7:]
if apikey is None:
return None
sql = "select * from userapp where apikey=${apikey}$ and expired_date > ${today}$"
recs = await sor.sqlExe(sql, {"apikey":apikey, 'today': curDateString()})
if len(recs) < 1:
return None
return recs[0].userid
async def getAuthenticationUserid(sor, request):
auth = request.headers.get('Authentication')
if auth is None:
return None
for h,f in registered_auth_methods.items():
if auth.startswith(h):
return await f(auth)
return None
async def objcheckperm(obj, request, userid, path):
debug(f'check permission: {userid=}, {path=}')
sql = """select distinct a.*, c.userid from
(select id, path from permission where path=${path}$) a
right join
rolepermission b on a.id = b.permid
right join userrole c on b.roleid = c.roleid
where c.userid = ${userid}$
"""
dbname = await get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
if userid is None:
userid = await getAuthenticationUserid(sor, request)
perms = await sor.R('permission', {'path':path})
if len(perms) == 0:
debug(f'{path=} not found in permission, can access')
return True
if userid is None:
debug(f'{userid=} is None, can not access {path=}')
return False
recs = await sor.sqlExe(sql, {'path':path, 'userid':userid})
for r in recs:
id = r['id']
if id is not None:
debug(f'{userid=} can access {path=}')
return True
debug(f'{userid=} has not permission to call {path=}')
return False
debug(f'error happened {userid}, {path}')
return False
registered_auth_methods = {
"Basic ": basic_auth,
"Bearer ": bearer_auth
}
def register_auth_method(heading, func):
registered_auth_methods[heading] = func

18
rbac/init.py Normal file
View File

@ -0,0 +1,18 @@
from ahserver.auth_api import AuthAPI
from ahserver.serverenv import ServerEnv
from rbac.check_perm import objcheckperm, get_user_roles, checkUserPassword, register_user, register_auth_method, create_org, create_user
from rbac.set_role_perms import set_role_perm, set_role_perms
def load_rbac():
AuthAPI.checkUserPermission = objcheckperm
env = ServerEnv()
env.create_org = create_org
env.create_user = create_user
env.get_user_roles = get_user_roles
env.check_user_password = checkUserPassword
env.register_user = register_user
env.set_role_perm = set_role_perm
env.set_role_perms = set_role_perms
env.register_auth_method = register_auth_method

71
rbac/set_role_perms.py Normal file
View File

@ -0,0 +1,71 @@
import sys
import os
import asyncio
from sqlor.dbpools import DBPools
from appPublic.jsonConfig import getConfig
from appPublic.uniqueID import getID
from appPublic.asynciorun import run
async def set_role_perm(dbname, module, orgtype, role, tblname):
db = DBPools()
async with db.sqlorContext(dbname) as sor:
if '/' in dbname:
path = [f'/{module}/{dbname}']
else:
paths = [
f'/{module}/{tblname}',
f'/{module}/{tblname}/index.ui',
f'/{module}/{tblname}/get_{tblname}.dspy',
f'/{module}/{tblname}/add_{tblname}.dspy',
f'/{module}/{tblname}/delete_{tblname}.dspy',
f'/{module}/{tblname}/update_{tblname}.dspy'
]
for pat in paths:
recs = await sor.R('permission', {'path': pat})
if len(recs) == 0:
permid = getID()
await sor.C('permission', {'id':permid, 'path':pat})
else:
permid = recs[0].id
recs = await sor.R('role', {'orgtypeid':orgtype, 'name':role})
if len(recs) == 0:
roleid = getID()
await sor.C('role', {
'id':roleid,
'name':role,
'orgtypeid':orgtype
})
else:
roleid = recs[0].id
await sor.C('rolepermission', {
'id':getID(),
'roleid':roleid,
'permid':permid
})
print(f'{orgtype=}, {role=}, {tblname=} permission configured')
async def set_role_perms(dbname, module, orgtype, role, items):
for tblname in items:
await set_role_perm(dbname, module, orgtype, role, tblname)
if __name__ == '__main__':
async def main():
if len(sys.argv) < 6:
print(f'{sys.argv[0]} dbname module orgtype role tblname ...\n')
sys.exit(1)
dbname = sys.argv[1]
module = sys.argv[2]
orgtype = sys.argv[3]
role = sys.argv[4]
await set_role_perms(dbname, module, orgtype, role, sys.argv[5:])
def run(coro):
p = '.'
config = getConfig(p, {'woridir':p})
DBPools(config.databases)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(coro())
run(main)

1
rbac/version.py Normal file
View File

@ -0,0 +1 @@
__version__ = '0.1.0'

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
aiohttp
PyJWT

191
script/init.py Normal file
View File

@ -0,0 +1,191 @@
import os
import sys
import argparse
from traceback import format_exc
import asyncio
from appPublic.uniqueID import getID
from appPublic.timeUtils import days_between, strdate_add
from appPublic.jsonConfig import getConfig
from random import randint, random
from appPublic.folderUtils import listFile
from appPublic.registerfunction import RegisterFunction
from sqlor.dbpools import DBPools
from rbac.check_perm import mypassword
databases = {
"sage":{
"driver":"aiomysql",
"async_mode":True,
"coding":"utf8",
"maxconn":100,
"dbname":"sage",
"kwargs":{
"user":"test",
"db":"sage",
"password":"QUZVcXg5V1p1STMybG5Ia6mX9D0v7+g=",
"host":"localhost"
}
}
}
async def insert_perm(path):
db = DBPools()
async with db.sqlorContext('sage') as sor:
ns = {
'id':getID(),
'path':path
}
await sor.C('permission', ns)
av_folders = [
'/index.ui',
'/user.ui',
'/menu.ui',
'/get_code.dspy',
'/top.ui',
'/center',
'/bottom.ui',
'/app_panel.ui',
'/bricks',
'/imgs',
'/login.ui',
'/gen_code.dspy',
'/code_login.dspy',
'/up_login.dspy',
'/wechat_login.ui',
'/public',
'/debug/index.dspy',
'/rbac/userpassword_login.ui',
'/rbac/userpassword_login.dspy',
'/tmp'
]
orgtypes = ['owner', 'reseller']
type_roles = {
'owner':['superuser', 'customer'],
'reseller':['admin','operator', 'sale', 'maintainer', 'accountant'],
'customer':['admin', 'customer']
}
rbac_tables = [
"organization", "orgtypes", "role", "users", "userrole", "rolepermission", "permission",
"userapp", "userdepartment" ]
role_perms = {
'superuser': [
'/get_code.dspy',
'/rbac/add_adminuser.ui',
'/rbac/add_adminuser.dspy',
'/rbac/role/index.ui',
'/rbac/role/get_role.dspy',
'/rbac/role/update_role.dspy',
'/rbac/role/delete_role.dspy',
'/rbac/role/add_role.dspy',
'/rbac/permission/add_permission.dspy',
'/rbac/permission/delete_permission.dspy',
'/rbac/permission/update_permission.dspy',
'/rbac/permission/get_permission.dspy',
'/rbac/permission/index.ui',
'/rbac/rolepermission/add_rolepermission.dspy',
'/rbac/rolepermission/delete_rolepermission.dspy',
'/rbac/rolepermission/update_rolepermission.dspy',
'/rbac/rolepermission/get_rolepermission.dspy',
'/rbac/rolepermission/index.ui'
],
'admin':[
'/rbac/users/add_users.dspy',
'/rbac/users/delete_users.dspy',
'/rbac/users/update_users.dspy',
'/rbac/users/get_users.dspy',
'/rbac/users/index.ui',
'/rbac/userrole/add_userrole.dspy',
'/rbac/userrole/delete_userrole.dspy',
'/rbac/userrole/update_userrole.dspy',
'/rbac/userrole/get_userrole.dspy',
'/rbac/userrole/index.ui'
]
}
async def init_perms():
db = DBPools()
async with db.sqlorContext('sage') as sor:
for rn, pths in role_perms.items():
roles = await sor.sqlExe('select * from role where name=${rn}$', {'rn':rn})
if len(roles) == 0:
print(f'{rn=} not exists')
continue
for p in pths:
print(f'{rn=} set {p=}')
perms = await sor.sqlExe('select * from permission where path=${p}$', {'p':p})
if len(perms):
await sor.C('rolepermission', {'id':getID(),
'roleid':roles[0].id,
'permid':perms[0].id})
else:
print(f'{p} is not exists')
async def init_rbac(passwd):
db = DBPools()
async with db.sqlorContext('sage') as sor:
recs = await sor.R('organization', {'id':'0'})
if len(recs) > 0:
return
await sor.C('organization',{'id':'0'})
await sor.C('orgtypes', {'id':getID(), 'orgid':'0', 'orgtypeid':'owner'})
await sor.C('orgtypes', {'id':getID(), 'orgid':'0', 'orgtypeid':'reseller'})
roleid = ''
for orgtype, roles in type_roles.items():
for r in roles:
id = getID()
await sor.C('role', {'id':id,'orgtypeid':orgtype,'name':r})
if orgtype == 'owner' and r == 'superuser':
roleid = id
ns = {
'id':getID(),
'username':'superuser',
'orgid':'0',
'password':passwd
}
await sor.C('users', ns)
ns1 = {
'id':getID(),
'userid':ns['id'],
'roleid':roleid
}
await sor.C('userrole', ns1)
async def add_permissions(workdir):
root = os.path.join(workdir, 'wwwroot')
for f in listFile(root, rescursive=True):
cnt = len(root)
pth = f[cnt:]
print(f'{f=},{root=},{pth=}, {cnt=}')
act = True
for f in av_folders:
if pth.startswith(f):
act = False
break
if act:
await insert_perm(pth)
async def main(workdir, passwd):
await add_permissions(workdir)
await init_rbac(passwd)
await init_perms()
if __name__ == '__main__':
parser = argparse.ArgumentParser(prog='RBAC init')
parser.add_argument('-w', '--workdir')
parser.add_argument('password')
args = parser.parse_args()
if args.password is None:
parser.usage()
sys.exit(1)
print(f'{args=}')
config = getConfig(args.workdir, {'workdir':args.workdir})
supassword = mypassword(args.password)
DBPools(config.databases)
asyncio.get_event_loop().run_until_complete(main(args.workdir, supassword))

7
script/setroleperms.sh Normal file
View File

@ -0,0 +1,7 @@
#!/usr/bin/bash
python -m rbac.set_role_perms sage rbac owner superuser role permission rolepermission user user role organizationorgtypes
python -m rbac.set_role_perms sage rbac owner admin user userrole
python -m rbac.set_role_perms sage rbac customer admin user userrole
python -m rbac.set_role_perms sage rbac reseller admin user userrole

16
setup.cfg Normal file
View File

@ -0,0 +1,16 @@
[metadata]
name=rbac
version = 1.0.0
description = a RBAC user authenticate module
author = "yu moqing"
author_email = "yumoqing@gmail.com"
readme = "README.md"
license = "MIT"
[options]
packages = find:
requires_python = ">=3.8"
install_requires =
apppublic
sqlor
ahserver

View File

@ -0,0 +1,23 @@
if params_kw.get('password') != params_kw.get('chkpassword'):
return Error(title='add user error', message='password not match')
ns = params_kw.copy()
ns['id'] = uuid()
await rfexe('passowrd', ns)
user_orgid = await get_userorgid()
ns['orgid'] = user_orgid
dbname = await rfexe('get_module_dbname','rbac')
db = DBPools()
debug(f'{dbname=}')
async with db.sqlorContext(dbname) as sor:
await sor.C('users',ns.copy())
uid = ns['id']
ns = {
'id':uuid(),
'userid':uid,
'roleid':'admin'
}
await sor.C('userrole', ns.copy())
return UiMesage(title='Success', message='admin user added')
return UiError(title='Error', message='Error happened when add admin user')

36
wwwroot/add_adminuser.ui Normal file
View File

@ -0,0 +1,36 @@
{
"widgettype":"ModalForm",
"options":{
"cwidth":20,
"cheight":30,
"title":"增加管理员",
"fields":[
{
"name":"username",
"label":"用户名",
"uitype":"str"
},
{
"name":"password",
"label":"密码",
"uitype":"password"
},
{
"name":"chkpassword",
"label":"确认密码",
"uitype":"password"
}
]
},
"binds":[
{
"wid":"self",
"event":"submit",
"actiontype":"urlwidget",
"target":"self",
"options":{
"url":"{{entire_url('./add_adminuser.dspy')}}"
}
}
]
}

36
wwwroot/add_provider.dspy Normal file
View File

@ -0,0 +1,36 @@
ns = params_kw.copy()
id = params_kw.id
if not id or len(id) > 32:
id = uuid()
ns['id'] = id
db = DBPools()
dbname = get_module_dbname('rbac')
async with db.sqlorContext(dbname) as sor:
ownerid = await get_userorgid()
await create_org(sor, ns, orgtypes=['customer', 'provider'])
if openCustomerAccounts:
await openCustomerAccounts(sor, ownerid, orgid)
if openProviderAccounts:
await openProviderAccounts(sor, ownerid, id)
return {
"widgettype":"Message",
"options":{
"user_data":ns,
"cwidth":16,
"cheight":9,
"title":"Add Success",
"timeout":3,
"message":"ok"
}
}
return {
"widgettype":"Error",
"options":{
"title":"Add Error",
"cwidth":16,
"cheight":9,
"timeout":3,
"message":"failed"
}
}

26
wwwroot/add_reseller.dspy Normal file
View File

@ -0,0 +1,26 @@
ns = params_kw.copy()
id = params_kw.id
if not id or len(id) > 32:
id = uuid()
ns['id'] = id
db = DBPools()
dbname = get_module_dbname('rbac')
async with db.sqlorContext(dbname) as sor:
ownerid = await get_userorgid()
await create_org(sor, ns, orgtypes=['customer', 'reseller'])
if openCustomerAccounts:
await openCustomerAccounts(sor, ownerid, orgid)
if openResellerAccounts:
await openResellerAccounts(sor, ownerid, id)
return UiMessage(title="Success", message="add reseller success")
return {
"widgettype":"Error",
"options":{
"title":"Add reseller Error",
"cwidth":16,
"cheight":9,
"timeout":3,
"message":"failed"
}
}

7
wwwroot/admin_menu.ui Normal file
View File

@ -0,0 +1,7 @@
[
{
"name":"users",
"label":"用户管理"
"url":"{{entire_url('/rbac/users')}}"
}
]

151
wwwroot/get_provider.dspy Normal file
View File

@ -0,0 +1,151 @@
ns = params_kw.copy()
debug(f'get_organization.dspy:{ns=}')
if not ns.get('page'):
ns['page'] = 1
if not ns.get('sort'):
ns['sort'] = 'orgname'
sql = '''select a.*, b.province_id_text, c.city_id_text, d.distinct_id_text
from (select x.* from (select * from organization where 1=1 [[filterstr]]) x, orgtypes y where x.id = y.orgid and y.orgtypeid='provider') a left join (select k as province_id,
v as province_id_text from appcodes_kv where parentid='chnaddr') b on a.province_id = b.province_id left join (select k as city_id,
v as city_id_text from appcodes_kv where parentid='city') c on a.city_id = c.city_id left join (select k as distinct_id,
v as distinct_id_text from appcodes_kv where parentid='distinct') d on a.distinct_id = d.distinct_id'''
filterjson = params_kw.get('data_filter')
if not filterjson:
fields = [ f['name'] for f in [
{
"name": "id",
"title": "机构编码",
"type": "str",
"length": 32
},
{
"name": "orgname",
"title": "机构名称",
"type": "str",
"length": 100
},
{
"name": "alias_name",
"title": "机构别名",
"type": "str",
"length": 100
},
{
"name": "contactor",
"title": "联系人",
"type": "str",
"length": 32
},
{
"name": "contactor_phone",
"title": "联系人电话",
"type": "str",
"length": 100
},
{
"name": "province_id",
"title": "所在省id",
"type": "str",
"length": 32
},
{
"name": "city_id",
"title": "所在城市id",
"type": "str",
"length": 32
},
{
"name": "distinct_id",
"title": "所在地区id",
"type": "str",
"length": 32
},
{
"name": "emailaddress",
"title": "邮箱",
"type": "str",
"length": 256
},
{
"name": "address",
"title": "地址",
"type": "str",
"length": 400
},
{
"name": "main_business",
"title": "主营业务描述",
"type": "str",
"length": 1000
},
{
"name": "orgcode",
"title": "组织结构代码",
"type": "str",
"length": 100,
"comments": "个人客户存身份证"
},
{
"name": "license_img",
"title": "营业执照",
"type": "str",
"length": 400,
"comments": "个人客户存身份证照片"
},
{
"name": "id_img",
"title": "身份证",
"type": "str",
"length": 400,
"comments": "个人客户存身份证背面照片"
},
{
"name": "parentid",
"title": "父机构id",
"type": "str",
"length": 32
},
{
"name": "org_type",
"title": "机构类型",
"type": "str",
"length": 32,
"comments": "0:业主机构1:分销商2:公司客户3:个人客户4供应商"
},
{
"name": "accountid",
"title": "账号",
"type": "str",
"length": 32
}
] ]
filterjson = default_filterjson(fields, ns)
filterdic = ns.copy()
filterdic['filterstr'] = ''
filterdic['userorgid'] = '${userorgid}$'
filterdic['userid'] = '${userid}$'
if filterjson:
dbf = DBFilter(filterjson)
conds = dbf.gen(ns)
if conds:
ns.update(dbf.consts)
conds = f' and {conds}'
filterdic['filterstr'] = conds
ac = ArgsConvert('[[', ']]')
vars = ac.findAllVariables(sql)
NameSpace = {v:'${' + v + '}$' for v in vars if v != 'filterstr' }
filterdic.update(NameSpace)
sql = ac.convert(sql, filterdic)
debug(f'{sql=}')
db = DBPools()
dbname = get_module_dbname('rbac')
async with db.sqlorContext(dbname) as sor:
r = await sor.sqlPaging(sql, ns)
return r
return {
"total":0,
"rows":[]
}

151
wwwroot/get_reseller.dspy Normal file
View File

@ -0,0 +1,151 @@
ns = params_kw.copy()
debug(f'get_organization.dspy:{ns=}')
if not ns.get('page'):
ns['page'] = 1
if not ns.get('sort'):
ns['sort'] = 'orgname'
sql = '''select a.*, b.province_id_text, c.city_id_text, d.distinct_id_text
from (select x.* from (select * from organization where 1=1 [[filterstr]]) x, orgtypes y where x.id = y.orgid and y.orgtypeid='reseller') a left join (select k as province_id,
v as province_id_text from appcodes_kv where parentid='chnaddr') b on a.province_id = b.province_id left join (select k as city_id,
v as city_id_text from appcodes_kv where parentid='city') c on a.city_id = c.city_id left join (select k as distinct_id,
v as distinct_id_text from appcodes_kv where parentid='distinct') d on a.distinct_id = d.distinct_id'''
filterjson = params_kw.get('data_filter')
if not filterjson:
fields = [ f['name'] for f in [
{
"name": "id",
"title": "机构编码",
"type": "str",
"length": 32
},
{
"name": "orgname",
"title": "机构名称",
"type": "str",
"length": 100
},
{
"name": "alias_name",
"title": "机构别名",
"type": "str",
"length": 100
},
{
"name": "contactor",
"title": "联系人",
"type": "str",
"length": 32
},
{
"name": "contactor_phone",
"title": "联系人电话",
"type": "str",
"length": 100
},
{
"name": "province_id",
"title": "所在省id",
"type": "str",
"length": 32
},
{
"name": "city_id",
"title": "所在城市id",
"type": "str",
"length": 32
},
{
"name": "distinct_id",
"title": "所在地区id",
"type": "str",
"length": 32
},
{
"name": "emailaddress",
"title": "邮箱",
"type": "str",
"length": 256
},
{
"name": "address",
"title": "地址",
"type": "str",
"length": 400
},
{
"name": "main_business",
"title": "主营业务描述",
"type": "str",
"length": 1000
},
{
"name": "orgcode",
"title": "组织结构代码",
"type": "str",
"length": 100,
"comments": "个人客户存身份证"
},
{
"name": "license_img",
"title": "营业执照",
"type": "str",
"length": 400,
"comments": "个人客户存身份证照片"
},
{
"name": "id_img",
"title": "身份证",
"type": "str",
"length": 400,
"comments": "个人客户存身份证背面照片"
},
{
"name": "parentid",
"title": "父机构id",
"type": "str",
"length": 32
},
{
"name": "org_type",
"title": "机构类型",
"type": "str",
"length": 32,
"comments": "0:业主机构1:分销商2:公司客户3:个人客户4供应商"
},
{
"name": "accountid",
"title": "账号",
"type": "str",
"length": 32
}
] ]
filterjson = default_filterjson(fields, ns)
filterdic = ns.copy()
filterdic['filterstr'] = ''
filterdic['userorgid'] = '${userorgid}$'
filterdic['userid'] = '${userid}$'
if filterjson:
dbf = DBFilter(filterjson)
conds = dbf.gen(ns)
if conds:
ns.update(dbf.consts)
conds = f' and {conds}'
filterdic['filterstr'] = conds
ac = ArgsConvert('[[', ']]')
vars = ac.findAllVariables(sql)
NameSpace = {v:'${' + v + '}$' for v in vars if v != 'filterstr' }
filterdic.update(NameSpace)
sql = ac.convert(sql, filterdic)
debug(f'{sql=}')
db = DBPools()
dbname = get_module_dbname('rbac')
async with db.sqlorContext(dbname) as sor:
r = await sor.sqlPaging(sql, ns)
return r
return {
"total":0,
"rows":[]
}

121
wwwroot/user/login.ui Normal file
View File

@ -0,0 +1,121 @@
{
"id":"login_window",
"widgettype":"PopupWindow",
"options":{
"auto_open":true,
"anthor":"cc",
"cwidth":22,
"cheight":19
},
"subwidgets":[
{
"widgettype":"TabPanel",
"options":{
"tab_wide":"auto",
"height":"100%",
"width":"100%",
"tab_pos":"top",
"items":[
{
"name":"userpasswd",
"label":"用户密码",
"content":{
"widgettype":"Form",
"options":{
"cols":1,
"fields":[
{
"name":"username",
"label":"用户名",
"uitype":"str"
},
{
"name":"password",
"label":"密码",
"uitype":"password"
}
]
},
"binds":[
{
"wid":"self",
"event":"submit",
"actiontype":"urlwidget",
"target":"self",
"options":{
"method":"POST",
"url":"{{entire_url('up_login.dspy')}}"
}
}
]
}
},
{
"name":"checkcode",
"label":"手机验证码",
"content":{
"widgettype":"Form",
"options":{
"toolbar":{
"tools":[
{
"name":"gen_code",
"label":"发送验证码"
}
]
},
"description":"限中国国内手机",
"fields":[
{
"name":"cell_no",
"label":"手机号",
"uitype":"str"
},{
"name":"codeid",
"uitype":"hide",
"value":"{{uuid()}}"
},{
"name":"check_code",
"uitype":"str"
}
]
},
"binds":[
{
"wid":"self",
"event":"gen_code",
"actiontype":"urlwidget",
"datawidget":"self",
"datamethod":"getValue",
"target":"self",
"options":{
"url":"{{entire_url('gen_code.dspy')}}"
}
},
{
"wid":"self",
"event":"submit",
"actiontype":"urlwidget",
"target":"self",
"options":{
"url":"{{entire_url('code_login.dspy')}}"
}
}
]
}
},
{
"name":"wechat",
"label":"微信",
"content":{
"widgettype":"urlwidget",
"options":{
"url":"{{entire_url('wechat_login.ui')}}"
}
}
}
]
}
}
]
}

23
wwwroot/user/logout.dspy Normal file
View File

@ -0,0 +1,23 @@
await forget_user()
return {
"widgettype":"Message",
"options":{
"title":"Message",
"message":"logout success",
"auto_open":true,
"anthor":"cc",
"timeout":3
},
"binds":[
{
"wid":"self",
"event":"dismissed",
"actiontype":"urlwidget",
"target":"window",
"options":{
"url":entire_url('/index.ui')
}
}
]
}

27
wwwroot/user/myrole.ui Normal file
View File

@ -0,0 +1,27 @@
{
"widgettype":"VBox",
"options":{
"height":"100%"
},
"subwidgets":[
{
"widgettype":"Title4",
"options":{
"otext":"我的角色",
"i18n":true
}
},
{% set roles = get_user_roles(get_user()) %}
{% for role in roles %}
{
"widgettype":"Text",
"options":{
"text":"{{role}}"
}
},
{% endfor %}
{
"oops":true
}
]
}

View File

@ -0,0 +1,10 @@
debug(f'{params_kw=}')
db = DBPools()
dbname = await rfexe('get_module_dbname', 'sage')
async with db.sqlorContext(dbname) as sor:
orgid = await register_user(sor, params_kw)
if get_owner_orgid and openCustomerAccounts:
ownerid = await get_owner_orgid(sor, orgid)
await openCustomerAccounts(sor, ownerid, orgid)
return UiMessage(title="Success", message="register success")
return UiError(title='Error', message="register failed")

80
wwwroot/user/register.ui Normal file
View File

@ -0,0 +1,80 @@
{
"widgettype":"PopupWindow",
"options":{
"cwidth":22,
"height":"75%",
"archor":"cc",
"auto_open":true
},
"subwidgets":[
{
"widgettype":"Form",
"options":{
"title":"user register",
"description":"base info we need is username, password and a cell phone number",
"fields":[
{
"name": "username",
"title": "\u7528\u6237\u540d",
"type": "str",
"length": 255,
"uitype": "str",
"datatype": "str",
"required":true,
"label": "\u7528\u6237\u540d"
},
{
"name": "password",
"type": "str",
"length": 255,
"uitype": "password",
"datatype": "str",
"required":true,
"label": "\u5bc6\u7801"
},
{
"name": "cfm_password",
"type": "str",
"length": 255,
"uitype": "password",
"datatype": "str",
"required":true,
"label": "\u5bc6\u7801"
},
{
"name": "email",
"title": "\u90ae\u4ef6\u5730\u5740",
"type": "str",
"length": 255,
"uitype": "str",
"datatype": "str",
"required":true,
"label": "\u90ae\u4ef6\u5730\u5740"
},
{
"name": "mobile",
"title": "\u624b\u673a",
"type": "str",
"length": 255,
"uitype": "str",
"datatype": "str",
"required":true,
"label": "\u624b\u673a"
}
]
},
"binds":[
{
"wid":"self",
"event":"submit",
"actiontype":"urlwidget",
"target":"self",
"options":{
"url":"{{entire_url('register.dspy')}}"
}
}
]
}
]
}

View File

@ -0,0 +1,49 @@
{
"widgettype":"PopupWindow",
"options":{
"cwidth":22,
"height":"75%",
"archor":"cc",
"auto_open":true
},
"subwidgets":[
{
"widgettype":"Form",
"options":{
"title":"Reset Password",
"description":"reset yourself password",
"fields":[
{
"name": "password",
"type": "str",
"length": 255,
"uitype": "password",
"datatype": "str",
"required":true,
"label": "\u5bc6\u7801"
},
{
"name": "cfm_password",
"type": "str",
"length": 255,
"uitype": "password",
"datatype": "str",
"required":true,
"label": "\u5bc6\u7801"
}
]
},
"binds":[
{
"wid":"self",
"event":"submit",
"actiontype":"urlwidget",
"target":"self",
"options":{
"url":"{{entire_url('reset_password.dspy')}}"
}
}
]
}
]
}

View File

@ -0,0 +1,17 @@
if params_kw.password != params_kw.cfm_password:
return UiError(title='Error', message='Password not match')
userid = await get_user()
if userid is None:
return UiError(title='Error', message='You need login first')
ns = {
'id':userid,
'password':params_kw.password
}
db = DBPools()
dbname = await rfexe('get_module_dbname', 'rbac')
async with db.sqlorContext(dbname) as sor:
await sor.U('users', ns)
return UiMessage(title='Success', message='Password reset success')
return UiError(title='Error', message='Reset password failed')

View File

@ -0,0 +1,58 @@
debug(f'{params_kw=}, {password=}')
ns = {
"username":params_kw.username,
"password":password_encode(params_kw.password)
}
info(f'{ns=}')
db = DBPools()
dbname = await rfexe('get_module_dbname', 'rbac')
async with db.sqlorContext(dbname) as sor:
r = await sor.sqlExe('select * from users where username=${username}$ and password=${password}$', ns.copy())
if len(r) == 0:
return {
"widgettype":"Error",
"options":{
"timeout":3,
"title":"Login Error",
"message":"user name or password error"
}
}
await remember_user(r[0].id, username=r[0].username, userorgid=r[0].orgid)
return {
"widgettype":"Message",
"options":{
"timeout":3,
"auto_open":true,
"title":"Login",
"message":"Welcome back"
},
"binds":[
{
"wid":"self",
"event":"dismissed",
"actiontype":"urlwidget",
"target":"window",
"options":{
"url":entire_url('/index.ui')
}
},
{
"wid":"self",
"event":"opened",
"actiontype":"script",
"target":"window.login_window",
"script":"this.destroy()"
}
]
}
return {
"widgettype":"Error",
"options":{
"timeout":3,
"title":"Login Error",
"message":"system error"
}
}

80
wwwroot/user/user.ui Normal file
View File

@ -0,0 +1,80 @@
{% if get_user() %}
{
"widgettype":"HBox",
"options":{
"css":"clickable",
"tip":"用户功能",
"cwidth":6
},
"binds":[
{
"wid":"self",
"event":"click",
"actiontype":"urlwidget",
"popup_options":{
"eventpos":true,
"dismiss_events":["command"]
},
"target":"Popup",
"options":{
"url":"{{entire_url('user_menu.ui')}}"
}
}
],
"subwidgets":[
{
"widgettype":"Svg",
"options":{
"url":"{{entire_url('/bricks/imgs/user.svg')}}",
"rate":1.5
}
},
{
"widgettype":"Text",
"options":{
"cwidth": 4,
"text":"{{get_username()}}"
}
}
]
}
{% else %}
{
"widgettype":"IconBar",
"options":{
"css":"filler",
"tools":[
{
"name":"login",
"tip":"点击登录",
"icon":"{{entire_url('/bricks/imgs/login.svg')}}"
},
{
"name":"register",
"tip":"点击注册",
"icon":"{{entire_url('/bricks/imgs/register.svg')}}"
}
]
},
"binds":[
{
"wid":"self",
"event":"login",
"actiontype":"urlwidget",
"target":"self",
"options":{
"url":"{{entire_url('login.ui')}}"
}
},
{
"wid":"self",
"event":"register",
"actiontype":"urlwidget",
"target":"self",
"options":{
"url":"{{entire_url('register.ui')}}"
}
}
]
}
{% endif %}

55
wwwroot/user/user_menu.ui Normal file
View File

@ -0,0 +1,55 @@
{
"widgettype":"Menu",
"options":{
"cwidth":10,
"target":"PopupWindow",
"popup_options":{
"height":"70%",
"width":"70%"
},
"items":[
{% if 'customer.customer' in get_user_roles(get_user()) %}
{
"name":"recharge",
"label":"充值",
"url":"{{entire_url('/platformbiz/recharge.ui')}}"
},
{
"name":"myacc",
"label":"我的账户",
"url":"{{entire_url('myaccount')}}"
},
{
"name":"mybill",
"label":"我的账单",
"url":"{{entire_url('mybill')}}"
},
{
"name":"myorder",
"label":"我的订单",
"url":"{{entire_url('myorder')}}"
},
{
"name":"myresource",
"label":"我的资源",
"url":"{{entire_url('myresource')}}"
},
{% endif %}
{
"name":"resetpwd",
"label":"重置密码",
"url":"{{entire_url('reset_password')}}"
},
{
"name":"myrole",
"label":"我的角色",
"url":"{{entire_url('myrole.ui')}}"
},
{
"name":"logout",
"label":"签退",
"url":"{{entire_url('logout.dspy')}}"
}
]
}
}

View File

@ -0,0 +1,15 @@
{
"id":"user_panel",
"widgettype":"VBox",
"options":{
"width":"auto"
},
"subwidgets":[
{
"widgettype":"urlwidget",
"options":{
"url":"{{entire_url('user.ui')}}"
}
}
]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
{
"widgettype":"VBox",
"options":{
}
}

View File

@ -0,0 +1,11 @@
username = params_kw.get('username')
passwd = params_kw.get('passwd')
if not passwd:
return UiError(title='Login failed', message='Password is required')
passwd = password(passwd)
rzt = await check_user_password(request, username, passwd)
if rzt:
return UiMessage(title='Logined', message=f'Welcome back ')
return UiError(title='login failed', message='user and password mismatch')

View File

@ -0,0 +1,43 @@
{
"id":"login_window",
"widgettype":"PopupWindow",
"options":{
"auto_open":true,
"anthor":"cc",
"cwidth":20,
"cheight":"14"
},
"subwidgets":[
{
"widgettype":"Form",
"id":"userpasswd",
"options":{
"cols":1,
"fields":[
{
"name":"username",
"label":"用户名",
"uitype":"str"
},
{
"name":"passwd",
"label":"密码",
"uitype":"password"
}
]
}
}
],
"binds":[
{
"wid":"userpasswd",
"event":"submit",
"actiontype":"urlwidget",
"target":"self",
"options":{
"url":"{{entire_url('userpassword_login.dspy')}}"
}
}
]
}