diff --git a/backend/controller/__init__.py b/backend/controller/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/backend/controller/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/backend/controller/common.py b/backend/controller/common.py deleted file mode 100644 index f58f775..0000000 --- a/backend/controller/common.py +++ /dev/null @@ -1,36 +0,0 @@ -import asyncio - -from fastapi import APIRouter -from starlette.websockets import WebSocket -from websockets.exceptions import WebSocketException - -from core.security import generate_token, verify_password -from core.utils import get_system_info -from dbhelper.user import get_user -from schemas import LoginForm, LoginResult, Response - -router = APIRouter(tags=["公共"]) - - -@router.post("/login", summary="登录", response_model=Response[LoginResult]) -async def login(auth_data: LoginForm): - user_obj = await get_user({"username": auth_data.username, "status__not": 9}) - if user_obj: - if verify_password(auth_data.password, user_obj.password): - return Response( - data=LoginResult( - id=user_obj.id, token=generate_token(auth_data.username) - ) - ) - return Response(code=400, msg="账号或密码错误") - - -@router.websocket("/ws", name="系统信息") -async def websocket(ws: WebSocket): - await ws.accept() - try: - while True: - await asyncio.sleep(1) - await ws.send_json(get_system_info()) - except WebSocketException: - await ws.close() diff --git a/backend/controller/menu.py b/backend/controller/menu.py deleted file mode 100644 index 714cbe9..0000000 --- a/backend/controller/menu.py +++ /dev/null @@ -1,40 +0,0 @@ -# router service db router+service db -from fastapi import APIRouter - -from core.utils import list_to_tree -from dbhelper.menu import del_menu, get_menu, get_tree_menu, insert_menu, put_menu -from schemas import MenuIn, MenuRead, Response - -router = APIRouter(prefix="/menu", tags=["菜单管理"]) - - -@router.post("", summary="菜单新增", response_model=Response[MenuRead]) -async def menu_add(data: MenuIn): - return Response(data=await insert_menu(data)) - - -@router.get("", summary="菜单列表", response_model=Response) -async def menu_arr(): - menus = await get_tree_menu() - try: - data = list_to_tree(menus) - except KeyError: - return Response(code=400, msg="菜单根节点丢失") - return Response(data=data) - - -@router.delete("/{pk}", summary="菜单删除", response_model=Response) -async def menu_del(pk: int): - if await get_menu({"pid": pk}) is not None: - return Response(code=400, msg="请先删除子节点") - if await del_menu(pk) == 0: - return Response(code=400, msg="菜单不存在") - return Response() - - -@router.put("/{pk}", summary="菜单更新", response_model=Response) -async def menu_put(pk: int, data: MenuIn): - """更新菜单""" - if await put_menu(pk, data) == 0: - return Response(code=400, msg="菜单不存在") - return Response() diff --git a/backend/controller/role.py b/backend/controller/role.py deleted file mode 100644 index ec25f84..0000000 --- a/backend/controller/role.py +++ /dev/null @@ -1,78 +0,0 @@ -from fastapi import APIRouter, Query - -from core.utils import list_to_tree -from dbhelper.menu import get_menu -from dbhelper.role import ( - del_role, - get_role, - get_role_menus, - get_roles, - new_role, - put_role, -) -from schemas import ListAll, Response, RoleIn, RoleInfo, RoleQuery, RoleRead - -router = APIRouter(prefix="/role", tags=["角色管理"]) - - -@router.post("", summary="角色新增", response_model=Response[RoleInfo]) -async def role_add(data: RoleIn): - if result := await new_role(data): - return Response(data=result) - return Response(code=400, msg="菜单不存在") - - -@router.get("/{rid}/menu", summary="查询角色拥有权限", response_model=Response) -async def role_has_menu(rid: int): - """ - rid: 角色ID - """ - menus = await get_role_menus(rid) - - try: - result = list_to_tree(menus) - except KeyError: - return Response(code=400, msg="菜单缺少根节点.") - return Response(data=result) - - -@router.get("", summary="角色列表", response_model=Response[ListAll[list[RoleRead]]]) -async def role_arr( - offset: int = Query(default=1, description="偏移量-页码"), - limit: int = Query(default=10, description="数据量"), -): - skip = (offset - 1) * limit - roles, count = await get_roles(skip, limit) - return Response(data=ListAll(total=count, items=roles)) - - -@router.delete("/{pk}", summary="角色删除", response_model=Response) -async def role_del(pk: int): - if await del_role(pk) == 0: - return Response(code=400, msg="角色不存在") - return Response() - - -@router.put("/{pk}", summary="角色更新", response_model=Response) -async def role_put(pk: int, data: RoleIn): - """更新角色""" - if await get_role({"id": pk}) is None: - - return Response(code=400, msg="角色不存在") - # 如果不为ture -> 有菜单id不存在 - if not all([await get_menu({"id": mid}) for mid in data.menus]): - return Response(code=400, msg="菜单不存在") - - if await put_role(pk, data) == 0: - return Response(code=400, msg="角色不存在") - return Response() - - -@router.post("/query", summary="角色查询", response_model=Response[ListAll[list[RoleRead]]]) -async def role_query(query: RoleQuery): - """post条件查询角色表""" - size = query.limit - skip = (query.offset - 1) * size - del query.offset, query.limit - users, count = await get_roles(skip, size, query.dict()) - return Response(data=ListAll(total=count, items=users)) diff --git a/backend/controller/user.py b/backend/controller/user.py deleted file mode 100644 index f425f4e..0000000 --- a/backend/controller/user.py +++ /dev/null @@ -1,89 +0,0 @@ -from fastapi import APIRouter, Depends, Query - -from core.security import check_permissions, get_password_hash -from dbhelper.user import ( - del_user, - get_user, - get_user_info, - get_users, - insert_user, - put_user, - select_role, -) -from schemas import Response, UserAdd, UserInfo, UserPut, UserQuery, UserRead -from schemas.common import ListAll - -router = APIRouter(prefix="/user", tags=["用户管理"]) - - -@router.post("", summary="用户新增", response_model=Response[UserRead]) -async def user_add(data: UserAdd): - """新增用户并分配角色 一步到位""" - if await get_user({"username": data.username}) is not None: - return Response(code=400, msg="用户名已存在") - rids = data.roles - del data.roles - data.password = get_password_hash(data.password) - result = await insert_user(data, rids) - if isinstance(result, int): - return Response(code=400, msg=f"角色{result}不存在") - return Response(data=result) - - -@router.get("/{pk}", summary="用户信息", response_model=Response[UserInfo]) -async def user_info(pk: int): - """获取用户信息""" - obj = await get_user({"id": pk}) - if obj is None: - return Response(code=400, msg="用户不存在") - return Response(data=await get_user_info(obj)) - - -@router.get("", summary="用户列表", response_model=Response[ListAll[list[UserRead]]]) -async def user_arr( - offset: int = Query(default=1, description="偏移量-页码"), - limit: int = Query(default=10, description="数据量"), -): - """分页列表数据""" - skip = (offset - 1) * limit - users, count = await get_users(skip, limit) - return Response(data=ListAll(total=count, items=users)) - - -@router.post("/query", summary="用户查询", response_model=Response[ListAll[list[UserRead]]]) -async def user_list(query: UserQuery): - """post查询用户列表""" - size = query.limit - skip = (query.offset - 1) * size - del query.offset, query.limit - users, count = await get_users(skip, size, query.dict()) - return Response(data=ListAll(total=count, items=users)) - - -@router.delete("/{pk}", summary="用户删除", response_model=Response) -async def user_del(pk: int): - """删除用户""" - if await del_user(pk) == 0: - return Response(code=400, msg="用户不存在") - return Response() - - -@router.put("/{pk}", summary="用户更新", response_model=Response) -async def user_put(pk: int, data: UserPut): - """更新用户""" - if await get_user({"id": pk}) is None: - return Response(code=400, msg="用户不存在") - - result = await put_user(pk, data) - if isinstance(result, int): - return Response(code=400, msg=f"角色不存在{result}") - return Response() - - -@router.put("/role/{rid}", summary="用户切换角色", response_model=Response) -async def user_select_role(rid: int, user=Depends(check_permissions)): - """用户切换角色""" - res = await select_role(user.id, rid) - if res == 0: - return Response(code=400, msg=f"角色不存在{res}") - return Response() diff --git a/backend/core/dbhelper.py b/backend/core/dbhelper.py index cd768f1..dfcb26f 100644 --- a/backend/core/dbhelper.py +++ b/backend/core/dbhelper.py @@ -1,14 +1,14 @@ """数据库通用查询方法""" -from typing import Optional +from tortoise import connections -from tortoise import connections, models +from models import MenuModel, RoleMenuModel, RoleModel, UserModel, UserRoleModel class DbHelper: - def __init__(self, model: models.Model): + def __init__(self, model): """ 初始化 - :param model: 模型类 + :param model: 模型类 orm model """ self.model = model @@ -20,7 +20,7 @@ class DbHelper: """ return self.model.filter(**kwargs) - async def select(self, kwargs: dict = None) -> Optional[models.Model]: + async def select(self, kwargs: dict = None): """ 查询符合条件的第一个对象, 查无结果时返回None :param kwargs: kwargs: {"name:"7y", "id": 1} @@ -58,7 +58,7 @@ class DbHelper: return await self.model.create(**data) async def selects( - self, offset: int, limit: int, kwargs: dict = None, order_by: str = None + self, offset: int, limit: int, kwargs: dict = None, order_by: str = "-created" ) -> dict: """ 条件分页查询数据列表, 支持排序 @@ -81,13 +81,13 @@ class DbHelper: items=await objs.offset(offset).limit(limit), total=await objs.count() ) - async def inserts(self, objs: list[models.Model]): + async def inserts(self, objs: list): """ 批量新增数据 :param objs: 模型列表 :return: """ - await self.model.bulk_create(objs) + await self.model.bulk_create([self.model(**obj) for obj in objs]) @classmethod async def raw_sql(cls, sql: str, args: list = None): @@ -101,3 +101,48 @@ class DbHelper: if args is None: args = [] return await db.execute_query_dict(sql, args) + + +UserDao = DbHelper(UserModel) +RoleDao = DbHelper(RoleModel) +UserRoleDao = DbHelper(UserRoleModel) +MenuDao = DbHelper(MenuModel) +RoleMenuDao = DbHelper(RoleMenuModel) + + +async def has_roles(uid): + """ + 获取用户角色信息,激活的角色升序 + :param uid: 用户id + :return: + """ + sql = """select r.id, r.name, ur.status from sys_role as r , sys_user_role as ur where r.id = ur.rid and + ur.uid = (?) and r.status = 1 and ur.status !=9 order by ur.status desc + """ + return await UserRoleDao.raw_sql(sql, [uid]) + + +async def has_user(username): + """ + 通过用户名检索数据是否存在 + :param username: + :return: + """ + return await UserDao.select({"username": username, "status__not": 9}) + + +async def has_permissions(rid, is_menu=False): + """ + 根据角色ID查到当前拥有的接口权限 + :param rid: 角色ID + :param is_menu: 是否是菜单,默认不是 -》接口 + :return: + """ + filters = "m.api, m.method" + if is_menu: + filters = "m.id, m.name, m.icon, m.path, m.type, m.component, m.pid, m.identifier, m.api, m.method" + sql = f""" + select {filters} + FROM sys_menu as m, sys_role_menu as srm WHERE m.id = srm.mid + AND srm.rid = (?) and m.status != 9 order by m.id asc""" + return await RoleMenuDao.raw_sql(sql, [rid]) diff --git a/backend/core/log.py b/backend/core/log.py index e46aa57..fcc3e27 100644 --- a/backend/core/log.py +++ b/backend/core/log.py @@ -1,15 +1 @@ -import logging -import sys - -fmt = logging.Formatter( - fmt="%(asctime)s - %(name)s:%(lineno)d - %(levelname)s - %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", -) -sh = logging.StreamHandler(sys.stdout) -sh.setLevel(logging.DEBUG) -sh.setFormatter(fmt) - -# will print debug sql -logger = logging.getLogger("mini-rbac") -logger.setLevel(logging.DEBUG) -logger.addHandler(sh) +from loguru import logger diff --git a/backend/core/middleware.py b/backend/core/middleware.py index 446232a..9687341 100644 --- a/backend/core/middleware.py +++ b/backend/core/middleware.py @@ -1,12 +1,28 @@ from fastapi.middleware import Middleware from fastapi.middleware.cors import CORSMiddleware +from starlette.middleware.base import BaseHTTPMiddleware + +from core.log import logger + + +class CustomRequestLogMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request, call_next): + logger.info( + f"Client: {request.client} Method: {request.method} " + f"Path: {request.url} Headers: {request.headers}" + ) + # python-multipart == await request.form() + response = await call_next(request) + return response + middlewares = [ + Middleware(CustomRequestLogMiddleware), Middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], - ) + ), ] diff --git a/backend/core/security.py b/backend/core/security.py index f6f0d1b..ef748a3 100644 --- a/backend/core/security.py +++ b/backend/core/security.py @@ -6,10 +6,8 @@ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from jose import JWTError, jwt from passlib.context import CryptContext +from core.dbhelper import has_permissions, has_roles, has_user from core.exceptions import PermissionsError, TokenAuthFailure -from dbhelper.menu import get_apis -from dbhelper.user import get_user, get_user_info -from models import UserModel # JWT SECRET_KEY = "lLNiBWPGiEmCLLR9kRGidgLY7Ac1rpSWwfGzTJpTmCU" @@ -59,20 +57,20 @@ async def check_token(security: HTTPAuthorizationCredentials = Depends(bearer)): try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) username: str = payload.get("sub") - return await get_user({"username": username}) + return await has_user(username) except JWTError: raise TokenAuthFailure(403, "认证失败") -async def check_permissions(request: Request, user: UserModel = Depends(check_token)): +async def check_permissions(request: Request, user=Depends(check_token)): """检查接口权限""" # 查询当前激活角色 - result = await get_user_info(user) - active_rid = result["roles"][0]["id"] + roles = await has_roles(user.id) + active_rid = roles[0]["id"] # 白名单 登录用户信息, 登录用户菜单信息 whitelist = [(f"/user/{user.id}", "GET"), (f"/role/{active_rid}/menu", "GET")] + [ - (f"/user/role/{rid['id']}", "PUT") for rid in result["roles"] + (f"/user/role/{rid['id']}", "PUT") for rid in roles ] if (request.url.path, request.method) in whitelist: @@ -82,11 +80,11 @@ async def check_permissions(request: Request, user: UserModel = Depends(check_to for k, v in request.path_params.items(): api = api.replace(v, "{%s}" % k) - # 2. 登录之后查一次 后面去结果查 todo 更新权限时需要更新 , 最好结果放redis + # todo 结果放redis cache_key = f"{user.username}_{active_rid}" # 缓存到fastapi 应用实例中 if not hasattr(request.app.state, cache_key): - setattr(request.app.state, cache_key, await get_apis(active_rid)) + setattr(request.app.state, cache_key, await has_permissions(active_rid)) if {"api": api, "method": request.method} not in getattr( request.app.state, cache_key ): diff --git a/backend/core/service.py b/backend/core/service.py new file mode 100644 index 0000000..ef02b88 --- /dev/null +++ b/backend/core/service.py @@ -0,0 +1,63 @@ +from core.dbhelper import DbHelper + + +class Service: + + filter_del = {"status__not": 9} + + def __init__(self, dao: DbHelper): + self.dao = dao + + async def get_items(self, offset, limit): + """ + 分页获取数据, 过滤掉删除 + :param offset: 起始值 + :param limit: 偏移量 + :return: + """ + skip = (offset - 1) * limit + return dict(data=await self.dao.selects(skip, limit, Service.filter_del)) + + async def query_items(self, query): + """ + 根据条件查询结果 + :param query: + :return: + """ + size = query.limit + skip = (query.offset - 1) * size + del query.offset, query.limit + filters = {f"{k}__contains": v for k, v in query.dict().items()} + filters.update(Service.filter_del) + return dict(data=await self.dao.selects(skip, size, filters)) + + async def delete_item(self, pk): + """ + 逻辑删除数据 + :param pk:主键 + :return: + """ + filters = {"id": pk} + filters.update(Service.filter_del) + if await self.dao.update(filters, {"status": 9}) == 0: + return dict(code=400, msg="数据不存在") + return dict() + + async def update_item(self, pk, data): + """ + 更新数据,不通用,可重写 + :param pk: 主键 + :param data: pydantic model + :return: + """ + if await self.dao.update({"id": pk}, data.dict()) == 0: + return dict(code=400, msg="数据不存在") + return dict() + + async def create_item(self, data): + """ + 创建数据,不通用可重写 + :param data: pydantic model + :return: + """ + return await self.dao.insert(data.dict()) diff --git a/backend/dbhelper/menu.py b/backend/dbhelper/menu.py deleted file mode 100644 index aae779d..0000000 --- a/backend/dbhelper/menu.py +++ /dev/null @@ -1,66 +0,0 @@ -from tortoise import connections - -from models import MenuModel -from schemas.menu import MenuIn - - -async def insert_menu(menu: MenuIn): - """新增菜单""" - return await MenuModel.create(**menu.dict()) - - -async def get_menus(skip: int, limit: int, kwargs: dict = None): - """ - 分页获取用户并且支持字段模糊查询 - Args: - skip: 偏移量 - limit: 数量 - kwargs: 查询字典 - - Returns: - - """ - if kwargs is not None: - kwargs = {f"{k}__contains": v for k, v in kwargs.items()} - else: - kwargs = {} - result = MenuModel.filter(status__not=9, **kwargs).all().order_by("-created") - return await result.offset(skip).limit(limit) - - -async def get_tree_menu(): - return await MenuModel.filter(status__not=9).all().values() - - -async def get_menu(kwargs): - """ - 根据条件查询到第一条符合结果的数据 - Args: - kwargs: - - Returns: - - """ - return await MenuModel.filter(**kwargs).first() - - -async def del_menu(mid: int): - """删除菜单""" - return await MenuModel.filter(id=mid).update(status=9) - - -async def get_apis(pk: int): - """返回当前角色拥有的接口权限列表""" - db = connections.get("default") - return await db.execute_query_dict( - """ - select m.api, m.method - FROM sys_menu as m, sys_role_menu as srm WHERE m.id = srm.mid - AND srm.rid = (?) and m.status != 9""", - [pk], - ) - - -async def put_menu(pk: int, data): - """更新菜单""" - return await MenuModel.filter(id=pk).update(**data.dict()) diff --git a/backend/dbhelper/role.py b/backend/dbhelper/role.py deleted file mode 100644 index 1124346..0000000 --- a/backend/dbhelper/role.py +++ /dev/null @@ -1,79 +0,0 @@ -from tortoise import connections - -from models import MenuModel, RoleMenuModel, RoleModel -from schemas.role import RoleIn - - -async def get_role_menus(rid: int): - """ - 根据角色id 获取菜单 - """ - db = connections.get("default") - # asc 降序 - return await db.execute_query_dict( - """ - select m.id, m.name, m.icon, m.path, m.type, m.component, m.pid, m.identifier, m.api, m.method - FROM sys_menu as m, sys_role_menu WHERE m.id = sys_role_menu.mid - AND sys_role_menu.rid = (?) AND sys_role_menu.`status` = 1 order by m.id asc""", - [rid], - ) - - -async def new_role(role: RoleIn): - """新增角色""" - # 校验菜单是否存在 - if not all([await MenuModel.filter(id=mid).first() for mid in role.menus]): - return False - - obj = await RoleModel.create(name=role.name, remark=role.remark) - # 写入菜单 - await RoleMenuModel.bulk_create( - [RoleMenuModel(rid=obj.id, mid=mid) for mid in role.menus] - ) - return obj - - -async def get_roles(skip: int, limit: int, kwargs: dict = None): - """ - 分页获取用户并且支持字段模糊查询 - Args: - skip: 偏移量 - limit: 数量 - kwargs: 查询字典 - - Returns: - - """ - if kwargs is not None: - kwargs = {f"{k}__contains": v for k, v in kwargs.items()} - else: - kwargs = {} - result = RoleModel.filter(**kwargs).all().order_by("-created") - return await result.offset(skip).limit(limit), await result.count() - - -async def get_role(kwargs): - """ - 根据条件查询到第一条符合结果的数据 - Args: - kwargs: - - Returns: - - """ - return await RoleModel.filter(**kwargs).first() - - -async def del_role(rid: int): - """删除用户""" - return await RoleModel.filter(id=rid).update(status=9) - - -async def put_role(pk, data): - """更新角色 菜单""" - await RoleModel.filter(id=pk).update(name=data.name, remark=data.remark) - await RoleMenuModel.filter(rid=pk).update(status=9) - - await RoleMenuModel.bulk_create( - [RoleMenuModel(rid=pk, mid=mid) for mid in data.menus] - ) diff --git a/backend/dbhelper/user.py b/backend/dbhelper/user.py deleted file mode 100644 index e979ec8..0000000 --- a/backend/dbhelper/user.py +++ /dev/null @@ -1,121 +0,0 @@ -from fastapi.encoders import jsonable_encoder -from tortoise import connections - -from dbhelper.role import get_role -from models import UserModel, UserRoleModel -from schemas import UserPut - - -async def get_user(kwargs): - """ - 根据条件查询到第一条符合结果的数据 - Args: - kwargs: - - Returns: - - """ - return await UserModel.filter(**kwargs).first() - - -async def get_user_info(user: UserModel): - """ - 根据id查用户角色列表 按激活角色倒序显示 - """ - db = connections.get("default") - # 查角色表 用户角色表中 角色状态 = 1, 关联表中 状态 != 9 为有效角色 - sql_result = await db.execute_query_dict( - """ - select r.id, r.name, ur.status from sys_role as r , sys_user_role as ur where r.id = ur.rid and - ur.uid = (?) and r.status = 1 and ur.status !=9 order by ur.status desc - """, - [user.id], - ) - return { - **jsonable_encoder(user), - "roles": sql_result, - } - - -async def get_users(skip: int, limit: int, kwargs: dict = None): - """ - 分页获取用户并且支持字段模糊查询 - Args: - skip: 偏移量 - limit: 数量 - kwargs: 查询字典 - - Returns: - - """ - if kwargs is not None: - kwargs = {f"{k}__contains": v for k, v in kwargs.items()} - else: - kwargs = {} - result = UserModel.filter(**kwargs).all().order_by("-created") - return await result.offset(skip).limit(limit), await result.count() - - -async def insert_user(user, roles): - """新增用户,选择角色""" - for role in roles: - if await get_role({"id": role.rid, "status__not": 9}) is None: - return role.rid - - # 创建用户 - obj = await UserModel.create(**user.dict()) - # 已有角色 关联 角色id 和是否选中状态 - await UserRoleModel.bulk_create( - [UserRoleModel(rid=role.rid, uid=obj.id, status=role.status) for role in roles] - ) - return obj - - -async def del_user(uid: int): - """删除用户""" - return await UserModel.filter(id=uid).update(status=9) - - -async def put_user(uid: int, data: UserPut): - """更新用户""" - from core.security import get_password_hash - - rids = data.roles - del data.roles - for role in rids: - if await get_role({"id": role.rid, "status__not": 9}) is None: - return role.rid - # 更新用户 - if data.password != "加密之后的密码": - data.password = get_password_hash(data.password) - else: - del data.password - await UserModel.filter(id=uid).update(**data.dict()) - - # todo 1. 先前有的角色,这次更新成没有 2. 先前没有的角色 这次更新成有, 3. 只更新了状态 - - db = connections.get("default") - # 1. 先把用户有的角色做删除 - has_roles = await db.execute_query_dict( - """ - select r.id from sys_role as r , sys_user_role as ur where r.id = ur.rid and - ur.uid = (?) and r.status = 1 and ur.status !=9 - """, - [uid], - ) - - # 2. 将先有的数据标记 删除 - [await UserRoleModel.filter(rid=role["id"]).update(status=9) for role in has_roles] - - # 2. 新增次此更新的数据 - await UserRoleModel.bulk_create( - [UserRoleModel(uid=uid, **role.dict()) for role in rids] - ) - - -async def select_role(uid: int, rid: int): - """用户切换角色""" - # 1.将用户id 未删除角色状态置为正常 1 ( 除切换角色id ) - await UserRoleModel.filter(uid=uid, rid__not=rid, status__not=9).update(status=1) - # 2.将用户id 角色id 和当前角色匹配的数据置为选中 - return await UserRoleModel.filter(uid=uid, rid=rid, status__not=9).update(status=5) diff --git a/backend/main.py b/backend/main.py index 5d1cb97..31e93dc 100644 --- a/backend/main.py +++ b/backend/main.py @@ -13,9 +13,7 @@ app = FastAPI( exception_handlers=exception_handlers, ) -load_routers( - app, "controller", no_depends="common", depends=[Depends(check_permissions)] -) +load_routers(app, "router", no_depends="auth", depends=[Depends(check_permissions)]) if __name__ == "__main__": import uvicorn diff --git a/backend/mini.db b/backend/mini.db deleted file mode 100644 index e2a2455..0000000 Binary files a/backend/mini.db and /dev/null differ diff --git a/backend/mini.db-shm b/backend/mini.db-shm deleted file mode 100644 index d7e2633..0000000 Binary files a/backend/mini.db-shm and /dev/null differ diff --git a/backend/mini.db-wal b/backend/mini.db-wal deleted file mode 100644 index 7f21fe6..0000000 Binary files a/backend/mini.db-wal and /dev/null differ diff --git a/backend/requirements.txt b/backend/requirements.txt index 8e3034a..6c09b2b 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -7,3 +7,4 @@ requests==2.28.1 tortoise-orm==0.19.2 uvicorn==0.18.3 websockets==10.3 +loguru==0.6.0 diff --git a/backend/dbhelper/__init__.py b/backend/router/__init__.py similarity index 100% rename from backend/dbhelper/__init__.py rename to backend/router/__init__.py diff --git a/backend/router/auth.py b/backend/router/auth.py new file mode 100644 index 0000000..694e3c0 --- /dev/null +++ b/backend/router/auth.py @@ -0,0 +1,19 @@ +from fastapi import APIRouter, WebSocket + +from schemas import common as BaseSchema +from service import auth as AuthService + +router = APIRouter(tags=["公共"]) + + +LoginResult = BaseSchema.Response[BaseSchema.LoginResult] + + +@router.post("/login", summary="登录", response_model=LoginResult) +async def login(data: BaseSchema.LoginForm): + return await AuthService.user_login(data) + + +@router.websocket("/ws", name="系统信息") +async def get_system_info(ws: WebSocket): + await AuthService.system_info(ws) diff --git a/backend/router/menu.py b/backend/router/menu.py new file mode 100644 index 0000000..e2381f3 --- /dev/null +++ b/backend/router/menu.py @@ -0,0 +1,30 @@ +from fastapi import APIRouter + +from schemas import common as BaseSchema +from schemas import menu as MenuSchema +from service.menu import service as MenuService + +router = APIRouter(prefix="/menu", tags=["菜单管理"]) + +Response = BaseSchema.Response + + +@router.post("", summary="菜单新增", response_model=Response[MenuSchema.MenuRead]) +async def menu_add(data: MenuSchema.MenuIn): + return await MenuService.create_item(data) + + +@router.get("", summary="菜单列表", response_model=Response) +async def menu_arr(): + return await MenuService.get_items() + + +@router.delete("/{pk}", summary="菜单删除", response_model=Response) +async def menu_del(pk: int): + return await MenuService.delete_item(pk) + + +@router.put("/{pk}", summary="菜单更新", response_model=Response) +async def menu_put(pk: int, data: MenuSchema.MenuIn): + """更新菜单""" + return await MenuService.update_item(pk, data) diff --git a/backend/router/role.py b/backend/router/role.py new file mode 100644 index 0000000..ff643fe --- /dev/null +++ b/backend/router/role.py @@ -0,0 +1,46 @@ +from fastapi import APIRouter, Query + +from schemas import common as BaseSchema +from schemas import role as RoleSchema +from service.role import service as RoleService + +router = APIRouter(prefix="/role", tags=["角色管理"]) + +Response = BaseSchema.Response +ListAll = BaseSchema.ListAll + +role_list_schema = ListAll[list[RoleSchema.RoleRead]] + + +@router.get("", summary="角色列表", response_model=Response[role_list_schema]) +async def role_list( + offset: int = Query(default=1, description="偏移量-页码"), + limit: int = Query(default=10, description="数据量"), +): + return await RoleService.get_items(offset, limit) + + +@router.post("/query", summary="角色查询", response_model=Response[role_list_schema]) +async def role_query(query: RoleSchema.RoleQuery): + return await RoleService.query_items(query) + + +@router.post("", summary="角色新增", response_model=Response[RoleSchema.RoleInfo]) +async def role_create(data: RoleSchema.RoleIn): + return await RoleService.create_item(data) + + +@router.get("/{rid}/menu", summary="查询角色拥有权限", response_model=Response) +async def role_has_menu(rid: int): + return await RoleService.has_tree_menus(rid) + + +@router.delete("/{pk}", summary="角色删除", response_model=Response) +async def role_del(pk: int): + return await RoleService.delete_item(pk) + + +@router.put("/{pk}", summary="角色更新", response_model=Response) +async def role_put(pk: int, data: RoleSchema.RoleIn): + """更新角色""" + return await RoleService.update_item(pk, data) diff --git a/backend/router/user.py b/backend/router/user.py new file mode 100644 index 0000000..896c28d --- /dev/null +++ b/backend/router/user.py @@ -0,0 +1,53 @@ +from fastapi import APIRouter, Depends, Query + +from core.security import check_permissions +from schemas import common as BaseSchema +from schemas import user as UserSchema +from service.user import service as UserService + +router = APIRouter(prefix="/user", tags=["用户管理"]) + +Response = BaseSchema.Response +ListAll = BaseSchema.ListAll + +user_list_schema = ListAll[list[UserSchema.UserRead]] + + +@router.get("", summary="用户列表", response_model=Response[user_list_schema]) +async def user_list( + offset: int = Query(default=1, description="偏移量-页码"), + limit: int = Query(default=10, description="数据量"), +): + return await UserService.get_items(offset, limit) + + +@router.post("/query", summary="用户查询", response_model=Response[user_list_schema]) +async def user_query(query: UserSchema.UserQuery): + return await UserService.query_items(query) + + +@router.post("", summary="用户新增", response_model=Response[UserSchema.UserRead]) +async def user_create(data: UserSchema.UserAdd): + return await UserService.create_item(data) + + +@router.delete("/{pk}", summary="用户删除", response_model=Response) +async def user_delete(pk: int): + return await UserService.delete_item(pk) + + +@router.get("/{pk}", summary="用户信息", response_model=Response[UserSchema.UserInfo]) +async def user_info(pk: int): + return await UserService.get_item(pk) + + +@router.put("/{pk}", summary="用户更新", response_model=Response) +async def user_update(pk: int, data: UserSchema.UserPut): + return await UserService.update_item(pk, data) + + +@router.put("/role/{rid}", summary="用户切换角色", response_model=Response) +async def user_change_role( + rid: int, user: UserSchema.UserRead = Depends(check_permissions) +): + return await UserService.change_current_role(user.id, rid) diff --git a/backend/schemas/__init__.py b/backend/schemas/__init__.py index e20bfde..8b13789 100644 --- a/backend/schemas/__init__.py +++ b/backend/schemas/__init__.py @@ -1,4 +1 @@ -from schemas.common import * -from schemas.menu import * -from schemas.role import * -from schemas.user import * + diff --git a/backend/service/__init__.py b/backend/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/service/auth.py b/backend/service/auth.py new file mode 100644 index 0000000..bbc52c3 --- /dev/null +++ b/backend/service/auth.py @@ -0,0 +1,26 @@ +import asyncio + +from websockets.exceptions import WebSocketException + +from core.dbhelper import has_user +from core.security import generate_token, verify_password +from core.utils import get_system_info + + +async def user_login(data): + """用户登录""" + user_obj = await has_user(data.username) + if user_obj: + if verify_password(data.password, user_obj.password): + return dict(data=dict(id=user_obj.id, token=generate_token(data.username))) + return dict(code=400, msg="账号或密码错误") + + +async def system_info(ws): + await ws.accept() + try: + while True: + await asyncio.sleep(1) + await ws.send_json(get_system_info()) + except WebSocketException: + await ws.close() diff --git a/backend/service/menu.py b/backend/service/menu.py new file mode 100644 index 0000000..b00fd05 --- /dev/null +++ b/backend/service/menu.py @@ -0,0 +1,26 @@ +from core.dbhelper import MenuDao +from core.service import Service +from core.utils import list_to_tree + + +class MenuService(Service): + def __init__(self): + super(MenuService, self).__init__(MenuDao) + + async def get_items(self): + sql = "select * from sys_menu where status != 9 ;" + menus = await self.dao.raw_sql(sql) + try: + return dict(data=list_to_tree(menus)) + except KeyError: + return dict(code=400, msg="菜单根节点丢失") + + async def delete_item(self, pk): + if await MenuDao.select({"pid": pk, "status__not": 9}) is not None: + return dict(code=400, msg="请先删除子节点") + if await MenuDao.delete(pk) == 0: + return dict(code=400, msg="菜单不存在") + return dict() + + +service = MenuService() diff --git a/backend/service/role.py b/backend/service/role.py new file mode 100644 index 0000000..c0ca35d --- /dev/null +++ b/backend/service/role.py @@ -0,0 +1,61 @@ +from core.dbhelper import MenuDao, RoleDao, RoleMenuDao, has_permissions +from core.service import Service +from core.utils import list_to_tree + + +class RoleService(Service): + def __init__(self): + super(RoleService, self).__init__(RoleDao) + + async def create_item(self, role): + """ + 创建角色 + :param role: pydantic model + :return: + """ + if not all( + [await MenuDao.select({"id": mid, "status__not": 9}) for mid in role.menus] + ): + return dict(code=400, msg="菜单不存在") + + obj = await RoleDao.insert(dict(name=role.name, remark=role.remark)) + # 写入菜单 + await RoleMenuDao.inserts([dict(rid=obj.id, mid=mid) for mid in role.menus]) + return dict(data=obj) + + async def update_item(self, pk, data): + """ + 更新角色 + :param pk: + :param data: + :return: + """ + if await RoleDao.select({"id": pk}) is None: + return dict(code=400, msg="角色不存在") + # 如果不为ture -> 有菜单id不存在 + if not all([await MenuDao.select({"id": mid}) for mid in data.menus]): + return dict(code=400, msg="菜单不存在") + + await RoleDao.update(dict(id=pk), dict(name=data.name, remark=data.remark)) + await RoleMenuDao.update(dict(rid=pk), dict(status=9)) + + await RoleMenuDao.inserts([dict(rid=pk, mid=mid) for mid in data.menus]) + + return dict() + + @staticmethod + async def has_tree_menus(pk): + """ + 查询角色拥有菜单 + :param pk: + :return: + """ + menus = await has_permissions(pk, is_menu=True) + + try: + return dict(data=list_to_tree(menus)) + except KeyError: + return dict(code=400, msg="菜单缺少根节点.") + + +service = RoleService() diff --git a/backend/service/user.py b/backend/service/user.py new file mode 100644 index 0000000..17cf53e --- /dev/null +++ b/backend/service/user.py @@ -0,0 +1,90 @@ +from fastapi.encoders import jsonable_encoder + +from core.dbhelper import RoleDao, UserDao, UserRoleDao, has_roles +from core.security import get_password_hash +from core.service import Service + + +class UserService(Service): + def __init__(self): + super(UserService, self).__init__(UserDao) + + async def create_item(self, data): + """创建用户""" + # 检查用户是否存在 + if await self.dao.select({"username": data.username}) is not None: + return dict(code=400, msg="用户名已存在") + rids = data.roles + del data.roles + data.password = get_password_hash(data.password) + # 检查选中的角色是否存在 + for role in rids: + if await RoleDao.select(dict(id=role.rid, status__not=9)) is None: + return dict(code=400, msg=f"角色{role.rid}不存在") + + # 创建用户- 用户表写入数据 + user_obj = await UserDao.insert(data.dict()) + # 关联表写入数据 + await UserRoleDao.inserts( + [dict(rid=role.rid, uid=user_obj.id, status=role.status) for role in rids] + ) + return dict(data=user_obj) + + async def get_item(self, pk): + """获取用户信息""" + user_obj = await self.dao.select({"id": pk}) + if user_obj is None: + return dict(code=400, msg="用户不存在") + roles = await has_roles(user_obj.id) + return dict(data=dict(**jsonable_encoder(user_obj), roles=roles)) + + async def update_item(self, pk, data): + """用户编辑修改""" + if await self.dao.select({"id": pk}) is None: + return dict(code=400, msg="用户不存在") + + rids = data.roles + del data.roles + for role in rids: + if await RoleDao.select({"id": role.rid, "status__not": 9}) is None: + return role.rid + # 更新用户 + if data.password != "加密之后的密码": + data.password = get_password_hash(data.password) + else: + del data.password + await UserDao.update(dict(id=pk), data.dict()) + + # todo 1. 先前有的角色,这次更新成没有 2. 先前没有的角色 这次更新成有, 3. 只更新了状态 + + roles = await has_roles(pk) + + # 2. 将先有的数据标记 删除 + [ + await UserRoleDao.update(dict(rid=role["id"], uid=pk), dict(status=9)) + for role in roles + ] + + # 2. 新增次此更新的数据 + await UserRoleDao.inserts( + [dict(role.dict(), uid=pk, status=role.status) for role in rids] + ) + return dict() + + @staticmethod + async def change_current_role(uid, rid): + """用户切换角色""" + # 1.将用户id 未删除角色状态置为正常 1 ( 除切换角色id ) + await UserRoleDao.update( + dict(uid=uid, rid__not=rid, status__not=9), dict(status=1) + ) + # 2.将用户id 角色id 和当前角色匹配的数据置为选中 + res = await UserRoleDao.update( + dict(uid=uid, rid=rid, status__not=9), dict(status=5) + ) + if res == 0: + return dict(code=400, msg=f"角色不存在{res}") + return dict() + + +service = UserService()