mirror of
https://github.com/fastapi-practices/fastapi_best_architecture.git
synced 2025-08-16 12:52:38 +08:00
Reconstruct RBAC authentication logic (#264)
* Reconstruct RBAC authentication logic * fix typo * Migrate casbin sqla Adapter to redis * Delete casbin model conf file * Add permission dependencies * Add request permission depends on execution condition * Update openapi authorization method * Add request permission identity * Add request permission dependency description * Migrate casbin redis adapter to sqla * Update menu model and add function * Fix menu permission identification * Update user partial interface permissions * Update menu table SQL * Add role menu permission description to README * fix README typo * Simplify permission dependency injection * Fix menu authorization store * Fix interface permission dependency order * Update role menu permission flag * Update the background permission logic of the interface
This commit is contained in:
14
README.md
14
README.md
@ -53,6 +53,7 @@ See a preview of some of the screenshots
|
||||
- [x] Global SQLAlchemy 2.0 syntax
|
||||
- [x] Pydantic v1 and v2 (different branches)
|
||||
- [x] Casbin RBAC access control model
|
||||
- [x] Role menu RBAC access control model
|
||||
- [x] Celery asynchronous tasks
|
||||
- [x] JWT middleware whitelist authentication
|
||||
- [x] Global customizable time zone time
|
||||
@ -218,18 +219,13 @@ Execute unittests via pytest
|
||||
- [Ruff](https://beta.ruff.rs/docs/)
|
||||
- ...
|
||||
|
||||
## 互动
|
||||
## Interactivity
|
||||
|
||||
We only have one current channel.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td style="text-align: center;"><a href="https://t.me/+ZlPhIFkPp7E4NGI1"> Jump </a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> Telegram </td>
|
||||
</tr>
|
||||
</table>
|
||||
| [Jump](https://t.me/+ZlPhIFkPp7E4NGI1) |
|
||||
|----------------------------------------|
|
||||
| Telegram |
|
||||
|
||||
## Sponsor us
|
||||
|
||||
|
@ -47,6 +47,7 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三
|
||||
- [x] 全局 SQLAlchemy 2.0 语法
|
||||
- [x] Pydantic v1 和 v2 (不同分支)
|
||||
- [x] Casbin RBAC 访问控制模型
|
||||
- [x] 角色菜单 RBAC 访问控制模型
|
||||
- [x] Celery 异步任务
|
||||
- [x] JWT 中间件白名单认证
|
||||
- [x] 全局自定义时区时间
|
||||
@ -216,14 +217,9 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三
|
||||
|
||||
有且仅有当前一个频道,请注意辨别真伪
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td style="text-align: center;"><a href="https://t.me/+ZlPhIFkPp7E4NGI1">直链跳转</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Telegram(科学上网)</td>
|
||||
</tr>
|
||||
</table>
|
||||
| [直链跳转](https://t.me/+ZlPhIFkPp7E4NGI1) |
|
||||
|----------------------------------------|
|
||||
| Telegram(科学上网) |
|
||||
|
||||
## 赞助我们
|
||||
|
||||
|
@ -2,10 +2,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
|
||||
from backend.app.common.jwt import DependsJwtAuth
|
||||
from backend.app.common.pagination import PageDepends, paging_data
|
||||
from backend.app.common.pagination import DependsPagination, paging_data
|
||||
from backend.app.common.permission import RequestPermission
|
||||
from backend.app.common.rbac import DependsRBAC
|
||||
from backend.app.common.response.response_schema import response_base
|
||||
from backend.app.database.db_mysql import CurrentSession
|
||||
@ -27,7 +28,14 @@ async def get_api(pk: int):
|
||||
return await response_base.success(data=api)
|
||||
|
||||
|
||||
@router.get('', summary='(模糊条件)分页获取所有接口', dependencies=[DependsJwtAuth, PageDepends])
|
||||
@router.get(
|
||||
'',
|
||||
summary='(模糊条件)分页获取所有接口',
|
||||
dependencies=[
|
||||
DependsJwtAuth,
|
||||
DependsPagination,
|
||||
],
|
||||
)
|
||||
async def get_api_list(
|
||||
db: CurrentSession,
|
||||
name: Annotated[str | None, Query()] = None,
|
||||
@ -39,13 +47,27 @@ async def get_api_list(
|
||||
return await response_base.success(data=page_data)
|
||||
|
||||
|
||||
@router.post('', summary='创建接口', dependencies=[DependsRBAC])
|
||||
@router.post(
|
||||
'',
|
||||
summary='创建接口',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:api:add')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def create_api(obj: CreateApi):
|
||||
await ApiService.create(obj=obj)
|
||||
return await response_base.success()
|
||||
|
||||
|
||||
@router.put('/{pk}', summary='更新接口', dependencies=[DependsRBAC])
|
||||
@router.put(
|
||||
'/{pk}',
|
||||
summary='更新接口',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:api:edit')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def update_api(pk: int, obj: UpdateApi):
|
||||
count = await ApiService.update(pk=pk, obj=obj)
|
||||
if count > 0:
|
||||
@ -53,7 +75,14 @@ async def update_api(pk: int, obj: UpdateApi):
|
||||
return await response_base.fail()
|
||||
|
||||
|
||||
@router.delete('', summary='(批量)删除接口', dependencies=[DependsRBAC])
|
||||
@router.delete(
|
||||
'',
|
||||
summary='(批量)删除接口',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:api:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_api(pk: Annotated[list[int], Query(...)]):
|
||||
count = await ApiService.delete(pk=pk)
|
||||
if count > 0:
|
||||
|
@ -16,7 +16,12 @@ from backend.app.services.auth_service import AuthService
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post('/swagger_login', summary='swagger 表单登录', description='form 格式登录,仅用于 swagger 文档调试接口')
|
||||
@router.post(
|
||||
'/swagger_login',
|
||||
summary='swagger 表单登录',
|
||||
description='form 格式登录,用于 swagger 文档调试以及获取 JWT Auth',
|
||||
deprecated=True,
|
||||
)
|
||||
async def swagger_user_login(form_data: OAuth2PasswordRequestForm = Depends()) -> GetSwaggerToken:
|
||||
token, user = await AuthService().swagger_login(form_data=form_data)
|
||||
return GetSwaggerToken(access_token=token, user=user) # type: ignore
|
||||
@ -25,7 +30,7 @@ async def swagger_user_login(form_data: OAuth2PasswordRequestForm = Depends()) -
|
||||
@router.post(
|
||||
'/login',
|
||||
summary='用户登录',
|
||||
description='json 格式登录, 仅支持在第三方api工具调试接口, 例如: postman',
|
||||
description='json 格式登录, 仅支持在第三方api工具调试, 例如: postman',
|
||||
dependencies=[Depends(RateLimiter(times=5, minutes=1))],
|
||||
)
|
||||
async def user_login(request: Request, obj: AuthLogin, background_tasks: BackgroundTasks):
|
||||
|
@ -2,10 +2,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Path, Query
|
||||
from fastapi import APIRouter, Depends, Path, Query
|
||||
|
||||
from backend.app.common.jwt import DependsJwtAuth
|
||||
from backend.app.common.pagination import PageDepends, paging_data
|
||||
from backend.app.common.pagination import DependsPagination, paging_data
|
||||
from backend.app.common.permission import RequestPermission
|
||||
from backend.app.common.rbac import DependsRBAC
|
||||
from backend.app.common.response.response_schema import response_base
|
||||
from backend.app.database.db_mysql import CurrentSession
|
||||
@ -13,7 +14,6 @@ from backend.app.schemas.casbin_rule import (
|
||||
CreatePolicy,
|
||||
CreateUserRole,
|
||||
DeleteAllPolicies,
|
||||
DeleteAllUserRoles,
|
||||
DeletePolicy,
|
||||
DeleteUserRole,
|
||||
GetAllPolicy,
|
||||
@ -24,11 +24,18 @@ from backend.app.services.casbin_service import CasbinService
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('', summary='(模糊条件)分页获取所有权限规则', dependencies=[DependsJwtAuth, PageDepends])
|
||||
@router.get(
|
||||
'',
|
||||
summary='(模糊条件)分页获取所有权限规则',
|
||||
dependencies=[
|
||||
DependsJwtAuth,
|
||||
DependsPagination,
|
||||
],
|
||||
)
|
||||
async def get_all_casbin(
|
||||
db: CurrentSession,
|
||||
ptype: Annotated[str | None, Query()] = None,
|
||||
sub: Annotated[str | None, Query()] = None,
|
||||
ptype: Annotated[str | None, Query(description='规则类型, p / g')] = None,
|
||||
sub: Annotated[str | None, Query(description='用户 uuid / 角色')] = None,
|
||||
):
|
||||
casbin_select = await CasbinService.get_casbin_list(ptype=ptype, sub=sub)
|
||||
page_data = await paging_data(db, casbin_select, GetAllPolicy)
|
||||
@ -47,7 +54,14 @@ async def get_role_policies(role: Annotated[str, Path(description='角色ID')]):
|
||||
return await response_base.success(data=policies)
|
||||
|
||||
|
||||
@router.post('/policy', summary='添加P权限规则', dependencies=[DependsRBAC])
|
||||
@router.post(
|
||||
'/policy',
|
||||
summary='添加P权限规则',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('casbin:p:add')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def create_policy(p: CreatePolicy):
|
||||
"""
|
||||
p 规则:
|
||||
@ -62,37 +76,79 @@ async def create_policy(p: CreatePolicy):
|
||||
return await response_base.success(data=data)
|
||||
|
||||
|
||||
@router.post('/policies', summary='添加多组P权限规则', dependencies=[DependsRBAC])
|
||||
@router.post(
|
||||
'/policies',
|
||||
summary='添加多组P权限规则',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('casbin:p:group:add')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def create_policies(ps: list[CreatePolicy]):
|
||||
data = await CasbinService.create_policies(ps=ps)
|
||||
return await response_base.success(data=data)
|
||||
|
||||
|
||||
@router.put('/policy', summary='更新P权限规则', dependencies=[DependsRBAC])
|
||||
@router.put(
|
||||
'/policy',
|
||||
summary='更新P权限规则',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('casbin:p:edit')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def update_policy(old: UpdatePolicy, new: UpdatePolicy):
|
||||
data = await CasbinService.update_policy(old=old, new=new)
|
||||
return await response_base.success(data=data)
|
||||
|
||||
|
||||
@router.put('/policies', summary='更新多组P权限规则', dependencies=[DependsRBAC])
|
||||
@router.put(
|
||||
'/policies',
|
||||
summary='更新多组P权限规则',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('casbin:p:group:edit')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def update_policies(old: list[UpdatePolicy], new: list[UpdatePolicy]):
|
||||
data = await CasbinService.update_policies(old=old, new=new)
|
||||
return await response_base.success(data=data)
|
||||
|
||||
|
||||
@router.delete('/policy', summary='删除P权限规则', dependencies=[DependsRBAC])
|
||||
@router.delete(
|
||||
'/policy',
|
||||
summary='删除P权限规则',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('casbin:p:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_policy(p: DeletePolicy):
|
||||
data = await CasbinService.delete_policy(p=p)
|
||||
return await response_base.success(data=data)
|
||||
|
||||
|
||||
@router.delete('/policies', summary='删除多组P权限规则', dependencies=[DependsRBAC])
|
||||
@router.delete(
|
||||
'/policies',
|
||||
summary='删除多组P权限规则',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('casbin:p:group:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_policies(ps: list[DeletePolicy]):
|
||||
data = await CasbinService.delete_policies(ps=ps)
|
||||
return await response_base.success(data=data)
|
||||
|
||||
|
||||
@router.delete('/policies/all', summary='删除所有P权限规则', dependencies=[DependsRBAC])
|
||||
@router.delete(
|
||||
'/policies/all',
|
||||
summary='删除所有P权限规则',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('casbin:p:empty')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_all_policies(sub: DeleteAllPolicies):
|
||||
count = await CasbinService.delete_all_policies(sub=sub)
|
||||
if count > 0:
|
||||
@ -106,7 +162,14 @@ async def get_all_groups():
|
||||
return await response_base.success(data=data)
|
||||
|
||||
|
||||
@router.post('/group', summary='添加G权限规则', dependencies=[DependsRBAC])
|
||||
@router.post(
|
||||
'/group',
|
||||
summary='添加G权限规则',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('casbin:g:add')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def create_group(g: CreateUserRole):
|
||||
"""
|
||||
g 规则 (**依赖 p 规则**):
|
||||
@ -121,26 +184,54 @@ async def create_group(g: CreateUserRole):
|
||||
return await response_base.success(data=data)
|
||||
|
||||
|
||||
@router.post('/groups', summary='添加多组G权限规则', dependencies=[DependsRBAC])
|
||||
@router.post(
|
||||
'/groups',
|
||||
summary='添加多组G权限规则',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('casbin:g:group:add')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def create_groups(gs: list[CreateUserRole]):
|
||||
data = await CasbinService.create_groups(gs=gs)
|
||||
return await response_base.success(data=data)
|
||||
|
||||
|
||||
@router.delete('/group', summary='删除G权限规则', dependencies=[DependsRBAC])
|
||||
@router.delete(
|
||||
'/group',
|
||||
summary='删除G权限规则',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('casbin:g:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_group(g: DeleteUserRole):
|
||||
data = await CasbinService.delete_group(g=g)
|
||||
return await response_base.success(data=data)
|
||||
|
||||
|
||||
@router.delete('/groups', summary='删除多组G权限规则', dependencies=[DependsRBAC])
|
||||
@router.delete(
|
||||
'/groups',
|
||||
summary='删除多组G权限规则',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('casbin:g:group:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_groups(gs: list[DeleteUserRole]):
|
||||
data = await CasbinService.delete_groups(gs=gs)
|
||||
return await response_base.success(data=data)
|
||||
|
||||
|
||||
@router.delete('/groups/all', summary='删除所有G权限规则', dependencies=[DependsRBAC])
|
||||
async def delete_all_groups(uuid: DeleteAllUserRoles):
|
||||
@router.delete(
|
||||
'/groups/all',
|
||||
summary='删除所有G权限规则',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('casbin:g:empty')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_all_groups(uuid: str):
|
||||
count = await CasbinService.delete_all_groups(uuid=uuid)
|
||||
if count > 0:
|
||||
return await response_base.success()
|
||||
|
@ -2,9 +2,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
|
||||
from backend.app.common.jwt import DependsJwtAuth
|
||||
from backend.app.common.permission import RequestPermission
|
||||
from backend.app.common.rbac import DependsRBAC
|
||||
from backend.app.common.response.response_schema import response_base
|
||||
from backend.app.schemas.dept import CreateDept, GetAllDept, UpdateDept
|
||||
@ -32,13 +33,27 @@ async def get_all_depts(
|
||||
return await response_base.success(data=dept)
|
||||
|
||||
|
||||
@router.post('', summary='创建部门', dependencies=[DependsRBAC])
|
||||
@router.post(
|
||||
'',
|
||||
summary='创建部门',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:dept:add')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def create_dept(obj: CreateDept):
|
||||
await DeptService.create(obj=obj)
|
||||
return await response_base.success()
|
||||
|
||||
|
||||
@router.put('/{pk}', summary='更新部门', dependencies=[DependsRBAC])
|
||||
@router.put(
|
||||
'/{pk}',
|
||||
summary='更新部门',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:dept:edit')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def update_dept(pk: int, obj: UpdateDept):
|
||||
count = await DeptService.update(pk=pk, obj=obj)
|
||||
if count > 0:
|
||||
@ -46,7 +61,14 @@ async def update_dept(pk: int, obj: UpdateDept):
|
||||
return await response_base.fail()
|
||||
|
||||
|
||||
@router.delete('{pk}', summary='删除部门', dependencies=[DependsRBAC])
|
||||
@router.delete(
|
||||
'{pk}',
|
||||
summary='删除部门',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:dept:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_dept(pk: int):
|
||||
count = await DeptService.delete(pk=pk)
|
||||
if count > 0:
|
||||
|
@ -2,10 +2,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
|
||||
from backend.app.common.jwt import DependsJwtAuth
|
||||
from backend.app.common.pagination import PageDepends, paging_data
|
||||
from backend.app.common.pagination import DependsPagination, paging_data
|
||||
from backend.app.common.permission import RequestPermission
|
||||
from backend.app.common.rbac import DependsRBAC
|
||||
from backend.app.common.response.response_schema import response_base
|
||||
from backend.app.database.db_mysql import CurrentSession
|
||||
@ -23,7 +24,14 @@ async def get_dict_data(pk: int):
|
||||
return await response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get('', summary='(模糊条件)分页获取所有字典', dependencies=[DependsJwtAuth, PageDepends])
|
||||
@router.get(
|
||||
'',
|
||||
summary='(模糊条件)分页获取所有字典',
|
||||
dependencies=[
|
||||
DependsJwtAuth,
|
||||
DependsPagination,
|
||||
],
|
||||
)
|
||||
async def get_all_dict_datas(
|
||||
db: CurrentSession,
|
||||
label: Annotated[str | None, Query()] = None,
|
||||
@ -35,13 +43,27 @@ async def get_all_dict_datas(
|
||||
return await response_base.success(data=page_data)
|
||||
|
||||
|
||||
@router.post('', summary='创建字典', dependencies=[DependsRBAC])
|
||||
@router.post(
|
||||
'',
|
||||
summary='创建字典',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:dict:data:add')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def create_dict_data(obj: CreateDictData):
|
||||
await DictDataService.create(obj=obj)
|
||||
return await response_base.success()
|
||||
|
||||
|
||||
@router.put('/{pk}', summary='更新字典', dependencies=[DependsRBAC])
|
||||
@router.put(
|
||||
'/{pk}',
|
||||
summary='更新字典',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:dict:data:edit')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def update_dict_data(pk: int, obj: UpdateDictData):
|
||||
count = await DictDataService.update(pk=pk, obj=obj)
|
||||
if count > 0:
|
||||
@ -49,7 +71,14 @@ async def update_dict_data(pk: int, obj: UpdateDictData):
|
||||
return await response_base.fail()
|
||||
|
||||
|
||||
@router.delete('', summary='(批量)删除字典', dependencies=[DependsRBAC])
|
||||
@router.delete(
|
||||
'',
|
||||
summary='(批量)删除字典',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:dict:data:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_dict_data(pk: Annotated[list[int], Query(...)]):
|
||||
count = await DictDataService.delete(pk=pk)
|
||||
if count > 0:
|
||||
|
@ -2,10 +2,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
|
||||
from backend.app.common.jwt import DependsJwtAuth
|
||||
from backend.app.common.pagination import PageDepends, paging_data
|
||||
from backend.app.common.pagination import DependsPagination, paging_data
|
||||
from backend.app.common.permission import RequestPermission
|
||||
from backend.app.common.rbac import DependsRBAC
|
||||
from backend.app.common.response.response_schema import response_base
|
||||
from backend.app.database.db_mysql import CurrentSession
|
||||
@ -15,7 +16,14 @@ from backend.app.services.dict_type_service import DictTypeService
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('', summary='(模糊条件)分页获取所有字典类型', dependencies=[DependsJwtAuth, PageDepends])
|
||||
@router.get(
|
||||
'',
|
||||
summary='(模糊条件)分页获取所有字典类型',
|
||||
dependencies=[
|
||||
DependsJwtAuth,
|
||||
DependsPagination,
|
||||
],
|
||||
)
|
||||
async def get_all_dict_types(
|
||||
db: CurrentSession,
|
||||
name: Annotated[str | None, Query()] = None,
|
||||
@ -27,13 +35,27 @@ async def get_all_dict_types(
|
||||
return await response_base.success(data=page_data)
|
||||
|
||||
|
||||
@router.post('', summary='创建字典类型', dependencies=[DependsRBAC])
|
||||
@router.post(
|
||||
'',
|
||||
summary='创建字典类型',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:dict:type:add')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def create_dict_type(obj: CreateDictType):
|
||||
await DictTypeService.create(obj=obj)
|
||||
return await response_base.success()
|
||||
|
||||
|
||||
@router.put('/{pk}', summary='更新字典类型', dependencies=[DependsRBAC])
|
||||
@router.put(
|
||||
'/{pk}',
|
||||
summary='更新字典类型',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:dict:type:edit')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def update_dict_type(pk: int, obj: UpdateDictType):
|
||||
count = await DictTypeService.update(pk=pk, obj=obj)
|
||||
if count > 0:
|
||||
@ -41,7 +63,14 @@ async def update_dict_type(pk: int, obj: UpdateDictType):
|
||||
return await response_base.fail()
|
||||
|
||||
|
||||
@router.delete('', summary='(批量)删除字典类型', dependencies=[DependsRBAC])
|
||||
@router.delete(
|
||||
'',
|
||||
summary='(批量)删除字典类型',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:dict:type:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_dict_type(pk: Annotated[list[int], Query(...)]):
|
||||
count = await DictTypeService.delete(pk=pk)
|
||||
if count > 0:
|
||||
|
@ -2,10 +2,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
|
||||
from backend.app.common.jwt import DependsJwtAuth
|
||||
from backend.app.common.pagination import PageDepends, paging_data
|
||||
from backend.app.common.pagination import DependsPagination, paging_data
|
||||
from backend.app.common.permission import RequestPermission
|
||||
from backend.app.common.rbac import DependsRBAC
|
||||
from backend.app.common.response.response_schema import response_base
|
||||
from backend.app.database.db_mysql import CurrentSession
|
||||
@ -15,7 +16,14 @@ from backend.app.services.login_log_service import LoginLogService
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('', summary='(模糊条件)分页获取登录日志', dependencies=[DependsJwtAuth, PageDepends])
|
||||
@router.get(
|
||||
'',
|
||||
summary='(模糊条件)分页获取登录日志',
|
||||
dependencies=[
|
||||
DependsJwtAuth,
|
||||
DependsPagination,
|
||||
],
|
||||
)
|
||||
async def get_all_login_logs(
|
||||
db: CurrentSession,
|
||||
username: Annotated[str | None, Query()] = None,
|
||||
@ -27,7 +35,14 @@ async def get_all_login_logs(
|
||||
return await response_base.success(data=page_data)
|
||||
|
||||
|
||||
@router.delete('', summary='(批量)删除登录日志', dependencies=[DependsRBAC])
|
||||
@router.delete(
|
||||
'',
|
||||
summary='(批量)删除登录日志',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('log:login:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_login_log(pk: Annotated[list[int], Query(...)]):
|
||||
count = await LoginLogService.delete(pk=pk)
|
||||
if count > 0:
|
||||
@ -35,7 +50,14 @@ async def delete_login_log(pk: Annotated[list[int], Query(...)]):
|
||||
return await response_base.fail()
|
||||
|
||||
|
||||
@router.delete('/all', summary='清空登录日志', dependencies=[DependsRBAC])
|
||||
@router.delete(
|
||||
'/all',
|
||||
summary='清空登录日志',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('log:login:empty')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_all_login_logs():
|
||||
count = await LoginLogService.delete_all()
|
||||
if count > 0:
|
||||
|
@ -2,10 +2,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
|
||||
from backend.app.common.jwt import DependsJwtAuth
|
||||
from backend.app.common.pagination import PageDepends, paging_data
|
||||
from backend.app.common.pagination import DependsPagination, paging_data
|
||||
from backend.app.common.permission import RequestPermission
|
||||
from backend.app.common.rbac import DependsRBAC
|
||||
from backend.app.common.response.response_schema import response_base
|
||||
from backend.app.database.db_mysql import CurrentSession
|
||||
@ -15,7 +16,14 @@ from backend.app.services.opera_log_service import OperaLogService
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('', summary='(模糊条件)分页获取操作日志', dependencies=[DependsJwtAuth, PageDepends])
|
||||
@router.get(
|
||||
'',
|
||||
summary='(模糊条件)分页获取操作日志',
|
||||
dependencies=[
|
||||
DependsJwtAuth,
|
||||
DependsPagination,
|
||||
],
|
||||
)
|
||||
async def get_all_opera_logs(
|
||||
db: CurrentSession,
|
||||
username: Annotated[str | None, Query()] = None,
|
||||
@ -27,7 +35,14 @@ async def get_all_opera_logs(
|
||||
return await response_base.success(data=page_data)
|
||||
|
||||
|
||||
@router.delete('', summary='(批量)删除操作日志', dependencies=[DependsRBAC])
|
||||
@router.delete(
|
||||
'',
|
||||
summary='(批量)删除操作日志',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('log:opera:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_opera_log(pk: Annotated[list[int], Query(...)]):
|
||||
count = await OperaLogService.delete(pk=pk)
|
||||
if count > 0:
|
||||
@ -35,7 +50,14 @@ async def delete_opera_log(pk: Annotated[list[int], Query(...)]):
|
||||
return await response_base.fail()
|
||||
|
||||
|
||||
@router.delete('/all', summary='清空操作日志', dependencies=[DependsRBAC])
|
||||
@router.delete(
|
||||
'/all',
|
||||
summary='清空操作日志',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('log:opera:empty')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_all_opera_logs():
|
||||
count = await OperaLogService.delete_all()
|
||||
if count > 0:
|
||||
|
@ -2,9 +2,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Query, Request
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
|
||||
from backend.app.common.jwt import DependsJwtAuth
|
||||
from backend.app.common.permission import RequestPermission
|
||||
from backend.app.common.rbac import DependsRBAC
|
||||
from backend.app.common.response.response_schema import response_base
|
||||
from backend.app.schemas.menu import CreateMenu, GetAllMenu, UpdateMenu
|
||||
@ -36,13 +37,27 @@ async def get_all_menus(
|
||||
return await response_base.success(data=menu)
|
||||
|
||||
|
||||
@router.post('', summary='创建菜单', dependencies=[DependsRBAC])
|
||||
@router.post(
|
||||
'',
|
||||
summary='创建菜单',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:menu:add')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def create_menu(obj: CreateMenu):
|
||||
await MenuService.create(obj=obj)
|
||||
return await response_base.success()
|
||||
|
||||
|
||||
@router.put('/{pk}', summary='更新菜单', dependencies=[DependsRBAC])
|
||||
@router.put(
|
||||
'/{pk}',
|
||||
summary='更新菜单',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:menu:edit')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def update_menu(pk: int, obj: UpdateMenu):
|
||||
count = await MenuService.update(pk=pk, obj=obj)
|
||||
if count > 0:
|
||||
@ -50,7 +65,14 @@ async def update_menu(pk: int, obj: UpdateMenu):
|
||||
return await response_base.fail()
|
||||
|
||||
|
||||
@router.delete('/{pk}', summary='删除菜单', dependencies=[DependsRBAC])
|
||||
@router.delete(
|
||||
'/{pk}',
|
||||
summary='删除菜单',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:menu:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_menu(pk: int):
|
||||
count = await MenuService.delete(pk=pk)
|
||||
if count > 0:
|
||||
|
@ -1,15 +1,23 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.routing import APIRoute
|
||||
|
||||
from backend.app.common.permission import RequestPermission
|
||||
from backend.app.common.rbac import DependsRBAC
|
||||
from backend.app.common.response.response_schema import response_base
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('/routers', summary='获取所有路由', dependencies=[DependsRBAC])
|
||||
@router.get(
|
||||
'/routers',
|
||||
summary='获取所有路由',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:route:list')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def get_all_route(request: Request):
|
||||
data = []
|
||||
for route in request.app.routes:
|
||||
|
@ -1,15 +1,23 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from backend.app.common.jwt import DependsJwtAuth
|
||||
from backend.app.common.permission import RequestPermission
|
||||
from backend.app.common.response.response_schema import response_base
|
||||
from backend.app.utils.redis_info import redis_info
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('/redis', summary='redis 监控', dependencies=[DependsJwtAuth])
|
||||
@router.get(
|
||||
'/redis',
|
||||
summary='redis 监控',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:monitor:redis')),
|
||||
DependsJwtAuth,
|
||||
],
|
||||
)
|
||||
async def get_redis_info():
|
||||
data = {'info': await redis_info.get_info(), 'stats': await redis_info.get_stats()}
|
||||
return await response_base.success(data=data)
|
||||
|
@ -1,16 +1,24 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, Depends
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
|
||||
from backend.app.common.jwt import DependsJwtAuth
|
||||
from backend.app.common.permission import RequestPermission
|
||||
from backend.app.common.response.response_schema import response_base
|
||||
from backend.app.utils.server_info import server_info
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('/server', summary='server 监控', dependencies=[DependsJwtAuth])
|
||||
@router.get(
|
||||
'/server',
|
||||
summary='server 监控',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:monitor:server')),
|
||||
DependsJwtAuth,
|
||||
],
|
||||
)
|
||||
async def get_server_info():
|
||||
"""IO密集型任务,使用线程池尽量减少性能损耗"""
|
||||
data = {
|
||||
|
@ -2,10 +2,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
|
||||
from backend.app.common.jwt import DependsJwtAuth
|
||||
from backend.app.common.pagination import PageDepends, paging_data
|
||||
from backend.app.common.pagination import DependsPagination, paging_data
|
||||
from backend.app.common.permission import RequestPermission
|
||||
from backend.app.common.rbac import DependsRBAC
|
||||
from backend.app.common.response.response_schema import response_base
|
||||
from backend.app.database.db_mysql import CurrentSession
|
||||
@ -44,7 +45,14 @@ async def get_role(pk: int):
|
||||
return await response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get('', summary='(模糊条件)分页获取所有角色', dependencies=[DependsJwtAuth, PageDepends])
|
||||
@router.get(
|
||||
'',
|
||||
summary='(模糊条件)分页获取所有角色',
|
||||
dependencies=[
|
||||
DependsJwtAuth,
|
||||
DependsPagination,
|
||||
],
|
||||
)
|
||||
async def get_all_role_list(
|
||||
db: CurrentSession,
|
||||
name: Annotated[str | None, Query()] = None,
|
||||
@ -56,13 +64,27 @@ async def get_all_role_list(
|
||||
return await response_base.success(data=page_data)
|
||||
|
||||
|
||||
@router.post('', summary='创建角色', dependencies=[DependsRBAC])
|
||||
@router.post(
|
||||
'',
|
||||
summary='创建角色',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:role:add')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def create_role(obj: CreateRole):
|
||||
await RoleService.create(obj=obj)
|
||||
return await response_base.success()
|
||||
|
||||
|
||||
@router.put('/{pk}', summary='更新角色', dependencies=[DependsRBAC])
|
||||
@router.put(
|
||||
'/{pk}',
|
||||
summary='更新角色',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:role:edit')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def update_role(pk: int, obj: UpdateRole):
|
||||
count = await RoleService.update(pk=pk, obj=obj)
|
||||
if count > 0:
|
||||
@ -70,15 +92,29 @@ async def update_role(pk: int, obj: UpdateRole):
|
||||
return await response_base.fail()
|
||||
|
||||
|
||||
@router.put('/{pk}/menu', summary='更新角色菜单', dependencies=[DependsRBAC])
|
||||
async def update_role_menu(pk: int, menu_ids: UpdateRoleMenu):
|
||||
count = await RoleService.update_menus(pk=pk, menu_ids=menu_ids)
|
||||
@router.put(
|
||||
'/{pk}/menu',
|
||||
summary='更新角色菜单',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:role:menu:edit')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def update_role_menu(request: Request, pk: int, menu_ids: UpdateRoleMenu):
|
||||
count = await RoleService.update_menus(request=request, pk=pk, menu_ids=menu_ids)
|
||||
if count > 0:
|
||||
return await response_base.success()
|
||||
return await response_base.fail()
|
||||
|
||||
|
||||
@router.delete('', summary='(批量)删除角色', dependencies=[DependsRBAC])
|
||||
@router.delete(
|
||||
'',
|
||||
summary='(批量)删除角色',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:role:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_role(pk: Annotated[list[int], Query(...)]):
|
||||
count = await RoleService.delete(pk=pk)
|
||||
if count > 0:
|
||||
|
@ -2,9 +2,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Path
|
||||
from fastapi import APIRouter, Body, Depends, Path
|
||||
|
||||
from backend.app.common.jwt import DependsJwtAuth
|
||||
from backend.app.common.permission import RequestPermission
|
||||
from backend.app.common.rbac import DependsRBAC
|
||||
from backend.app.common.response.response_code import CustomResponseCode
|
||||
from backend.app.common.response.response_schema import response_base
|
||||
@ -27,7 +28,14 @@ async def get_task_result(pk: str = Path(description='任务ID')):
|
||||
return await response_base.success(data=task.result)
|
||||
|
||||
|
||||
@router.post('/{module}', summary='执行任务', dependencies=[DependsRBAC])
|
||||
@router.post(
|
||||
'/{module}',
|
||||
summary='执行任务',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:task:run')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def run_task(
|
||||
module: Annotated[str, Path(description='任务模块')],
|
||||
args: Annotated[list | None, Body()] = None,
|
||||
|
@ -2,10 +2,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Query, Request
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
|
||||
from backend.app.common.jwt import DependsJwtAuth
|
||||
from backend.app.common.pagination import PageDepends, paging_data
|
||||
from backend.app.common.pagination import DependsPagination, paging_data
|
||||
from backend.app.common.permission import RequestPermission
|
||||
from backend.app.common.rbac import DependsRBAC
|
||||
from backend.app.common.response.response_schema import response_base
|
||||
from backend.app.database.db_mysql import CurrentSession
|
||||
@ -32,8 +33,8 @@ async def user_register(obj: RegisterUser):
|
||||
|
||||
|
||||
@router.post('/add', summary='添加用户', dependencies=[DependsRBAC])
|
||||
async def add_user(obj: AddUser):
|
||||
await UserService.add(obj=obj)
|
||||
async def add_user(request: Request, obj: AddUser):
|
||||
await UserService.add(request=request, obj=obj)
|
||||
current_user = await UserService.get_userinfo(username=obj.username)
|
||||
data = GetAllUserInfo(**await select_as_dict(current_user))
|
||||
return await response_base.success(data=data)
|
||||
@ -68,9 +69,16 @@ async def update_userinfo(request: Request, username: str, obj: UpdateUser):
|
||||
return await response_base.fail()
|
||||
|
||||
|
||||
@router.put('/{username}/role', summary='更新用户角色', dependencies=[DependsRBAC])
|
||||
@router.put(
|
||||
'/{username}/role',
|
||||
summary='更新用户角色',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:user:role:edit')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def update_user_role(request: Request, username: str, obj: UpdateUserRole):
|
||||
await UserService.update_role(request=request, username=username, obj=obj)
|
||||
await UserService.update_roles(request=request, username=username, obj=obj)
|
||||
return await response_base.success()
|
||||
|
||||
|
||||
@ -82,7 +90,14 @@ async def update_avatar(request: Request, username: str, avatar: Avatar):
|
||||
return await response_base.fail()
|
||||
|
||||
|
||||
@router.get('', summary='(模糊条件)分页获取所有用户', dependencies=[DependsJwtAuth, PageDepends])
|
||||
@router.get(
|
||||
'',
|
||||
summary='(模糊条件)分页获取所有用户',
|
||||
dependencies=[
|
||||
DependsJwtAuth,
|
||||
DependsPagination,
|
||||
],
|
||||
)
|
||||
async def get_all_users(
|
||||
db: CurrentSession,
|
||||
dept: Annotated[int | None, Query()] = None,
|
||||
@ -131,10 +146,13 @@ async def multi_set(request: Request, pk: int):
|
||||
path='/{username}',
|
||||
summary='用户注销',
|
||||
description='用户注销 != 用户登出,注销之后用户将从数据库删除',
|
||||
dependencies=[DependsRBAC],
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:user:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_user(request: Request, username: str):
|
||||
count = await UserService.delete(request=request, username=username)
|
||||
async def delete_user(username: str):
|
||||
count = await UserService.delete(username=username)
|
||||
if count > 0:
|
||||
return await response_base.success()
|
||||
return await response_base.fail()
|
||||
|
@ -50,6 +50,7 @@ class MethodType(StrEnum):
|
||||
PUT = 'PUT'
|
||||
DELETE = 'DELETE'
|
||||
PATCH = 'PATCH'
|
||||
OPTIONS = 'OPTIONS'
|
||||
|
||||
|
||||
class LoginLogStatusType(IntEnum):
|
||||
|
@ -4,7 +4,7 @@ from datetime import datetime, timedelta
|
||||
|
||||
from asgiref.sync import sync_to_async
|
||||
from fastapi import Depends, Request
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from fastapi.security import HTTPBearer, OAuth2PasswordBearer
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
from jose import jwt
|
||||
from passlib.context import CryptContext
|
||||
@ -19,8 +19,12 @@ from backend.app.utils.timezone import timezone
|
||||
|
||||
pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto')
|
||||
|
||||
# Deprecated, may be enabled when oauth2 is actually integrated
|
||||
oauth2_schema = OAuth2PasswordBearer(tokenUrl=settings.TOKEN_URL_SWAGGER)
|
||||
|
||||
# JWT authorizes dependency injection
|
||||
DependsJwtAuth = Depends(HTTPBearer())
|
||||
|
||||
|
||||
@sync_to_async
|
||||
def get_hash_password(password: str) -> str:
|
||||
@ -209,8 +213,3 @@ def superuser_verify(request: Request) -> bool:
|
||||
if not request.user.is_staff:
|
||||
raise AuthorizationError(msg='此管理员已被禁止后台管理操作')
|
||||
return is_superuser
|
||||
|
||||
|
||||
# JWT authorizes dependency injection, which can be used if the interface only
|
||||
# needs to provide a token instead of RBAC permission control
|
||||
DependsJwtAuth = Depends(oauth2_schema)
|
||||
|
@ -84,4 +84,4 @@ async def paging_data(db: AsyncSession, select: Select, page_data_schema: Schema
|
||||
|
||||
|
||||
# 分页依赖注入
|
||||
PageDepends = Depends(pagination_ctx(_Page))
|
||||
DependsPagination = Depends(pagination_ctx(_Page))
|
||||
|
26
backend/app/common/permission.py
Normal file
26
backend/app/common/permission.py
Normal file
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from fastapi import Request
|
||||
|
||||
from backend.app.common.exception.errors import ServerError
|
||||
from backend.app.core.conf import settings
|
||||
|
||||
|
||||
class RequestPermission:
|
||||
"""
|
||||
请求权限,仅用于角色菜单RBAC
|
||||
|
||||
Tip:
|
||||
使用此请求权限时,需要将 `Depends(RequestPermission('xxx'))` 在 `DependsRBAC` 之前设置,
|
||||
因为 fastapi 当前版本的接口依赖注入按正序执行,意味着 RBAC 标识会在验证前被设置
|
||||
"""
|
||||
|
||||
def __init__(self, value: str):
|
||||
self.value = value
|
||||
|
||||
async def __call__(self, request: Request):
|
||||
if settings.PERMISSION_MODE == 'role-menu':
|
||||
if not isinstance(self.value, str):
|
||||
raise ServerError
|
||||
# 附加权限标识
|
||||
request.state.permission = self.value
|
@ -5,13 +5,13 @@ import casbin_async_sqlalchemy_adapter
|
||||
|
||||
from fastapi import Depends, Request
|
||||
|
||||
from backend.app.common.enums import StatusType
|
||||
from backend.app.common.enums import StatusType, MethodType
|
||||
from backend.app.common.exception.errors import AuthorizationError, TokenError
|
||||
from backend.app.common.jwt import DependsJwtAuth
|
||||
from backend.app.common.redis import redis_client
|
||||
from backend.app.core.conf import settings
|
||||
from backend.app.core.path_conf import RBAC_MODEL_CONF
|
||||
from backend.app.database.db_mysql import async_engine
|
||||
from backend.app.models.sys_casbin_rule import CasbinRule
|
||||
from backend.app.models import CasbinRule
|
||||
|
||||
|
||||
class RBAC:
|
||||
@ -22,28 +22,46 @@ class RBAC:
|
||||
|
||||
:return:
|
||||
"""
|
||||
# 规则数据作为死数据直接在方法内定义
|
||||
_CASBIN_RBAC_MODEL_CONF_TEXT = """
|
||||
[request_definition]
|
||||
r = sub, obj, act
|
||||
|
||||
[policy_definition]
|
||||
p = sub, obj, act
|
||||
|
||||
[role_definition]
|
||||
g = _, _
|
||||
|
||||
[policy_effect]
|
||||
e = some(where (p.eft == allow))
|
||||
|
||||
[matchers]
|
||||
m = g(r.sub, p.sub) && (keyMatch(r.obj, p.obj) || keyMatch3(r.obj, p.obj)) && (r.act == p.act || p.act == "*")
|
||||
"""
|
||||
adapter = casbin_async_sqlalchemy_adapter.Adapter(async_engine, db_class=CasbinRule)
|
||||
enforcer = casbin.AsyncEnforcer(RBAC_MODEL_CONF, adapter)
|
||||
model = casbin.AsyncEnforcer.new_model(text=_CASBIN_RBAC_MODEL_CONF_TEXT)
|
||||
enforcer = casbin.AsyncEnforcer(model, adapter)
|
||||
await enforcer.load_policy()
|
||||
return enforcer
|
||||
|
||||
async def rbac_verify(self, request: Request, _: dict = DependsJwtAuth) -> None:
|
||||
async def rbac_verify(self, request: Request, _token: str = DependsJwtAuth) -> None:
|
||||
"""
|
||||
RBAC 权限校验
|
||||
|
||||
:param request:
|
||||
:param _:
|
||||
:param _token:
|
||||
:return:
|
||||
"""
|
||||
path = request.url.path
|
||||
# 鉴权白名单
|
||||
if path in settings.TOKEN_EXCLUDE:
|
||||
return
|
||||
# 强制校验 JWT 授权状态
|
||||
# JWT 授权状态强制校验
|
||||
if not request.auth.scopes:
|
||||
raise TokenError
|
||||
# 超级管理员免校验
|
||||
super_user = request.user.is_superuser
|
||||
if super_user:
|
||||
if request.user.is_superuser:
|
||||
return
|
||||
# 检测角色数据权限范围
|
||||
user_roles = request.user.roles
|
||||
@ -51,44 +69,67 @@ class RBAC:
|
||||
raise AuthorizationError(msg='用户未分配角色,授权失败')
|
||||
if not any(len(role.menus) > 0 for role in user_roles):
|
||||
raise AuthorizationError(msg='用户所属角色未分配菜单,授权失败')
|
||||
method = request.method
|
||||
if method != MethodType.GET or method != MethodType.OPTIONS:
|
||||
if not request.user.is_staff:
|
||||
raise AuthorizationError(msg='此用户已被禁止后台管理操作')
|
||||
# 数据权限范围
|
||||
data_scope = any(role.data_scope == 1 for role in user_roles)
|
||||
if data_scope:
|
||||
return
|
||||
method = request.method
|
||||
if settings.MENU_PERMISSION:
|
||||
# 菜单权限校验
|
||||
# TODO: 改用流行方案,自定义接口权限字段标识
|
||||
path_auth = path.split(f'{settings.API_V1_STR}/')[-1].replace('/', ':') + f':{method}'
|
||||
menu_perms = []
|
||||
forbid_menu_perms = []
|
||||
for role in user_roles:
|
||||
if role.menus:
|
||||
for menu in role.menus:
|
||||
if menu.status == StatusType.enable:
|
||||
menu_perms.append(menu.perms)
|
||||
else:
|
||||
forbid_menu_perms.append(menu.perms)
|
||||
if path_auth in set(settings.MENU_EXCLUDE):
|
||||
user_uuid = request.user.uuid
|
||||
path_auth_perm = request.state.permission
|
||||
if settings.PERMISSION_MODE == 'role-menu':
|
||||
# 角色菜单权限校验
|
||||
if path_auth_perm in set(settings.ROLE_MENU_EXCLUDE):
|
||||
return
|
||||
if path_auth in set([perm for perms_str in forbid_menu_perms for perm in perms_str.split(',')]):
|
||||
user_menu_perms = await redis_client.get(f'{settings.PERMISSION_REDIS_PREFIX}:{user_uuid}:enable')
|
||||
user_forbid_menu_perms = await redis_client.get(f'{settings.PERMISSION_REDIS_PREFIX}:{user_uuid}:disable')
|
||||
if not user_menu_perms or not user_forbid_menu_perms:
|
||||
user_menu_perms = []
|
||||
user_forbid_menu_perms = []
|
||||
for role in user_roles:
|
||||
user_menus = role.menus
|
||||
if user_menus:
|
||||
for menu in user_menus:
|
||||
perms = menu.perms
|
||||
if perms:
|
||||
if menu.status == StatusType.enable:
|
||||
user_menu_perms.extend(perms.split(','))
|
||||
else:
|
||||
user_forbid_menu_perms.extend(perms.split(','))
|
||||
await redis_client.set(
|
||||
f'{settings.PERMISSION_REDIS_PREFIX}:{user_uuid}:enable', ','.join(user_menu_perms)
|
||||
)
|
||||
await redis_client.set(
|
||||
f'{settings.PERMISSION_REDIS_PREFIX}:{user_uuid}:disable', ','.join(user_forbid_menu_perms)
|
||||
)
|
||||
if path_auth_perm in user_forbid_menu_perms:
|
||||
raise AuthorizationError(msg='菜单已禁用,授权失败')
|
||||
if path_auth not in set([perm for perms_str in menu_perms for perm in perms_str.split(',')]):
|
||||
if path_auth_perm not in user_menu_perms:
|
||||
raise AuthorizationError
|
||||
else:
|
||||
# casbin 权限校验
|
||||
forbid_menu_path = []
|
||||
user_forbid_menu_perms = await redis_client.get(
|
||||
f'{settings.PERMISSION_REDIS_PREFIX}:{request.user.uuid}:disable'
|
||||
)
|
||||
if not user_forbid_menu_perms:
|
||||
user_forbid_menu_perms = []
|
||||
for role in user_roles:
|
||||
if role.menus:
|
||||
for menu in role.menus:
|
||||
user_menus = role.menus
|
||||
if user_menus:
|
||||
for menu in user_menus:
|
||||
perms = menu.perms
|
||||
if perms:
|
||||
if menu.status == StatusType.disable:
|
||||
forbid_menu_path.append(menu.path)
|
||||
if path.split('/')[-1] in forbid_menu_path:
|
||||
user_forbid_menu_perms.extend(perms.split(','))
|
||||
await redis_client.set(
|
||||
f'{settings.PERMISSION_REDIS_PREFIX}:{user_uuid}:disable', ','.join(user_forbid_menu_perms)
|
||||
)
|
||||
if path_auth_perm in user_forbid_menu_perms:
|
||||
raise AuthorizationError(msg='菜单已禁用,授权失败')
|
||||
if (method, path) in settings.CASBIN_EXCLUDE:
|
||||
return
|
||||
user_uuid = request.user.uuid
|
||||
enforcer = await self.enforcer()
|
||||
if not enforcer.enforce(user_uuid, path, method):
|
||||
raise AuthorizationError
|
||||
|
@ -103,7 +103,7 @@ class Settings(BaseSettings):
|
||||
TOKEN_URL_SWAGGER: str = f'{API_V1_STR}/auth/swagger_login'
|
||||
TOKEN_REDIS_PREFIX: str = 'fba_token'
|
||||
TOKEN_REFRESH_REDIS_PREFIX: str = 'fba_refresh_token'
|
||||
TOKEN_EXCLUDE: list[str] = [ # 白名单
|
||||
TOKEN_EXCLUDE: list[str] = [ # JWT / RBAC 白名单
|
||||
f'{API_V1_STR}/auth/login',
|
||||
]
|
||||
|
||||
@ -120,8 +120,11 @@ class Settings(BaseSettings):
|
||||
MIDDLEWARE_GZIP: bool = True
|
||||
MIDDLEWARE_ACCESS: bool = False
|
||||
|
||||
# Casbin
|
||||
CASBIN_RBAC_MODEL_NAME: str = 'rbac_model.conf'
|
||||
# RBAC Permission
|
||||
PERMISSION_MODE: Literal['casbin', 'role-menu'] = 'casbin'
|
||||
PERMISSION_REDIS_PREFIX: str = 'fba_permission'
|
||||
|
||||
# Casbin Auth
|
||||
CASBIN_EXCLUDE: set[tuple[str, str]] = {
|
||||
('POST', f'{API_V1_STR}/auth/swagger_login'),
|
||||
('POST', f'{API_V1_STR}/auth/login'),
|
||||
@ -130,14 +133,10 @@ class Settings(BaseSettings):
|
||||
('GET', f'{API_V1_STR}/auth/captcha'),
|
||||
}
|
||||
|
||||
# Menu
|
||||
MENU_PERMISSION: bool = False # 危险行为,开启此功能, Casbin 鉴权将失效,并将使用角色菜单鉴权 (默认关闭)
|
||||
MENU_EXCLUDE: list[str] = [
|
||||
'auth:swagger_login:post',
|
||||
'auth:login:post',
|
||||
'auth:logout:post',
|
||||
'auth:register:post',
|
||||
'auth:captcha:get',
|
||||
# Role Menu Auth
|
||||
ROLE_MENU_EXCLUDE: list[str] = [
|
||||
'sys:monitor:redis',
|
||||
'sys:monitor:server',
|
||||
]
|
||||
|
||||
# Opera log
|
||||
|
@ -4,8 +4,6 @@ import os
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from backend.app.core.conf import settings
|
||||
|
||||
# 获取项目根目录
|
||||
# 或使用绝对路径,指到backend目录为止,例如windows:BasePath = D:\git_project\fastapi_mysql\backend
|
||||
BasePath = Path(__file__).resolve().parent.parent.parent
|
||||
@ -16,8 +14,5 @@ Versions = os.path.join(BasePath, 'app', 'alembic', 'versions')
|
||||
# 日志文件路径
|
||||
LogPath = os.path.join(BasePath, 'app', 'log')
|
||||
|
||||
# RBAC model.conf 文件路径
|
||||
RBAC_MODEL_CONF = os.path.join(BasePath, 'app', 'core', settings.CASBIN_RBAC_MODEL_NAME)
|
||||
|
||||
# 离线 IP 数据库路径
|
||||
IP2REGION_XDB = os.path.join(BasePath, 'app', 'static', 'ip2region.xdb')
|
||||
|
@ -1,14 +0,0 @@
|
||||
[request_definition]
|
||||
r = sub, obj, act
|
||||
|
||||
[policy_definition]
|
||||
p = sub, obj, act
|
||||
|
||||
[role_definition]
|
||||
g = _, _
|
||||
|
||||
[policy_effect]
|
||||
e = some(where (p.eft == allow))
|
||||
|
||||
[matchers]
|
||||
m = g(r.sub, p.sub) && (keyMatch(r.obj, p.obj) || keyMatch3(r.obj, p.obj)) && (r.act == p.act || p.act == "*")
|
@ -5,7 +5,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.app.crud.base import CRUDBase
|
||||
from backend.app.models import CasbinRule
|
||||
from backend.app.schemas.casbin_rule import CreatePolicy, DeleteAllPolicies, DeleteAllUserRoles, UpdatePolicy
|
||||
from backend.app.schemas.casbin_rule import CreatePolicy, DeleteAllPolicies, UpdatePolicy
|
||||
|
||||
|
||||
class CRUDCasbin(CRUDBase[CasbinRule, CreatePolicy, UpdatePolicy]):
|
||||
@ -28,8 +28,8 @@ class CRUDCasbin(CRUDBase[CasbinRule, CreatePolicy, UpdatePolicy]):
|
||||
result = await db.execute(delete(self.model).where(or_(*where_list)))
|
||||
return result.rowcount
|
||||
|
||||
async def delete_groups_by_uuid(self, db: AsyncSession, sub: DeleteAllUserRoles) -> int:
|
||||
result = await db.execute(delete(self.model).where(self.model.v0 == sub.uuid))
|
||||
async def delete_groups_by_uuid(self, db: AsyncSession, uuid: str) -> int:
|
||||
result = await db.execute(delete(self.model).where(self.model.v0 == uuid))
|
||||
return result.rowcount
|
||||
|
||||
|
||||
|
@ -15,7 +15,7 @@ class CRUDMenu(CRUDBase[Menu, CreateMenu, UpdateMenu]):
|
||||
return await self.get_(db, pk=menu_id)
|
||||
|
||||
async def get_by_title(self, db, title: str) -> Menu | None:
|
||||
result = await db.execute(select(self.model).where(self.model.title == title))
|
||||
result = await db.execute(select(self.model).where(and_(self.model.title == title, self.model.menu_type != 2)))
|
||||
return result.scalars().first()
|
||||
|
||||
async def get_all(self, db, title: str | None = None, status: int | None = None) -> Sequence[Menu]:
|
||||
@ -43,7 +43,8 @@ class CRUDMenu(CRUDBase[Menu, CreateMenu, UpdateMenu]):
|
||||
await self.create_(db, obj_in)
|
||||
|
||||
async def update(self, db, menu_id: int, obj_in: UpdateMenu) -> int:
|
||||
return await self.update_(db, menu_id, obj_in)
|
||||
count = await self.update_(db, menu_id, obj_in)
|
||||
return count
|
||||
|
||||
async def delete(self, db, menu_id: int) -> int:
|
||||
return await self.delete_(db, menu_id)
|
||||
|
@ -44,7 +44,7 @@ class CRUDUser(CRUDBase[User, RegisterUser, UpdateUser]):
|
||||
async def add(self, db: AsyncSession, obj: AddUser) -> None:
|
||||
salt = text_captcha(5)
|
||||
obj.password = await jwt.get_hash_password(obj.password + salt)
|
||||
dict_obj = obj.dict(exclude={'roles'})
|
||||
dict_obj = obj.model_dump(exclude={'roles'})
|
||||
dict_obj.update({'salt': salt})
|
||||
new_user = self.model(**dict_obj)
|
||||
role_list = []
|
||||
|
@ -8,13 +8,13 @@ from backend.app.models.base import MappedBase, id_key
|
||||
|
||||
|
||||
class CasbinRule(MappedBase):
|
||||
"""重写 casbin 中的 casbinRule model 类, 使用自定义 Base, 避免产生 alembic 迁移问题"""
|
||||
"""重写 casbin 中的 CasbinRule model 类, 使用自定义 Base, 避免产生 alembic 迁移问题"""
|
||||
|
||||
__tablename__ = 'sys_casbin_rule'
|
||||
|
||||
id: Mapped[id_key]
|
||||
ptype: Mapped[str] = mapped_column(String(255), comment='策略类型: p 或者 g')
|
||||
v0: Mapped[str] = mapped_column(String(255), comment='角色 / 用户uuid')
|
||||
ptype: Mapped[str] = mapped_column(String(255), comment='策略类型: p / g')
|
||||
v0: Mapped[str] = mapped_column(String(255), comment='角色ID / 用户uuid')
|
||||
v1: Mapped[str] = mapped_column(LONGTEXT, comment='api路径 / 角色名称')
|
||||
v2: Mapped[str | None] = mapped_column(String(255), comment='请求方法')
|
||||
v3: Mapped[str | None] = mapped_column(String(255))
|
||||
|
@ -16,7 +16,7 @@ class Menu(Base):
|
||||
__tablename__ = 'sys_menu'
|
||||
|
||||
id: Mapped[id_key] = mapped_column(init=False)
|
||||
title: Mapped[str] = mapped_column(String(50), unique=True, comment='菜单标题')
|
||||
title: Mapped[str] = mapped_column(String(50), comment='菜单标题')
|
||||
name: Mapped[str] = mapped_column(String(50), comment='菜单名称')
|
||||
level: Mapped[int] = mapped_column(default=0, comment='菜单层级')
|
||||
sort: Mapped[int] = mapped_column(default=0, comment='排序')
|
||||
|
@ -7,7 +7,7 @@ from backend.app.schemas.base import SchemaBase
|
||||
|
||||
|
||||
class CreatePolicy(SchemaBase):
|
||||
sub: str = Field(..., description='用户uuid / 角色')
|
||||
sub: str = Field(..., description='用户uuid / 角色ID')
|
||||
path: str = Field(..., description='api 路径')
|
||||
method: MethodType = Field(default=MethodType.GET, description='请求方法')
|
||||
|
||||
@ -41,15 +41,11 @@ class DeleteUserRole(CreateUserRole):
|
||||
pass
|
||||
|
||||
|
||||
class DeleteAllUserRoles(SchemaBase):
|
||||
uuid: str
|
||||
|
||||
|
||||
class GetAllPolicy(SchemaBase):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
ptype: str = Field(..., description='规则类型, p 或 g')
|
||||
ptype: str = Field(..., description='规则类型, p / g')
|
||||
v0: str = Field(..., description='用户 uuid / 角色')
|
||||
v1: str = Field(..., description='api 路径 / 角色')
|
||||
v2: str | None = None
|
||||
|
@ -76,6 +76,9 @@ class GetAllUserInfo(GetUserInfoNoRelation):
|
||||
class GetCurrentUserInfo(GetAllUserInfo):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
dept: GetAllDept | str | None = None
|
||||
roles: list[GetAllRole] | list[str] | None = None
|
||||
|
||||
@model_validator(mode='after')
|
||||
def handel(self, values):
|
||||
"""处理部门和角色"""
|
||||
|
@ -10,7 +10,6 @@ from backend.app.schemas.casbin_rule import (
|
||||
CreatePolicy,
|
||||
CreateUserRole,
|
||||
DeleteAllPolicies,
|
||||
DeleteAllUserRoles,
|
||||
DeletePolicy,
|
||||
DeleteUserRole,
|
||||
UpdatePolicy,
|
||||
@ -130,7 +129,7 @@ class CasbinService:
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
async def delete_all_groups(*, uuid: DeleteAllUserRoles) -> int:
|
||||
async def delete_all_groups(*, uuid: str) -> int:
|
||||
async with async_db_session.begin() as db:
|
||||
count = await CasbinDao.delete_groups_by_uuid(db, uuid)
|
||||
return count
|
||||
|
@ -5,6 +5,8 @@ from typing import Any
|
||||
from fastapi import Request
|
||||
|
||||
from backend.app.common.exception import errors
|
||||
from backend.app.common.redis import redis_client
|
||||
from backend.app.core.conf import settings
|
||||
from backend.app.crud.crud_menu import MenuDao
|
||||
from backend.app.crud.crud_role import RoleDao
|
||||
from backend.app.database.db_mysql import async_db_session
|
||||
@ -79,6 +81,7 @@ class MenuService:
|
||||
if not parent_menu:
|
||||
raise errors.NotFoundError(msg='父级菜单不存在')
|
||||
count = await MenuDao.update(db, pk, obj)
|
||||
await redis_client.delete_prefix(settings.PERMISSION_REDIS_PREFIX)
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
|
@ -2,9 +2,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Sequence
|
||||
|
||||
from fastapi import Request
|
||||
from sqlalchemy import Select
|
||||
|
||||
from backend.app.common.exception import errors
|
||||
from backend.app.common.redis import redis_client
|
||||
from backend.app.core.conf import settings
|
||||
from backend.app.crud.crud_menu import MenuDao
|
||||
from backend.app.crud.crud_role import RoleDao
|
||||
from backend.app.database.db_mysql import async_db_session
|
||||
@ -59,7 +62,7 @@ class RoleService:
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def update_menus(*, pk: int, menu_ids: UpdateRoleMenu) -> int:
|
||||
async def update_menus(*, request: Request, pk: int, menu_ids: UpdateRoleMenu) -> int:
|
||||
async with async_db_session.begin() as db:
|
||||
role = await RoleDao.get(db, pk)
|
||||
if not role:
|
||||
@ -69,6 +72,7 @@ class RoleService:
|
||||
if not menu:
|
||||
raise errors.NotFoundError(msg='菜单不存在')
|
||||
count = await RoleDao.update_menus(db, pk, menu_ids)
|
||||
await redis_client.delete_prefix(f'{settings.PERMISSION_REDIS_PREFIX}:{request.user.uuid}')
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
|
@ -34,8 +34,9 @@ class UserService:
|
||||
await UserDao.create(db, obj)
|
||||
|
||||
@staticmethod
|
||||
async def add(*, obj: AddUser) -> None:
|
||||
async def add(*, request: Request, obj: AddUser) -> None:
|
||||
async with async_db_session.begin() as db:
|
||||
await superuser_verify(request)
|
||||
username = await UserDao.get_by_username(db, obj.username)
|
||||
if username:
|
||||
raise errors.ForbiddenError(msg='此用户名已注册')
|
||||
@ -85,7 +86,9 @@ class UserService:
|
||||
@staticmethod
|
||||
async def update(*, request: Request, username: str, obj: UpdateUser) -> int:
|
||||
async with async_db_session.begin() as db:
|
||||
await superuser_verify(request)
|
||||
if not request.user.is_superuser:
|
||||
if request.user.username != username:
|
||||
raise errors.ForbiddenError(msg='你只能修改自己的信息')
|
||||
input_user = await UserDao.get_with_relation(db, username=username)
|
||||
if not input_user:
|
||||
raise errors.NotFoundError(msg='用户不存在')
|
||||
@ -105,7 +108,7 @@ class UserService:
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def update_role(*, request: Request, username: str, obj: UpdateUserRole) -> None:
|
||||
async def update_roles(*, request: Request, username: str, obj: UpdateUserRole) -> None:
|
||||
async with async_db_session.begin() as db:
|
||||
if not request.user.is_superuser:
|
||||
if request.user.username != username:
|
||||
@ -118,6 +121,7 @@ class UserService:
|
||||
if not role:
|
||||
raise errors.NotFoundError(msg='角色不存在')
|
||||
await UserDao.update_role(db, input_user, obj)
|
||||
await redis_client.delete_prefix(f'{settings.PERMISSION_REDIS_PREFIX}:{request.user.uuid}')
|
||||
|
||||
@staticmethod
|
||||
async def update_avatar(*, request: Request, username: str, avatar: Avatar) -> int:
|
||||
@ -196,9 +200,8 @@ class UserService:
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def delete(*, request: Request, username: str) -> int:
|
||||
async def delete(*, username: str) -> int:
|
||||
async with async_db_session.begin() as db:
|
||||
await superuser_verify(request)
|
||||
input_user = await UserDao.get_by_username(db, username)
|
||||
if not input_user:
|
||||
raise errors.NotFoundError(msg='用户不存在')
|
||||
|
@ -114,8 +114,7 @@ CREATE TABLE sys_menu
|
||||
created_time DATETIME NOT NULL COMMENT '创建时间',
|
||||
updated_time DATETIME COMMENT '更新时间',
|
||||
PRIMARY KEY (id),
|
||||
FOREIGN KEY (parent_id) REFERENCES sys_menu (id) ON DELETE SET NULL,
|
||||
UNIQUE (title)
|
||||
FOREIGN KEY (parent_id) REFERENCES sys_menu (id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX ix_sys_menu_id ON sys_menu (id);
|
||||
|
@ -2,7 +2,7 @@ INSERT INTO fba_test.sys_dept (id, name, level, sort, leader, phone, email, stat
|
||||
VALUES (1, 'test', 0, 0, null, null, null, 1, 0, null, '2023-06-26 17:13:45', null);
|
||||
|
||||
INSERT INTO fba_test.sys_menu (id, title, name, level, sort, icon, path, menu_type, component, perms, status, `show`, cache, remark, parent_id, created_time, updated_time)
|
||||
VALUES (1, '测试', 'test', 0, 0, '', null, 0, null, null, 0, 0, 1, null, null, '2023-07-27 19:14:10', '2023-07-27 19:14:52'),
|
||||
VALUES (1, '测试', 'test', 0, 0, '', null, 0, null, null, 0, 0, 1, null, null, '2023-07-27 19:14:10', null),
|
||||
(2, '仪表盘', 'dashboard', 0, 0, 'IconDashboard', 'dashboard', 0, null, null, 1, 1, 1, null, null, '2023-07-27 19:15:45', null),
|
||||
(3, '工作台', 'Workplace', 0, 0, null, 'workplace', 1, '/dashboard/workplace/index.vue', null, 1, 1, 1, null, 2, '2023-07-27 19:17:59', null),
|
||||
(4, 'arco官网', 'arcoWebsite', 0, 888, 'IconLink', 'https://arco.design', 1, null, null, 1, 1, 1, null, null, '2023-07-27 19:19:23', null),
|
||||
@ -12,13 +12,28 @@ VALUES (1, '测试', 'test', 0, 0, '', null, 0, null, null, 0, 0, 1, null, null
|
||||
(8, '常见问题', 'faq', 0, 999, 'IconQuestion', 'https://arco.design/vue/docs/pro/faq', 1, null, null, 1, 1, 1, null, null, '2023-07-27 19:22:24', null),
|
||||
(9, '系统管理', 'admin', 0, 6, 'IconSettings', 'admin', 0, null, null, 1, 1, 1, null, null, '2023-07-27 19:23:00', null),
|
||||
(10, '部门管理', 'SysDept', 0, 0, null, 'sys-dept', 1, '/admin/dept/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:23:42', null),
|
||||
(11, 'API管理', 'SysApi', 0, 1, null, 'sys-api', 1, '/admin/api/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:24:12', null),
|
||||
(12, '用户管理', 'SysUser', 0, 0, null, 'sys-user', 1, '/admin/user/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:25:13', null),
|
||||
(13, '角色管理', 'SysRole', 0, 2, null, 'sys-role', 1, '/admin/role/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:25:45', null),
|
||||
(14, '菜单管理', 'SysMenu', 0, 2, null, 'sys-menu', 1, '/admin/menu/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:45:29', null),
|
||||
(15, '系统监控', 'monitor', 0, 88, 'IconComputer', 'monitor', 0, null, null, 1, 1, 1, null, null, '2023-07-27 19:27:08', null),
|
||||
(16, 'Redis监控', 'Redis', 0, 0, null, 'redis', 1, '/monitor/redis/index.vue', null, 1, 1, 1, null, 15, '2023-07-27 19:28:03', null),
|
||||
(17, '服务器监控', 'Server', 0, 0, null, 'server', 1, '/monitor/server/index.vue', null, 1, 1, 1, null, 15, '2023-07-27 19:28:29', null);
|
||||
(11, '新增', '', 0, 0, null, null, 2, null, 'sys:dept:add', 1, 1, 1, null, 10, '2024-01-07 11:37:00', null),
|
||||
(12, '编辑', '', 0, 0, null, null, 2, null, 'sys:dept:edit', 1, 1, 1, null, 10, '2024-01-07 11:37:29', null),
|
||||
(13, '删除', '', 0, 0, null, null, 2, null, 'sys:dept:del', 1, 1, 1, null, 10, '2024-01-07 11:37:44', null),
|
||||
(14, 'API管理', 'SysApi', 0, 1, null, 'sys-api', 1, '/admin/api/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:24:12', null),
|
||||
(15, '新增', '', 0, 0, null, null, 2, null, 'sys:api:add', 1, 1, 1, null, 14, '2024-01-07 11:57:09', null),
|
||||
(16, '编辑', '', 0, 0, null, null, 2, null, 'sys:api:edit', 1, 1, 1, null, 14, '2024-01-07 11:57:44', null),
|
||||
(17, '删除', '', 0, 0, null, null, 2, null, 'sys:api:del', 1, 1, 1, null, 14, '2024-01-07 11:57:56', null),
|
||||
(18, '用户管理', 'SysUser', 0, 0, null, 'sys-user', 1, '/admin/user/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:25:13', null),
|
||||
(19, '编辑用户角色', '', 0, 0, null, null, 2, null, 'sys:user:role:edit', 1, 1, 1, null, 18, '2024-01-07 12:04:20', null),
|
||||
(20, '注销', '', 0, 0, null, null, 2, null, 'sys:user:del', 1, 1, 1, '用户注销 != 用户登出,注销之后用户将从数据库删除', 18, '2024-01-07 02:28:09', null),
|
||||
(21, '角色管理', 'SysRole', 0, 2, null, 'sys-role', 1, '/admin/role/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:25:45', null),
|
||||
(22, '新增', '', 0, 0, null, null, 2, null, 'sys:role:add', 1, 1, 1, null, 21, '2024-01-07 11:58:37', null),
|
||||
(23, '编辑', '', 0, 0, null, null, 2, null, 'sys:role:edit', 1, 1, 1, null, 21, '2024-01-07 11:58:52', null),
|
||||
(24, '删除', '', 0, 0, null, null, 2, null, 'sys:role:del', 1, 1, 1, null, 21, '2024-01-07 11:59:07', null),
|
||||
(25, '编辑角色菜单', '', 0, 0, null, null, 2, null, 'sys:role:menu:edit', 1, 1, 1, null, 21, '2024-01-07 01:59:39', null),
|
||||
(26, '菜单管理', 'SysMenu', 0, 2, null, 'sys-menu', 1, '/admin/menu/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:45:29', null),
|
||||
(27, '新增', '', 0, 0, null, null, 2, null, 'sys:menu:add', 1, 1, 1, null, 26, '2024-01-07 12:01:24', null),
|
||||
(28, '编辑', '', 0, 0, null, null, 2, null, 'sys:menu:edit', 1, 1, 1, null, 26, '2024-01-07 12:01:34', null),
|
||||
(29, '删除', '', 0, 0, null, null, 2, null, 'sys:menu:del', 1, 1, 1, null, 26, '2024-01-07 12:01:48', null),
|
||||
(30, '系统监控', 'monitor', 0, 88, 'IconComputer', 'monitor', 0, null, null, 1, 1, 1, null, null, '2023-07-27 19:27:08', null),
|
||||
(31, 'Redis监控', 'Redis', 0, 0, null, 'redis', 1, '/monitor/redis/index.vue', 'sys:monitor:redis', 1, 1, 1, null, 30, '2023-07-27 19:28:03', null),
|
||||
(32, '服务器监控', 'Server', 0, 0, null, 'server', 1, '/monitor/server/index.vue', 'sys:monitor:server', 1, 1, 1, null, 30, '2023-07-27 19:28:29', null);
|
||||
|
||||
INSERT INTO fba_test.sys_role (id, name, data_scope, status, remark, created_time, updated_time)
|
||||
VALUES (1, 'test', 2, 1, null, '2023-06-26 17:13:45', null);
|
||||
|
@ -2,7 +2,7 @@ INSERT INTO fba.sys_dept (id, name, level, sort, leader, phone, email, status, d
|
||||
VALUES (1, 'test', 0, 0, null, null, null, 1, 0, null, '2023-06-26 17:13:45', null);
|
||||
|
||||
INSERT INTO fba.sys_menu (id, title, name, level, sort, icon, path, menu_type, component, perms, status, `show`, cache, remark, parent_id, created_time, updated_time)
|
||||
VALUES (1, '测试', 'test', 0, 0, '', null, 0, null, null, 0, 0, 1, null, null, '2023-07-27 19:14:10', '2023-07-27 19:14:52'),
|
||||
VALUES (1, '测试', 'test', 0, 0, '', null, 0, null, null, 0, 0, 1, null, null, '2023-07-27 19:14:10', null),
|
||||
(2, '仪表盘', 'dashboard', 0, 0, 'IconDashboard', 'dashboard', 0, null, null, 1, 1, 1, null, null, '2023-07-27 19:15:45', null),
|
||||
(3, '工作台', 'Workplace', 0, 0, null, 'workplace', 1, '/dashboard/workplace/index.vue', null, 1, 1, 1, null, 2, '2023-07-27 19:17:59', null),
|
||||
(4, 'arco官网', 'arcoWebsite', 0, 888, 'IconLink', 'https://arco.design', 1, null, null, 1, 1, 1, null, null, '2023-07-27 19:19:23', null),
|
||||
@ -12,13 +12,28 @@ VALUES (1, '测试', 'test', 0, 0, '', null, 0, null, null, 0, 0, 1, null, null
|
||||
(8, '常见问题', 'faq', 0, 999, 'IconQuestion', 'https://arco.design/vue/docs/pro/faq', 1, null, null, 1, 1, 1, null, null, '2023-07-27 19:22:24', null),
|
||||
(9, '系统管理', 'admin', 0, 6, 'IconSettings', 'admin', 0, null, null, 1, 1, 1, null, null, '2023-07-27 19:23:00', null),
|
||||
(10, '部门管理', 'SysDept', 0, 0, null, 'sys-dept', 1, '/admin/dept/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:23:42', null),
|
||||
(11, 'API管理', 'SysApi', 0, 1, null, 'sys-api', 1, '/admin/api/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:24:12', null),
|
||||
(12, '用户管理', 'SysUser', 0, 0, null, 'sys-user', 1, '/admin/user/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:25:13', null),
|
||||
(13, '角色管理', 'SysRole', 0, 2, null, 'sys-role', 1, '/admin/role/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:25:45', null),
|
||||
(14, '菜单管理', 'SysMenu', 0, 2, null, 'sys-menu', 1, '/admin/menu/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:45:29', null),
|
||||
(15, '系统监控', 'monitor', 0, 88, 'IconComputer', 'monitor', 0, null, null, 1, 1, 1, null, null, '2023-07-27 19:27:08', null),
|
||||
(16, 'Redis监控', 'Redis', 0, 0, null, 'redis', 1, '/monitor/redis/index.vue', null, 1, 1, 1, null, 15, '2023-07-27 19:28:03', null),
|
||||
(17, '服务器监控', 'Server', 0, 0, null, 'server', 1, '/monitor/server/index.vue', null, 1, 1, 1, null, 15, '2023-07-27 19:28:29', null);
|
||||
(11, '新增', '', 0, 0, null, null, 2, null, 'sys:dept:add', 1, 1, 1, null, 10, '2024-01-07 11:37:00', null),
|
||||
(12, '编辑', '', 0, 0, null, null, 2, null, 'sys:dept:edit', 1, 1, 1, null, 10, '2024-01-07 11:37:29', null),
|
||||
(13, '删除', '', 0, 0, null, null, 2, null, 'sys:dept:del', 1, 1, 1, null, 10, '2024-01-07 11:37:44', null),
|
||||
(14, 'API管理', 'SysApi', 0, 1, null, 'sys-api', 1, '/admin/api/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:24:12', null),
|
||||
(15, '新增', '', 0, 0, null, null, 2, null, 'sys:api:add', 1, 1, 1, null, 14, '2024-01-07 11:57:09', null),
|
||||
(16, '编辑', '', 0, 0, null, null, 2, null, 'sys:api:edit', 1, 1, 1, null, 14, '2024-01-07 11:57:44', null),
|
||||
(17, '删除', '', 0, 0, null, null, 2, null, 'sys:api:del', 1, 1, 1, null, 14, '2024-01-07 11:57:56', null),
|
||||
(18, '用户管理', 'SysUser', 0, 0, null, 'sys-user', 1, '/admin/user/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:25:13', null),
|
||||
(19, '编辑用户角色', '', 0, 0, null, null, 2, null, 'sys:user:role:edit', 1, 1, 1, null, 18, '2024-01-07 12:04:20', null),
|
||||
(20, '注销', '', 0, 0, null, null, 2, null, 'sys:user:del', 1, 1, 1, '用户注销 != 用户登出,注销之后用户将从数据库删除', 18, '2024-01-07 02:28:09', null),
|
||||
(21, '角色管理', 'SysRole', 0, 2, null, 'sys-role', 1, '/admin/role/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:25:45', null),
|
||||
(22, '新增', '', 0, 0, null, null, 2, null, 'sys:role:add', 1, 1, 1, null, 21, '2024-01-07 11:58:37', null),
|
||||
(23, '编辑', '', 0, 0, null, null, 2, null, 'sys:role:edit', 1, 1, 1, null, 21, '2024-01-07 11:58:52', null),
|
||||
(24, '删除', '', 0, 0, null, null, 2, null, 'sys:role:del', 1, 1, 1, null, 21, '2024-01-07 11:59:07', null),
|
||||
(25, '编辑角色菜单', '', 0, 0, null, null, 2, null, 'sys:role:menu:edit', 1, 1, 1, null, 21, '2024-01-07 01:59:39', null),
|
||||
(26, '菜单管理', 'SysMenu', 0, 2, null, 'sys-menu', 1, '/admin/menu/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:45:29', null),
|
||||
(27, '新增', '', 0, 0, null, null, 2, null, 'sys:menu:add', 1, 1, 1, null, 26, '2024-01-07 12:01:24', null),
|
||||
(28, '编辑', '', 0, 0, null, null, 2, null, 'sys:menu:edit', 1, 1, 1, null, 26, '2024-01-07 12:01:34', null),
|
||||
(29, '删除', '', 0, 0, null, null, 2, null, 'sys:menu:del', 1, 1, 1, null, 26, '2024-01-07 12:01:48', null),
|
||||
(30, '系统监控', 'monitor', 0, 88, 'IconComputer', 'monitor', 0, null, null, 1, 1, 1, null, null, '2023-07-27 19:27:08', null),
|
||||
(31, 'Redis监控', 'Redis', 0, 0, null, 'redis', 1, '/monitor/redis/index.vue', 'sys:monitor:redis', 1, 1, 1, null, 30, '2023-07-27 19:28:03', null),
|
||||
(32, '服务器监控', 'Server', 0, 0, null, 'server', 1, '/monitor/server/index.vue', 'sys:monitor:server', 1, 1, 1, null, 30, '2023-07-27 19:28:29', null);
|
||||
|
||||
INSERT INTO fba.sys_role (id, name, data_scope, status, remark, created_time, updated_time)
|
||||
VALUES (1, 'test', 2, 1, null, '2023-06-26 17:13:45', null);
|
||||
|
@ -4,14 +4,14 @@ alembic==1.13.0
|
||||
asgiref==3.7.2
|
||||
asyncmy==0.2.9
|
||||
bcrypt==4.0.1
|
||||
casbin==1.33.0
|
||||
casbin-async-sqlalchemy-adapter==1.3.0
|
||||
casbin==1.34.0
|
||||
casbin-async-sqlalchemy-adapter==1.4.0
|
||||
celery==5.3.6
|
||||
cryptography==41.0.7
|
||||
email-validator==2.0.0
|
||||
fast-captcha==0.2.1
|
||||
fastapi==0.108.0
|
||||
fastapi-limiter==0.1.5
|
||||
fastapi-limiter==0.1.6
|
||||
fastapi-pagination==0.12.13
|
||||
gunicorn==21.2.0
|
||||
httpx==0.25.2
|
||||
@ -30,7 +30,7 @@ pytest-pretty==1.2.0
|
||||
python-jose==3.3.0
|
||||
python-multipart==0.0.6
|
||||
pytz==2023.3
|
||||
redis[hiredis]==4.5.5
|
||||
redis[hiredis]==5.0.1
|
||||
ruff==0.1.8
|
||||
SQLAlchemy==2.0.23
|
||||
supervisor==4.2.5
|
||||
|
Reference in New Issue
Block a user