Compare commits
10 Commits
0fb9718153
...
da5ac866d9
Author | SHA1 | Date | |
---|---|---|---|
|
da5ac866d9 | ||
|
0ef41c6cde | ||
|
e7cc1e5e18 | ||
|
99354c92f9 | ||
|
19f616e786 | ||
|
9da334adaa | ||
|
a033eceb3a | ||
|
9749330624 | ||
|
fdbb7be58d | ||
|
987345f724 |
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 zy7y
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
163
README.md
163
README.md
@ -1,67 +1,134 @@
|
||||
# Mini RBAC
|
||||
# Mini RBAC 系统
|
||||
`Python(FastAPI)`、`VUE3`实现,仅保留核心权限控制的极简后台管理。
|
||||
![index](./imgs/index.png)
|
||||
|
||||
[演示地址-前端: http://49.232.203.244:1856](http://49.232.203.244:1856/login)
|
||||
相关视频:[https://www.bilibili.com/video/BV1mP411H7PL/](https://www.bilibili.com/video/BV1mP411H7PL/)
|
||||
|
||||
[演示地址-接口地址: http://49.232.203.244:1855/docs](http://49.232.203.244:1855/docs)
|
||||
## 项目说明文档
|
||||
|
||||
## 功能
|
||||
### 权限控制
|
||||
- [x] 前端菜单权限控制
|
||||
- [x] 前端路由权限控制
|
||||
- [x] 前端按钮权限控制
|
||||
- [x] 后端接口权限控制
|
||||
### 业务功能
|
||||
- [x] 登录、退出、切换角色
|
||||
- [x] 用户管理 -> 新增、编辑、删除、查询
|
||||
- [x] 角色管理 -> 新增、编辑、删除、查询
|
||||
- [x] 菜单管理 -> 新增、编辑、删除、查询
|
||||
## 1. 数据库模型
|
||||
|
||||
### 1.1 用户表 (sys_user)
|
||||
- username: 账号 (唯一)
|
||||
- nickname: 姓名
|
||||
- password: 密码
|
||||
|
||||
### 1.2 角色表 (sys_role)
|
||||
- name: 角色名称
|
||||
- remark: 角色描述
|
||||
|
||||
### 1.3 菜单表 (sys_menu)
|
||||
- name: 名称
|
||||
- icon: 菜单图标
|
||||
- path: 菜单url
|
||||
- type: 菜单类型 (0目录 1组件 2按钮 3数据)
|
||||
- component: 组件地址
|
||||
- pid: 父id
|
||||
- identifier: 权限标识 (如user:add)
|
||||
- api: 接口地址
|
||||
- method: 接口请求方式
|
||||
|
||||
### 1.4 关系表
|
||||
- 用户角色关系表 (sys_user_role)
|
||||
- uid: 用户id
|
||||
- rid: 角色id
|
||||
|
||||
- 角色菜单关系表 (sys_role_menu)
|
||||
- rid: 角色id
|
||||
- mid: 菜单id
|
||||
|
||||
## 2. API接口
|
||||
|
||||
### 2.1 认证相关
|
||||
- POST /login 用户登录
|
||||
- GET /ws WebSocket系统信息
|
||||
|
||||
### 2.2 用户管理
|
||||
- GET /user 用户列表
|
||||
- POST /user/query 用户查询
|
||||
- POST /user 用户新增
|
||||
- DELETE /user/{pk} 用户删除
|
||||
- GET /user/{pk} 用户信息
|
||||
- PUT /user/{pk} 用户更新
|
||||
- PUT /user/role/{rid} 用户切换角色
|
||||
|
||||
### 2.3 角色管理
|
||||
- GET /role 角色列表
|
||||
- POST /role/query 角色查询
|
||||
- POST /role 角色新增
|
||||
- GET /role/{rid}/menu 查询角色权限
|
||||
- DELETE /role/{pk} 角色删除
|
||||
- PUT /role/{pk} 角色更新
|
||||
|
||||
### 2.4 菜单管理
|
||||
- POST /menu 菜单新增
|
||||
- GET /menu 菜单列表
|
||||
- DELETE /menu/{pk} 菜单删除
|
||||
- PUT /menu/{pk} 菜单更新
|
||||
|
||||
## 3. 技术栈
|
||||
|
||||
### 3.1 后端
|
||||
- Python 3.x
|
||||
- FastAPI (Web框架)
|
||||
- Tortoise ORM (ORM框架)
|
||||
- Uvicorn (ASGI服务器)
|
||||
- Gunicorn (WSGI服务器)
|
||||
- bcrypt (密码加密)
|
||||
- python-jose (JWT认证)
|
||||
|
||||
### 3.2 前端
|
||||
- Vue 3 (前端框架)
|
||||
- Pinia (状态管理)
|
||||
- Vue Router (路由管理)
|
||||
- Ant Design Vue (UI组件库)
|
||||
- ECharts (图表库)
|
||||
- Vite (构建工具)
|
||||
- Axios (HTTP客户端)
|
||||
|
||||
## 4. 网页页面
|
||||
|
||||
### 4.1 主要页面结构
|
||||
- 登录页 (login)
|
||||
- 主页面 (main)
|
||||
- 仪表盘 (dashboard)
|
||||
- 系统管理
|
||||
- 用户管理 (user)
|
||||
- 角色管理 (role)
|
||||
- 菜单管理 (menu)
|
||||
- 测试页面 (test)
|
||||
|
||||
### 4.2 页面功能
|
||||
- 登录认证
|
||||
- 用户管理:增删改查、角色分配
|
||||
- 角色管理:增删改查、权限分配
|
||||
- 菜单管理:增删改查
|
||||
- 仪表盘:系统信息展示
|
||||
- WebSocket实时数据更新
|
||||
|
||||
### 4.3权限控制
|
||||
- 前端菜单权限控制
|
||||
- 前端路由权限控制
|
||||
- 前端按钮权限控制
|
||||
- 后端接口权限控制
|
||||
|
||||
|
||||
# 使用框架&工具
|
||||
## 前端 `node 16.15 LTS`
|
||||
```json
|
||||
{
|
||||
"@kangc/v-md-editor": "^2.3.15",
|
||||
"ant-design-vue": "^3.2.12",
|
||||
"axios": "^0.27.2",
|
||||
"echarts": "^5.3.3",
|
||||
"moment": "^2.29.4",
|
||||
"normalize.css": "^8.0.1",
|
||||
"pinia": "^2.0.21",
|
||||
"pinia-plugin-persistedstate": "^2.2.0",
|
||||
"vue": "^3.2.38",
|
||||
"vue-router": "^4.1.5",
|
||||
"vite": "^3.0.9"
|
||||
}
|
||||
```
|
||||
## 后端 `Python 3.9.7`
|
||||
```
|
||||
bcrypt==4.0.0
|
||||
fastapi==0.82.0
|
||||
passlib==1.7.4
|
||||
pytest==7.1.3
|
||||
python-jose==3.3.0
|
||||
requests==2.28.1
|
||||
uvicorn==0.18.3
|
||||
tortoise-orm==0.19.2
|
||||
websockets==10.3
|
||||
```
|
||||
## 备注
|
||||
原项目地址:[https://github.com/zy7y/mini-rbac](https://github.com/zy7y/mini-rbac)
|
||||
|
||||
本人学习全栈项目时clone了这个项目作为参考,同时优化了readme文件,未违反mit开源协议
|
||||
|
||||
|
||||
<details>
|
||||
<summary>更多图片</summary>
|
||||
## 图片展示
|
||||
|
||||
### 页面
|
||||
|
||||
![login](./imgs/login.png)
|
||||
![change](./imgs//select.png)
|
||||
![user](./imgs/useradd.png)
|
||||
![role](./imgs/menuadd.png)
|
||||
![menu](./imgs/roleadd.png)
|
||||
|
||||
### 接口
|
||||
|
||||
![user-api](./imgs/user-api.png)
|
||||
![role-menu](./imgs/role-menu-api.png)
|
||||
|
||||
</details>
|
||||
|
139
backend/.dockerignore
Normal file
139
backend/.dockerignore
Normal file
@ -0,0 +1,139 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Python template
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
1
backend/.gitignore
vendored
1
backend/.gitignore
vendored
@ -11,7 +11,6 @@ __pycache__/
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
|
15
backend/Dockerfile
Normal file
15
backend/Dockerfile
Normal file
@ -0,0 +1,15 @@
|
||||
# 第一阶段:构建镜像
|
||||
FROM python:3.9-slim-buster AS build
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
|
||||
echo 'Asia/Shanghai' >/etc/timezone && \
|
||||
pip install --no-cache-dir --user -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/
|
||||
COPY . .
|
||||
# 第二阶段:运行镜像
|
||||
FROM python:3.9-slim-buster
|
||||
WORKDIR /app
|
||||
COPY --from=build /root/.local /root/.local
|
||||
COPY --from=build /app .
|
||||
ENV PATH=/root/.local/bin:$PATH
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]
|
@ -144,5 +144,5 @@ async def has_permissions(rid, is_menu=False):
|
||||
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"""
|
||||
AND srm.rid = (?) and m.status != 9 and srm.status !=9 order by m.id asc"""
|
||||
return await RoleMenuDao.raw_sql(sql, [rid])
|
||||
|
@ -2,9 +2,10 @@ from fastapi.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from core.log import logger
|
||||
|
||||
|
||||
class TokenAuthFailure(HTTPException):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@ -20,4 +21,18 @@ async def http_exception(request: Request, exc: HTTPException):
|
||||
)
|
||||
|
||||
|
||||
exception_handlers = {HTTPException: http_exception}
|
||||
async def global_exception(request: Request, exc):
|
||||
if hasattr(request.state, "request_id"):
|
||||
request_id = request.state.request_id
|
||||
else:
|
||||
request_id = None
|
||||
logger.info("request_id 获取失败 请确认对应APIRouter使用了route_class=LogRoute ")
|
||||
logger.exception(f"{request_id} Exception Log: {exc}")
|
||||
return JSONResponse({
|
||||
"msg": str(exc),
|
||||
"code": 500,
|
||||
"data": None
|
||||
})
|
||||
|
||||
|
||||
exception_handlers = {Exception: global_exception, HTTPException: http_exception}
|
||||
|
@ -1 +1,15 @@
|
||||
from loguru import logger
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# 创建一个 FileHandler,用于写入日志文件
|
||||
handler = logging.FileHandler('log_file.log')
|
||||
handler.setLevel(logging.INFO)
|
||||
|
||||
# 设置日志格式
|
||||
formatter = logging.Formatter("[%(asctime)s] %(levelname)s %(message)s")
|
||||
handler.setFormatter(formatter)
|
||||
|
||||
# 添加 FileHandler 到日志处理器中
|
||||
logger.addHandler(handler)
|
||||
|
@ -1,10 +1,18 @@
|
||||
import time
|
||||
import uuid
|
||||
from typing import Callable
|
||||
|
||||
from fastapi.middleware import Middleware
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.routing import APIRoute
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
from core.log import logger
|
||||
|
||||
|
||||
# fix: 中间件没法获取到request 请求体数据 响应体
|
||||
class CustomRequestLogMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request, call_next):
|
||||
logger.info(
|
||||
@ -16,8 +24,28 @@ class CustomRequestLogMiddleware(BaseHTTPMiddleware):
|
||||
return response
|
||||
|
||||
|
||||
class LogRoute(APIRoute):
|
||||
def get_route_handler(self) -> Callable:
|
||||
original_route_handler = super().get_route_handler()
|
||||
|
||||
async def custom_route_handler(request: Request) -> Response:
|
||||
request_id = str(uuid.uuid4())
|
||||
request.state.request_id = request_id
|
||||
logger.info(f"{request_id} Request Log {request.client} {request.method}"
|
||||
f" {request.url} {request.headers}\n {await request.body()}")
|
||||
before = time.time()
|
||||
response: Response = await original_route_handler(request)
|
||||
duration = time.time() - before
|
||||
response.headers["X-Response-Time"] = str(duration)
|
||||
logger.info(f"{request_id} Response Log {duration}s {response.headers}\n"
|
||||
f" {response.body.decode('utf-8')}")
|
||||
return response
|
||||
|
||||
return custom_route_handler
|
||||
|
||||
|
||||
middlewares = [
|
||||
Middleware(CustomRequestLogMiddleware),
|
||||
# Middleware(CustomRequestLogMiddleware),
|
||||
Middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
|
@ -2,7 +2,6 @@ from core.dbhelper import DbHelper
|
||||
|
||||
|
||||
class Service:
|
||||
|
||||
filter_del = {"status__not": 9}
|
||||
|
||||
def __init__(self, dao: DbHelper):
|
||||
|
@ -20,7 +20,6 @@ def list_to_tree(
|
||||
menu_map = {menu["id"]: menu for menu in menus}
|
||||
arr = []
|
||||
for menu in menus:
|
||||
|
||||
# 有父级
|
||||
if mid := menu.get(parent_flag):
|
||||
# 有 子项的情况
|
||||
@ -79,7 +78,7 @@ def load_routers(
|
||||
kwargs = dict(router=router_obj, dependencies=depends)
|
||||
app.include_router(**kwargs)
|
||||
|
||||
logger.info("开始扫描路由。")
|
||||
logger.info("♻️开始扫描路由。")
|
||||
if depends is None:
|
||||
depends = []
|
||||
if is_init:
|
||||
@ -99,9 +98,9 @@ def load_routers(
|
||||
|
||||
for route in app.routes:
|
||||
try:
|
||||
logger.debug(
|
||||
f"{route.path}, {route.methods}, {route.__dict__.get('summary')}"
|
||||
logger.info(
|
||||
f"🦌{route.path}, {route.methods}, {route.__dict__.get('summary')}"
|
||||
)
|
||||
except AttributeError as e:
|
||||
logger.error(e)
|
||||
logger.info("👌路由注册完成✅。")
|
||||
logger.info("®️路由注册完成✅。")
|
||||
|
@ -1,10 +1,10 @@
|
||||
bcrypt==4.0.0
|
||||
fastapi==0.85.0
|
||||
bcrypt==4.0.1
|
||||
fastapi==0.94.0
|
||||
gunicorn==20.1.0
|
||||
passlib==1.7.4
|
||||
pytest==7.1.3
|
||||
pytest==7.2.2
|
||||
python-jose==3.3.0
|
||||
requests==2.28.1
|
||||
tortoise-orm==0.19.2
|
||||
uvicorn==0.18.3
|
||||
websockets==10.3
|
||||
loguru==0.6.0
|
||||
requests==2.28.2
|
||||
tortoise-orm==0.19.3
|
||||
uvicorn==0.21.0
|
||||
websockets==10.4
|
||||
|
@ -1,16 +1,17 @@
|
||||
from fastapi import APIRouter, WebSocket
|
||||
|
||||
from core.middleware import LogRoute
|
||||
from schemas import common as BaseSchema
|
||||
from service import auth as AuthService
|
||||
|
||||
router = APIRouter(tags=["公共"])
|
||||
router = APIRouter(tags=["公共"], route_class=LogRoute)
|
||||
|
||||
|
||||
LoginResult = BaseSchema.Response[BaseSchema.LoginResult]
|
||||
|
||||
|
||||
@router.post("/login", summary="登录", response_model=LoginResult)
|
||||
async def login(data: BaseSchema.LoginForm):
|
||||
@router.post("/login", summary="登录")
|
||||
async def login(data: BaseSchema.LoginForm) -> LoginResult:
|
||||
return await AuthService.user_login(data)
|
||||
|
||||
|
||||
|
@ -1,30 +1,31 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from core.middleware import LogRoute
|
||||
from schemas import common as BaseSchema
|
||||
from schemas import menu as MenuSchema
|
||||
from service.menu import service as MenuService
|
||||
|
||||
router = APIRouter(prefix="/menu", tags=["菜单管理"])
|
||||
router = APIRouter(prefix="/menu", tags=["菜单管理"], route_class=LogRoute)
|
||||
|
||||
Response = BaseSchema.Response
|
||||
|
||||
|
||||
@router.post("", summary="菜单新增", response_model=Response[MenuSchema.MenuRead])
|
||||
async def menu_add(data: MenuSchema.MenuIn):
|
||||
@router.post("", summary="菜单新增")
|
||||
async def menu_add(data: MenuSchema.MenuIn) -> Response[MenuSchema.MenuRead]:
|
||||
return await MenuService.create_item(data)
|
||||
|
||||
|
||||
@router.get("", summary="菜单列表", response_model=Response)
|
||||
async def menu_arr():
|
||||
@router.get("", summary="菜单列表")
|
||||
async def menu_arr() -> Response:
|
||||
return await MenuService.get_items()
|
||||
|
||||
|
||||
@router.delete("/{pk}", summary="菜单删除", response_model=Response)
|
||||
async def menu_del(pk: int):
|
||||
@router.delete("/{pk}", summary="菜单删除")
|
||||
async def menu_del(pk: int) -> Response:
|
||||
return await MenuService.delete_item(pk)
|
||||
|
||||
|
||||
@router.put("/{pk}", summary="菜单更新", response_model=Response)
|
||||
async def menu_put(pk: int, data: MenuSchema.MenuIn):
|
||||
@router.put("/{pk}", summary="菜单更新")
|
||||
async def menu_put(pk: int, data: MenuSchema.MenuIn) -> Response:
|
||||
"""更新菜单"""
|
||||
return await MenuService.update_item(pk, data)
|
||||
|
@ -1,46 +1,46 @@
|
||||
from fastapi import APIRouter, Query
|
||||
from fastapi import APIRouter, Depends
|
||||
from typing import List
|
||||
|
||||
from core.middleware import LogRoute
|
||||
from schemas import common as BaseSchema
|
||||
from schemas import role as RoleSchema
|
||||
from schemas.common import QueryData
|
||||
from service.role import service as RoleService
|
||||
|
||||
router = APIRouter(prefix="/role", tags=["角色管理"])
|
||||
router = APIRouter(prefix="/role", tags=["角色管理"], route_class=LogRoute)
|
||||
|
||||
Response = BaseSchema.Response
|
||||
ListAll = BaseSchema.ListAll
|
||||
|
||||
role_list_schema = ListAll[list[RoleSchema.RoleRead]]
|
||||
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.get("", summary="角色列表")
|
||||
async def role_list(query: QueryData = Depends()) -> Response[role_list_schema]:
|
||||
return await RoleService.get_items(query.offset, query.limit)
|
||||
|
||||
|
||||
@router.post("/query", summary="角色查询", response_model=Response[role_list_schema])
|
||||
async def role_query(query: RoleSchema.RoleQuery):
|
||||
@router.post("/query", summary="角色查询")
|
||||
async def role_query(query: RoleSchema.RoleQuery) -> Response[role_list_schema]:
|
||||
return await RoleService.query_items(query)
|
||||
|
||||
|
||||
@router.post("", summary="角色新增", response_model=Response[RoleSchema.RoleInfo])
|
||||
async def role_create(data: RoleSchema.RoleIn):
|
||||
@router.post("", summary="角色新增")
|
||||
async def role_create(data: RoleSchema.RoleIn) -> Response[RoleSchema.RoleInfo]:
|
||||
return await RoleService.create_item(data)
|
||||
|
||||
|
||||
@router.get("/{rid}/menu", summary="查询角色拥有权限", response_model=Response)
|
||||
async def role_has_menu(rid: int):
|
||||
@router.get("/{rid}/menu", summary="查询角色拥有权限")
|
||||
async def role_has_menu(rid: int) -> Response:
|
||||
return await RoleService.has_tree_menus(rid)
|
||||
|
||||
|
||||
@router.delete("/{pk}", summary="角色删除", response_model=Response)
|
||||
async def role_del(pk: int):
|
||||
@router.delete("/{pk}", summary="角色删除")
|
||||
async def role_del(pk: int) -> Response:
|
||||
return await RoleService.delete_item(pk)
|
||||
|
||||
|
||||
@router.put("/{pk}", summary="角色更新", response_model=Response)
|
||||
async def role_put(pk: int, data: RoleSchema.RoleIn):
|
||||
@router.put("/{pk}", summary="角色更新")
|
||||
async def role_put(pk: int, data: RoleSchema.RoleIn) -> Response:
|
||||
"""更新角色"""
|
||||
return await RoleService.update_item(pk, data)
|
||||
|
@ -1,53 +1,53 @@
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi import APIRouter, Depends
|
||||
from typing import List
|
||||
|
||||
from core.middleware import LogRoute
|
||||
from core.security import check_permissions
|
||||
from schemas import common as BaseSchema
|
||||
from schemas import user as UserSchema
|
||||
from schemas.common import QueryData
|
||||
from service.user import service as UserService
|
||||
|
||||
router = APIRouter(prefix="/user", tags=["用户管理"])
|
||||
router = APIRouter(prefix="/user", tags=["用户管理"], route_class=LogRoute)
|
||||
|
||||
Response = BaseSchema.Response
|
||||
ListAll = BaseSchema.ListAll
|
||||
|
||||
user_list_schema = ListAll[list[UserSchema.UserRead]]
|
||||
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.get("", summary="用户列表")
|
||||
async def user_list(query: QueryData = Depends()) -> Response[user_list_schema]:
|
||||
return await UserService.get_items(query.offset, query.limit)
|
||||
|
||||
|
||||
@router.post("/query", summary="用户查询", response_model=Response[user_list_schema])
|
||||
async def user_query(query: UserSchema.UserQuery):
|
||||
@router.post("/query", summary="用户查询")
|
||||
async def user_query(query: UserSchema.UserQuery) -> Response[user_list_schema]:
|
||||
return await UserService.query_items(query)
|
||||
|
||||
|
||||
@router.post("", summary="用户新增", response_model=Response[UserSchema.UserRead])
|
||||
async def user_create(data: UserSchema.UserAdd):
|
||||
@router.post("", summary="用户新增")
|
||||
async def user_create(data: UserSchema.UserAdd) -> Response[UserSchema.UserRead]:
|
||||
return await UserService.create_item(data)
|
||||
|
||||
|
||||
@router.delete("/{pk}", summary="用户删除", response_model=Response)
|
||||
async def user_delete(pk: int):
|
||||
@router.delete("/{pk}", summary="用户删除")
|
||||
async def user_delete(pk: int) -> Response:
|
||||
return await UserService.delete_item(pk)
|
||||
|
||||
|
||||
@router.get("/{pk}", summary="用户信息", response_model=Response[UserSchema.UserInfo])
|
||||
async def user_info(pk: int):
|
||||
@router.get("/{pk}", summary="用户信息")
|
||||
async def user_info(pk: int) -> Response[UserSchema.UserInfo]:
|
||||
return await UserService.get_item(pk)
|
||||
|
||||
|
||||
@router.put("/{pk}", summary="用户更新", response_model=Response)
|
||||
async def user_update(pk: int, data: UserSchema.UserPut):
|
||||
@router.put("/{pk}", summary="用户更新")
|
||||
async def user_update(pk: int, data: UserSchema.UserPut) -> Response:
|
||||
return await UserService.update_item(pk, data)
|
||||
|
||||
|
||||
@router.put("/role/{rid}", summary="用户切换角色", response_model=Response)
|
||||
@router.put("/role/{rid}", summary="用户切换角色")
|
||||
async def user_change_role(
|
||||
rid: int, user: UserSchema.UserRead = Depends(check_permissions)
|
||||
):
|
||||
) -> Response:
|
||||
return await UserService.change_current_role(user.id, rid)
|
||||
|
@ -26,8 +26,8 @@ class ReadBase(BaseModel):
|
||||
class LoginForm(BaseModel):
|
||||
"""用户登录参数"""
|
||||
|
||||
username: str = Field(..., description="账号", max_length=12, min_length=3)
|
||||
password: str = Field(..., description="密码", min_length=6, max_length=16)
|
||||
username: str = Field("admin", description="账号", max_length=12, min_length=3)
|
||||
password: str = Field("123456", description="密码", min_length=6, max_length=16)
|
||||
|
||||
|
||||
class LoginResult(BaseModel):
|
||||
@ -41,8 +41,8 @@ class LoginResult(BaseModel):
|
||||
class QueryData(BaseModel):
|
||||
"""分页查询基础数据"""
|
||||
|
||||
offset: int = 1
|
||||
limit: int = 10
|
||||
offset: int = Field(default=1, description="页码", ge=1)
|
||||
limit: int = Field(default=10, description="数量", ge=1)
|
||||
|
||||
|
||||
class ListAll(GenericModel, Generic[T]):
|
||||
|
@ -1,4 +1,5 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List
|
||||
|
||||
from schemas.common import QueryData, ReadBase
|
||||
|
||||
@ -7,7 +8,7 @@ class RoleMenuIn(BaseModel):
|
||||
"""角色 -分配菜单id"""
|
||||
|
||||
rid: int = Field(description="角色ID")
|
||||
menus: list[int] = Field(description="菜单ID 列表")
|
||||
menus: List[int] = Field(description="菜单ID 列表")
|
||||
|
||||
|
||||
class RoleMenuRead(RoleMenuIn, ReadBase):
|
||||
@ -20,7 +21,7 @@ class RoleBasic(BaseModel):
|
||||
|
||||
|
||||
class RoleIn(RoleBasic):
|
||||
menus: list[int] = Field(..., description="菜单id列表")
|
||||
menus: List[int] = Field(..., description="菜单id列表")
|
||||
|
||||
|
||||
class RoleRead(RoleBasic, ReadBase):
|
||||
|
@ -1,4 +1,4 @@
|
||||
from typing import Optional
|
||||
from typing import Optional, List
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@ -40,7 +40,7 @@ class UserHasRole(BaseModel):
|
||||
class UserInfo(UserRead):
|
||||
"""用户信息模型"""
|
||||
|
||||
roles: list[UserHasRole] = Field(..., description="用户拥有角色")
|
||||
roles: List[UserHasRole] = Field(..., description="用户拥有角色")
|
||||
|
||||
|
||||
class RoleActive(BaseModel):
|
||||
@ -51,7 +51,7 @@ class RoleActive(BaseModel):
|
||||
class UserAdd(UserIn):
|
||||
"""新增用户模型"""
|
||||
|
||||
roles: list[RoleActive] = Field(..., description="选择角色列表")
|
||||
roles: List[RoleActive] = Field(..., description="选择角色列表")
|
||||
|
||||
|
||||
class UserQuery(QueryData):
|
||||
@ -66,4 +66,4 @@ class UserPut(BaseModel):
|
||||
|
||||
nickname: str = Field(..., description="用户昵称")
|
||||
password: str = Field(..., description="密码")
|
||||
roles: list[RoleActive] = Field(..., description="选择角色列表")
|
||||
roles: List[RoleActive] = Field(..., description="选择角色列表")
|
||||
|
23
docker-compose.yml
Normal file
23
docker-compose.yml
Normal file
@ -0,0 +1,23 @@
|
||||
version: "3"
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
container_name: mini-rbac
|
||||
command: gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app --bind 0.0.0.0:80
|
||||
ports:
|
||||
- "1855:80"
|
||||
|
||||
frontend:
|
||||
build:
|
||||
# 上下文管理, 执行frontend 下的 Dockerfile
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
# 容器名称
|
||||
container_name: mini-web
|
||||
# 宿主机2152 端口 绑定 容器中的80 端口
|
||||
ports:
|
||||
- "1856:80"
|
||||
depends_on: # 依赖于backend容器,被依赖容器启动后此web容器才可启动
|
||||
- backend
|
129
frontend/.dockerignore
Normal file
129
frontend/.dockerignore
Normal file
@ -0,0 +1,129 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Vue template
|
||||
# gitignore template for Vue.js projects
|
||||
#
|
||||
# Recommended template: Node.gitignore
|
||||
|
||||
# TODO: where does this rule come from?
|
||||
docs/_book
|
||||
|
||||
# TODO: where does this rule come from?
|
||||
test/
|
||||
|
||||
### Node template
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverag
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
@ -1,2 +1,2 @@
|
||||
VITE_BASE_URL = http://127.0.0.1:8000/api
|
||||
VITE_WEBSOCKET = ws://localhost:8000/ws
|
||||
VITE_BASE_URL = http://localhost:1855
|
||||
VITE_WS = ws://localhost:1855/ws
|
||||
|
11
frontend/Dockerfile
Normal file
11
frontend/Dockerfile
Normal file
@ -0,0 +1,11 @@
|
||||
FROM node:16.15-alpine
|
||||
COPY ./ /app
|
||||
WORKDIR /app
|
||||
RUN npm config set registry https://registry.npmjs.org
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.15.2-alpine
|
||||
RUN mkdir /app
|
||||
COPY --from=0 /app/dist /app
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
30
frontend/nginx.conf
Normal file
30
frontend/nginx.conf
Normal file
@ -0,0 +1,30 @@
|
||||
user nginx;
|
||||
worker_processes 1;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
access_log /var/log/nginx/access.log main;
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
location / {
|
||||
root /app;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
}
|
12
frontend/package-lock.json
generated
12
frontend/package-lock.json
generated
@ -2160,9 +2160,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/decode-uri-component": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
||||
"integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==",
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
|
||||
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
@ -7591,9 +7591,9 @@
|
||||
}
|
||||
},
|
||||
"decode-uri-component": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
||||
"integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og=="
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
|
||||
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="
|
||||
},
|
||||
"deep-is": {
|
||||
"version": "0.1.4",
|
||||
|
Loading…
Reference in New Issue
Block a user