Compare commits

..

10 Commits

Author SHA1 Message Date
carry
da5ac866d9 优化readme 2025-01-17 01:04:12 +08:00
zy7y
0ef41c6cde feat: 增加全局异常处理、使用route_class记录请求日志 2023-06-14 19:52:28 +08:00
zy7y
e7cc1e5e18 fix: use python3.8 type error add local runing video url 2023-05-31 20:34:10 +08:00
zy7y
99354c92f9 feat: FastAPI挂载前端打包后文件 2023-03-17 21:01:59 +08:00
zy7y
19f616e786 docs: backend docker image slim down 2023-03-12 00:02:09 +08:00
zy7y
9da334adaa chore: update backend lib version and update Dockerfile 2023-03-11 23:59:51 +08:00
dependabot[bot]
a033eceb3a
build(deps): bump decode-uri-component from 0.2.0 to 0.2.2 in /frontend (#8)
Bumps [decode-uri-component](https://github.com/SamVerschueren/decode-uri-component) from 0.2.0 to 0.2.2.
- [Release notes](https://github.com/SamVerschueren/decode-uri-component/releases)
- [Commits](https://github.com/SamVerschueren/decode-uri-component/compare/v0.2.0...v0.2.2)

---
updated-dependencies:
- dependency-name: decode-uri-component
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-24 20:46:09 +08:00
zy7y
9749330624
docs:添加开源协议 #5 (#6)
fix:#5
2022-11-26 17:49:34 +08:00
zy7y
fdbb7be58d
fix:#3增加过滤条件sys_role_menu状态不为9 (#4) 2022-10-09 19:10:32 +08:00
zy7y
987345f724 feat: Deploy the project using Docker 2022-10-08 12:09:20 +08:00
26 changed files with 633 additions and 138 deletions

21
LICENSE Normal file
View 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
View File

@ -1,67 +1,134 @@
# Mini RBAC # Mini RBAC 系统
`Python(FastAPI)`、`VUE3`实现,仅保留核心权限控制的极简后台管理。 `Python(FastAPI)`、`VUE3`实现,仅保留核心权限控制的极简后台管理。
![index](./imgs/index.png) ![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) ## 项目说明文档
## 功能 ## 1. 数据库模型
### 权限控制
- [x] 前端菜单权限控制 ### 1.1 用户表 (sys_user)
- [x] 前端路由权限控制 - username: 账号 (唯一)
- [x] 前端按钮权限控制 - nickname: 姓名
- [x] 后端接口权限控制 - password: 密码
### 业务功能
- [x] 登录、退出、切换角色 ### 1.2 角色表 (sys_role)
- [x] 用户管理 -> 新增、编辑、删除、查询 - name: 角色名称
- [x] 角色管理 -> 新增、编辑、删除、查询 - remark: 角色描述
- [x] 菜单管理 -> 新增、编辑、删除、查询
### 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` 原项目地址:[https://github.com/zy7y/mini-rbac](https://github.com/zy7y/mini-rbac)
```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
```
本人学习全栈项目时clone了这个项目作为参考同时优化了readme文件未违反mit开源协议
## 图片展示
<details>
<summary>更多图片</summary>
### 页面 ### 页面
![login](./imgs/login.png) ![login](./imgs/login.png)
![change](./imgs//select.png) ![change](./imgs//select.png)
![user](./imgs/useradd.png) ![user](./imgs/useradd.png)
![role](./imgs/menuadd.png) ![role](./imgs/menuadd.png)
![menu](./imgs/roleadd.png) ![menu](./imgs/roleadd.png)
### 接口 ### 接口
![user-api](./imgs/user-api.png) ![user-api](./imgs/user-api.png)
![role-menu](./imgs/role-menu-api.png) ![role-menu](./imgs/role-menu-api.png)
</details>

139
backend/.dockerignore Normal file
View 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
View File

@ -11,7 +11,6 @@ __pycache__/
.Python .Python
build/ build/
develop-eggs/ develop-eggs/
dist/
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/

15
backend/Dockerfile Normal file
View 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"]

View File

@ -144,5 +144,5 @@ async def has_permissions(rid, is_menu=False):
sql = f""" sql = f"""
select {filters} select {filters}
FROM sys_menu as m, sys_role_menu as srm WHERE m.id = srm.mid 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]) return await RoleMenuDao.raw_sql(sql, [rid])

View File

@ -2,9 +2,10 @@ from fastapi.exceptions import HTTPException
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import JSONResponse from starlette.responses import JSONResponse
from core.log import logger
class TokenAuthFailure(HTTPException): class TokenAuthFailure(HTTPException):
pass 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}

View File

@ -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)

View File

@ -1,10 +1,18 @@
import time
import uuid
from typing import Callable
from fastapi.middleware import Middleware from fastapi.middleware import Middleware
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.routing import APIRoute
from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
from core.log import logger from core.log import logger
# fix 中间件没法获取到request 请求体数据 响应体
class CustomRequestLogMiddleware(BaseHTTPMiddleware): class CustomRequestLogMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next): async def dispatch(self, request, call_next):
logger.info( logger.info(
@ -16,8 +24,28 @@ class CustomRequestLogMiddleware(BaseHTTPMiddleware):
return response 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 = [ middlewares = [
Middleware(CustomRequestLogMiddleware), # Middleware(CustomRequestLogMiddleware),
Middleware( Middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], allow_origins=["*"],

View File

@ -2,7 +2,6 @@ from core.dbhelper import DbHelper
class Service: class Service:
filter_del = {"status__not": 9} filter_del = {"status__not": 9}
def __init__(self, dao: DbHelper): def __init__(self, dao: DbHelper):

View File

@ -20,7 +20,6 @@ def list_to_tree(
menu_map = {menu["id"]: menu for menu in menus} menu_map = {menu["id"]: menu for menu in menus}
arr = [] arr = []
for menu in menus: for menu in menus:
# 有父级 # 有父级
if mid := menu.get(parent_flag): if mid := menu.get(parent_flag):
# 有 子项的情况 # 有 子项的情况
@ -79,7 +78,7 @@ def load_routers(
kwargs = dict(router=router_obj, dependencies=depends) kwargs = dict(router=router_obj, dependencies=depends)
app.include_router(**kwargs) app.include_router(**kwargs)
logger.info("开始扫描路由。") logger.info("♻️开始扫描路由。")
if depends is None: if depends is None:
depends = [] depends = []
if is_init: if is_init:
@ -99,9 +98,9 @@ def load_routers(
for route in app.routes: for route in app.routes:
try: try:
logger.debug( logger.info(
f"{route.path}, {route.methods}, {route.__dict__.get('summary')}" f"🦌{route.path}, {route.methods}, {route.__dict__.get('summary')}"
) )
except AttributeError as e: except AttributeError as e:
logger.error(e) logger.error(e)
logger.info("👌路由注册完成✅。") logger.info("®️路由注册完成✅。")

View File

@ -1,10 +1,10 @@
bcrypt==4.0.0 bcrypt==4.0.1
fastapi==0.85.0 fastapi==0.94.0
gunicorn==20.1.0
passlib==1.7.4 passlib==1.7.4
pytest==7.1.3 pytest==7.2.2
python-jose==3.3.0 python-jose==3.3.0
requests==2.28.1 requests==2.28.2
tortoise-orm==0.19.2 tortoise-orm==0.19.3
uvicorn==0.18.3 uvicorn==0.21.0
websockets==10.3 websockets==10.4
loguru==0.6.0

View File

@ -1,16 +1,17 @@
from fastapi import APIRouter, WebSocket from fastapi import APIRouter, WebSocket
from core.middleware import LogRoute
from schemas import common as BaseSchema from schemas import common as BaseSchema
from service import auth as AuthService from service import auth as AuthService
router = APIRouter(tags=["公共"]) router = APIRouter(tags=["公共"], route_class=LogRoute)
LoginResult = BaseSchema.Response[BaseSchema.LoginResult] LoginResult = BaseSchema.Response[BaseSchema.LoginResult]
@router.post("/login", summary="登录", response_model=LoginResult) @router.post("/login", summary="登录")
async def login(data: BaseSchema.LoginForm): async def login(data: BaseSchema.LoginForm) -> LoginResult:
return await AuthService.user_login(data) return await AuthService.user_login(data)

View File

@ -1,30 +1,31 @@
from fastapi import APIRouter from fastapi import APIRouter
from core.middleware import LogRoute
from schemas import common as BaseSchema from schemas import common as BaseSchema
from schemas import menu as MenuSchema from schemas import menu as MenuSchema
from service.menu import service as MenuService from service.menu import service as MenuService
router = APIRouter(prefix="/menu", tags=["菜单管理"]) router = APIRouter(prefix="/menu", tags=["菜单管理"], route_class=LogRoute)
Response = BaseSchema.Response Response = BaseSchema.Response
@router.post("", summary="菜单新增", response_model=Response[MenuSchema.MenuRead]) @router.post("", summary="菜单新增")
async def menu_add(data: MenuSchema.MenuIn): async def menu_add(data: MenuSchema.MenuIn) -> Response[MenuSchema.MenuRead]:
return await MenuService.create_item(data) return await MenuService.create_item(data)
@router.get("", summary="菜单列表", response_model=Response) @router.get("", summary="菜单列表")
async def menu_arr(): async def menu_arr() -> Response:
return await MenuService.get_items() return await MenuService.get_items()
@router.delete("/{pk}", summary="菜单删除", response_model=Response) @router.delete("/{pk}", summary="菜单删除")
async def menu_del(pk: int): async def menu_del(pk: int) -> Response:
return await MenuService.delete_item(pk) return await MenuService.delete_item(pk)
@router.put("/{pk}", summary="菜单更新", response_model=Response) @router.put("/{pk}", summary="菜单更新")
async def menu_put(pk: int, data: MenuSchema.MenuIn): async def menu_put(pk: int, data: MenuSchema.MenuIn) -> Response:
"""更新菜单""" """更新菜单"""
return await MenuService.update_item(pk, data) return await MenuService.update_item(pk, data)

View File

@ -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 common as BaseSchema
from schemas import role as RoleSchema from schemas import role as RoleSchema
from schemas.common import QueryData
from service.role import service as RoleService from service.role import service as RoleService
router = APIRouter(prefix="/role", tags=["角色管理"]) router = APIRouter(prefix="/role", tags=["角色管理"], route_class=LogRoute)
Response = BaseSchema.Response Response = BaseSchema.Response
ListAll = BaseSchema.ListAll 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]) @router.get("", summary="角色列表")
async def role_list( async def role_list(query: QueryData = Depends()) -> Response[role_list_schema]:
offset: int = Query(default=1, description="偏移量-页码"), return await RoleService.get_items(query.offset, query.limit)
limit: int = Query(default=10, description="数据量"),
):
return await RoleService.get_items(offset, limit)
@router.post("/query", summary="角色查询", response_model=Response[role_list_schema]) @router.post("/query", summary="角色查询")
async def role_query(query: RoleSchema.RoleQuery): async def role_query(query: RoleSchema.RoleQuery) -> Response[role_list_schema]:
return await RoleService.query_items(query) return await RoleService.query_items(query)
@router.post("", summary="角色新增", response_model=Response[RoleSchema.RoleInfo]) @router.post("", summary="角色新增")
async def role_create(data: RoleSchema.RoleIn): async def role_create(data: RoleSchema.RoleIn) -> Response[RoleSchema.RoleInfo]:
return await RoleService.create_item(data) return await RoleService.create_item(data)
@router.get("/{rid}/menu", summary="查询角色拥有权限", response_model=Response) @router.get("/{rid}/menu", summary="查询角色拥有权限")
async def role_has_menu(rid: int): async def role_has_menu(rid: int) -> Response:
return await RoleService.has_tree_menus(rid) return await RoleService.has_tree_menus(rid)
@router.delete("/{pk}", summary="角色删除", response_model=Response) @router.delete("/{pk}", summary="角色删除")
async def role_del(pk: int): async def role_del(pk: int) -> Response:
return await RoleService.delete_item(pk) return await RoleService.delete_item(pk)
@router.put("/{pk}", summary="角色更新", response_model=Response) @router.put("/{pk}", summary="角色更新")
async def role_put(pk: int, data: RoleSchema.RoleIn): async def role_put(pk: int, data: RoleSchema.RoleIn) -> Response:
"""更新角色""" """更新角色"""
return await RoleService.update_item(pk, data) return await RoleService.update_item(pk, data)

View File

@ -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 core.security import check_permissions
from schemas import common as BaseSchema from schemas import common as BaseSchema
from schemas import user as UserSchema from schemas import user as UserSchema
from schemas.common import QueryData
from service.user import service as UserService from service.user import service as UserService
router = APIRouter(prefix="/user", tags=["用户管理"]) router = APIRouter(prefix="/user", tags=["用户管理"], route_class=LogRoute)
Response = BaseSchema.Response Response = BaseSchema.Response
ListAll = BaseSchema.ListAll 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]) @router.get("", summary="用户列表")
async def user_list( async def user_list(query: QueryData = Depends()) -> Response[user_list_schema]:
offset: int = Query(default=1, description="偏移量-页码"), return await UserService.get_items(query.offset, query.limit)
limit: int = Query(default=10, description="数据量"),
):
return await UserService.get_items(offset, limit)
@router.post("/query", summary="用户查询", response_model=Response[user_list_schema]) @router.post("/query", summary="用户查询")
async def user_query(query: UserSchema.UserQuery): async def user_query(query: UserSchema.UserQuery) -> Response[user_list_schema]:
return await UserService.query_items(query) return await UserService.query_items(query)
@router.post("", summary="用户新增", response_model=Response[UserSchema.UserRead]) @router.post("", summary="用户新增")
async def user_create(data: UserSchema.UserAdd): async def user_create(data: UserSchema.UserAdd) -> Response[UserSchema.UserRead]:
return await UserService.create_item(data) return await UserService.create_item(data)
@router.delete("/{pk}", summary="用户删除", response_model=Response) @router.delete("/{pk}", summary="用户删除")
async def user_delete(pk: int): async def user_delete(pk: int) -> Response:
return await UserService.delete_item(pk) return await UserService.delete_item(pk)
@router.get("/{pk}", summary="用户信息", response_model=Response[UserSchema.UserInfo]) @router.get("/{pk}", summary="用户信息")
async def user_info(pk: int): async def user_info(pk: int) -> Response[UserSchema.UserInfo]:
return await UserService.get_item(pk) return await UserService.get_item(pk)
@router.put("/{pk}", summary="用户更新", response_model=Response) @router.put("/{pk}", summary="用户更新")
async def user_update(pk: int, data: UserSchema.UserPut): async def user_update(pk: int, data: UserSchema.UserPut) -> Response:
return await UserService.update_item(pk, data) 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( async def user_change_role(
rid: int, user: UserSchema.UserRead = Depends(check_permissions) rid: int, user: UserSchema.UserRead = Depends(check_permissions)
): ) -> Response:
return await UserService.change_current_role(user.id, rid) return await UserService.change_current_role(user.id, rid)

View File

@ -26,8 +26,8 @@ class ReadBase(BaseModel):
class LoginForm(BaseModel): class LoginForm(BaseModel):
"""用户登录参数""" """用户登录参数"""
username: str = Field(..., description="账号", max_length=12, min_length=3) username: str = Field("admin", description="账号", max_length=12, min_length=3)
password: str = Field(..., description="密码", min_length=6, max_length=16) password: str = Field("123456", description="密码", min_length=6, max_length=16)
class LoginResult(BaseModel): class LoginResult(BaseModel):
@ -41,8 +41,8 @@ class LoginResult(BaseModel):
class QueryData(BaseModel): class QueryData(BaseModel):
"""分页查询基础数据""" """分页查询基础数据"""
offset: int = 1 offset: int = Field(default=1, description="页码", ge=1)
limit: int = 10 limit: int = Field(default=10, description="数量", ge=1)
class ListAll(GenericModel, Generic[T]): class ListAll(GenericModel, Generic[T]):

View File

@ -1,4 +1,5 @@
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from typing import List
from schemas.common import QueryData, ReadBase from schemas.common import QueryData, ReadBase
@ -7,7 +8,7 @@ class RoleMenuIn(BaseModel):
"""角色 -分配菜单id""" """角色 -分配菜单id"""
rid: int = Field(description="角色ID") rid: int = Field(description="角色ID")
menus: list[int] = Field(description="菜单ID 列表") menus: List[int] = Field(description="菜单ID 列表")
class RoleMenuRead(RoleMenuIn, ReadBase): class RoleMenuRead(RoleMenuIn, ReadBase):
@ -20,7 +21,7 @@ class RoleBasic(BaseModel):
class RoleIn(RoleBasic): class RoleIn(RoleBasic):
menus: list[int] = Field(..., description="菜单id列表") menus: List[int] = Field(..., description="菜单id列表")
class RoleRead(RoleBasic, ReadBase): class RoleRead(RoleBasic, ReadBase):

View File

@ -1,4 +1,4 @@
from typing import Optional from typing import Optional, List
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@ -40,7 +40,7 @@ class UserHasRole(BaseModel):
class UserInfo(UserRead): class UserInfo(UserRead):
"""用户信息模型""" """用户信息模型"""
roles: list[UserHasRole] = Field(..., description="用户拥有角色") roles: List[UserHasRole] = Field(..., description="用户拥有角色")
class RoleActive(BaseModel): class RoleActive(BaseModel):
@ -51,7 +51,7 @@ class RoleActive(BaseModel):
class UserAdd(UserIn): class UserAdd(UserIn):
"""新增用户模型""" """新增用户模型"""
roles: list[RoleActive] = Field(..., description="选择角色列表") roles: List[RoleActive] = Field(..., description="选择角色列表")
class UserQuery(QueryData): class UserQuery(QueryData):
@ -66,4 +66,4 @@ class UserPut(BaseModel):
nickname: str = Field(..., description="用户昵称") nickname: str = Field(..., description="用户昵称")
password: str = Field(..., description="密码") password: str = Field(..., description="密码")
roles: list[RoleActive] = Field(..., description="选择角色列表") roles: List[RoleActive] = Field(..., description="选择角色列表")

23
docker-compose.yml Normal file
View 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
View 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.*

View File

@ -1,2 +1,2 @@
VITE_BASE_URL = http://127.0.0.1:8000/api VITE_BASE_URL = http://localhost:1855
VITE_WEBSOCKET = ws://localhost:8000/ws VITE_WS = ws://localhost:1855/ws

11
frontend/Dockerfile Normal file
View 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
View 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;
}
}
}

View File

@ -2160,9 +2160,9 @@
} }
}, },
"node_modules/decode-uri-component": { "node_modules/decode-uri-component": {
"version": "0.2.0", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
"integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
"engines": { "engines": {
"node": ">=0.10" "node": ">=0.10"
} }
@ -7591,9 +7591,9 @@
} }
}, },
"decode-uri-component": { "decode-uri-component": {
"version": "0.2.0", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
"integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==" "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="
}, },
"deep-is": { "deep-is": {
"version": "0.1.4", "version": "0.1.4",

3
reset.sh Normal file
View File

@ -0,0 +1,3 @@
docker rm -f mini-web mini-rbac
docker rmi -f mini-rbac_backend mini-rbac_frontend mini-rbac-backend mini-rbac-frontend
docker-compose up -d