项目初次提交

This commit is contained in:
2025-04-11 08:54:28 +08:00
commit 9e14a3256f
220 changed files with 15673 additions and 0 deletions

View File

1075
apps/vadmin/auth/crud.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,14 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/7/7 13:41
# @File : __init__.py
# @IDE : PyCharm
# @desc : 简要说明
from .m2m import vadmin_auth_user_roles, vadmin_auth_role_menus, vadmin_auth_user_depts, vadmin_auth_role_depts
from .menu import VadminMenu
from .role import VadminRole
from .user import VadminUser
from .dept import VadminDept

View File

@ -0,0 +1,31 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023/10/23 13:41
# @File : dept.py
# @IDE : PyCharm
# @desc : 部门模型
from sqlalchemy.orm import Mapped, mapped_column
from db.db_base import BaseModel
from sqlalchemy import String, Boolean, Integer, ForeignKey
class VadminDept(BaseModel):
__tablename__ = "vadmin_auth_dept"
__table_args__ = ({'comment': '部门表'})
name: Mapped[str] = mapped_column(String(50), index=True, nullable=False, comment="部门名称")
dept_key: Mapped[str] = mapped_column(String(50), index=True, nullable=False, comment="部门标识")
disabled: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否禁用")
order: Mapped[int | None] = mapped_column(Integer, comment="显示排序")
desc: Mapped[str | None] = mapped_column(String(255), comment="描述")
owner: Mapped[str | None] = mapped_column(String(255), comment="负责人")
phone: Mapped[str | None] = mapped_column(String(255), comment="联系电话")
email: Mapped[str | None] = mapped_column(String(255), comment="邮箱")
parent_id: Mapped[int | None] = mapped_column(
Integer,
ForeignKey("vadmin_auth_dept.id", ondelete='CASCADE'),
comment="上级部门"
)

View File

@ -0,0 +1,41 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/7/7 13:41
# @File : m2m.py
# @IDE : PyCharm
# @desc : 关联中间表
from db.db_base import Base
from sqlalchemy import ForeignKey, Column, Table, Integer
vadmin_auth_user_roles = Table(
"vadmin_auth_user_roles",
Base.metadata,
Column("user_id", Integer, ForeignKey("vadmin_auth_user.id", ondelete="CASCADE")),
Column("role_id", Integer, ForeignKey("vadmin_auth_role.id", ondelete="CASCADE")),
)
vadmin_auth_role_menus = Table(
"vadmin_auth_role_menus",
Base.metadata,
Column("role_id", Integer, ForeignKey("vadmin_auth_role.id", ondelete="CASCADE")),
Column("menu_id", Integer, ForeignKey("vadmin_auth_menu.id", ondelete="CASCADE")),
)
vadmin_auth_user_depts = Table(
"vadmin_auth_user_depts",
Base.metadata,
Column("user_id", Integer, ForeignKey("vadmin_auth_user.id", ondelete="CASCADE")),
Column("dept_id", Integer, ForeignKey("vadmin_auth_dept.id", ondelete="CASCADE")),
)
vadmin_auth_role_depts = Table(
"vadmin_auth_role_depts",
Base.metadata,
Column("role_id", Integer, ForeignKey("vadmin_auth_role.id", ondelete="CASCADE")),
Column("dept_id", Integer, ForeignKey("vadmin_auth_dept.id", ondelete="CASCADE")),
)

View File

@ -0,0 +1,67 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/7/7 13:41
# @File : menu.py
# @IDE : PyCharm
# @desc : 菜单模型
from db.db_base import BaseModel
from sqlalchemy import String, Boolean, Integer, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column
class VadminMenu(BaseModel):
__tablename__ = "vadmin_auth_menu"
__table_args__ = ({'comment': '菜单表'})
title: Mapped[str] = mapped_column(String(50), comment="名称")
icon: Mapped[str | None] = mapped_column(String(50), comment="菜单图标")
redirect: Mapped[str | None] = mapped_column(String(100), comment="重定向地址")
component: Mapped[str | None] = mapped_column(String(255), comment="前端组件地址")
path: Mapped[str | None] = mapped_column(String(50), comment="前端路由地址")
disabled: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否禁用")
hidden: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否隐藏")
order: Mapped[int] = mapped_column(Integer, comment="排序")
menu_type: Mapped[str] = mapped_column(String(8), comment="菜单类型")
parent_id: Mapped[int | None] = mapped_column(
Integer,
ForeignKey("vadmin_auth_menu.id", ondelete='CASCADE'),
comment="父菜单"
)
perms: Mapped[str | None] = mapped_column(String(50), comment="权限标识", unique=False, index=True)
"""以下属性主要用于补全前端路由属性,"""
noCache: Mapped[bool] = mapped_column(
Boolean,
comment="如果设置为true则不会被 <keep-alive> 缓存(默认 false)",
default=False
)
breadcrumb: Mapped[bool] = mapped_column(
Boolean,
comment="如果设置为false则不会在breadcrumb面包屑中显示(默认 true)",
default=True
)
affix: Mapped[bool] = mapped_column(
Boolean,
comment="如果设置为true则会一直固定在tag项中(默认 false)",
default=False
)
noTagsView: Mapped[bool] = mapped_column(
Boolean,
comment="如果设置为true则不会出现在tag中(默认 false)",
default=False
)
canTo: Mapped[bool] = mapped_column(
Boolean,
comment="设置为true即使hidden为true也依然可以进行路由跳转(默认 false)",
default=False
)
alwaysShow: Mapped[bool] = mapped_column(
Boolean,
comment="""当你一个路由下面的 children 声明的路由大于1个时自动会变成嵌套的模式
只有一个时,会将那个子路由当做根路由显示在侧边栏,若你想不管路由下面的 children 声明的个数都显示你的根路由,
你可以设置 alwaysShow: true这样它就会忽略之前定义的规则一直显示根路由(默认 true)""",
default=True
)

View File

@ -0,0 +1,30 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/7/7 13:41
# @File : role.py
# @IDE : PyCharm
# @desc : 角色模型
from sqlalchemy.orm import relationship, Mapped, mapped_column
from db.db_base import BaseModel
from sqlalchemy import String, Boolean, Integer
from .menu import VadminMenu
from .dept import VadminDept
from .m2m import vadmin_auth_role_menus, vadmin_auth_role_depts
class VadminRole(BaseModel):
__tablename__ = "vadmin_auth_role"
__table_args__ = ({'comment': '角色表'})
name: Mapped[str] = mapped_column(String(50), index=True, comment="名称")
role_key: Mapped[str] = mapped_column(String(50), index=True, comment="权限字符")
data_range: Mapped[int] = mapped_column(Integer, default=4, comment="数据权限范围")
disabled: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否禁用")
order: Mapped[int | None] = mapped_column(Integer, comment="排序")
desc: Mapped[str | None] = mapped_column(String(255), comment="描述")
is_admin: Mapped[bool] = mapped_column(Boolean, comment="是否为超级角色", default=False)
menus: Mapped[set[VadminMenu]] = relationship(secondary=vadmin_auth_role_menus)
depts: Mapped[set[VadminDept]] = relationship(secondary=vadmin_auth_role_depts)

View File

@ -0,0 +1,72 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/7/7 13:41
# @File : user.py
# @IDE : PyCharm
# @desc : 用户模型
from datetime import datetime
from sqlalchemy.orm import relationship, Mapped, mapped_column
from db.db_base import BaseModel
from sqlalchemy import String, Boolean, DateTime
from passlib.context import CryptContext
from .role import VadminRole
from .dept import VadminDept
from .m2m import vadmin_auth_user_roles, vadmin_auth_user_depts
pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto')
class VadminUser(BaseModel):
__tablename__ = "vadmin_auth_user"
__table_args__ = ({'comment': '用户表'})
avatar: Mapped[str | None] = mapped_column(String(500), comment='头像')
telephone: Mapped[str] = mapped_column(String(11), nullable=False, index=True, comment="手机号", unique=False)
email: Mapped[str | None] = mapped_column(String(50), comment="邮箱地址")
name: Mapped[str] = mapped_column(String(50), index=True, nullable=False, comment="姓名")
nickname: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="昵称")
password: Mapped[str] = mapped_column(String(255), nullable=True, comment="密码")
gender: Mapped[str | None] = mapped_column(String(8), nullable=True, comment="性别")
is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否可用")
is_reset_password: Mapped[bool] = mapped_column(
Boolean,
default=False,
comment="是否已经重置密码,没有重置的,登陆系统后必须重置密码"
)
last_ip: Mapped[str | None] = mapped_column(String(50), comment="最后一次登录IP")
last_login: Mapped[datetime | None] = mapped_column(DateTime, comment="最近一次登录时间")
is_staff: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否为工作人员")
wx_server_openid: Mapped[str | None] = mapped_column(String(255), comment="服务端微信平台openid")
is_wx_server_openid: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否已有服务端微信平台openid")
roles: Mapped[set[VadminRole]] = relationship(secondary=vadmin_auth_user_roles)
depts: Mapped[set[VadminDept]] = relationship(secondary=vadmin_auth_user_depts)
@staticmethod
def get_password_hash(password: str) -> str:
"""
生成哈希密码
:param password: 原始密码
:return: 哈希密码
"""
return pwd_context.hash(password)
@staticmethod
def verify_password(password: str, hashed_password: str) -> bool:
"""
验证原始密码是否与哈希密码一致
:param password: 原始密码
:param hashed_password: 哈希密码
:return:
"""
return pwd_context.verify(password, hashed_password)
def is_admin(self) -> bool:
"""
获取该用户是否拥有最高权限
以最高权限为准
:return:
"""
return any([i.is_admin for i in self.roles])

View File

@ -0,0 +1,3 @@
from .user import UserParams
from .role import RoleParams
from .dept import DeptParams

View File

@ -0,0 +1,31 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023/12/18 10:19
# @File : dept.py
# @IDE : PyCharm
# @desc : 查询参数-类依赖项
"""
类依赖项-官方文档https://fastapi.tiangolo.com/zh/tutorial/dependencies/classes-as-dependencies/
"""
from fastapi import Depends, Query
from core.dependencies import Paging, QueryParams
class DeptParams(QueryParams):
"""
列表分页
"""
def __init__(
self,
name: str | None = Query(None, title="部门名称"),
dept_key: str | None = Query(None, title="部门标识"),
disabled: bool | None = Query(None, title="是否禁用"),
params: Paging = Depends()
):
super().__init__(params)
self.name = ("like", name)
self.dept_key = ("like", dept_key)
self.disabled = disabled

View File

@ -0,0 +1,31 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/18 22:19
# @File : role.py
# @IDE : PyCharm
# @desc : 查询参数-类依赖项
"""
类依赖项-官方文档https://fastapi.tiangolo.com/zh/tutorial/dependencies/classes-as-dependencies/
"""
from fastapi import Depends, Query
from core.dependencies import Paging, QueryParams
class RoleParams(QueryParams):
"""
列表分页
"""
def __init__(
self,
name: str | None = Query(None, title="角色名称"),
role_key: str | None = Query(None, title="权限字符"),
disabled: bool | None = Query(None, title="是否禁用"),
params: Paging = Depends()
):
super().__init__(params)
self.name = ("like", name)
self.role_key = ("like", role_key)
self.disabled = disabled

View File

@ -0,0 +1,37 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/18 22:19
# @File : user.py
# @IDE : PyCharm
# @desc : 查询参数-类依赖项
"""
类依赖项-官方文档https://fastapi.tiangolo.com/zh/tutorial/dependencies/classes-as-dependencies/
"""
from fastapi import Depends, Query
from core.dependencies import Paging, QueryParams
class UserParams(QueryParams):
"""
列表分页
"""
def __init__(
self,
name: str | None = Query(None, title="用户名称"),
telephone: str | None = Query(None, title="手机号"),
email: str | None = Query(None, title="邮箱"),
is_active: bool | None = Query(None, title="是否可用"),
is_staff: bool | None = Query(None, title="是否为工作人员"),
params: Paging = Depends()
):
super().__init__(params)
self.name = ("like", name)
self.telephone = ("like", telephone)
self.email = ("like", email)
self.is_active = is_active
self.is_staff = is_staff

View File

@ -0,0 +1,4 @@
from .user import UserOut, UserUpdate, User, UserIn, UserSimpleOut, ResetPwd, UserUpdateBaseInfo, UserPasswordOut
from .role import Role, RoleOut, RoleIn, RoleOptionsOut, RoleSimpleOut
from .menu import Menu, MenuSimpleOut, RouterOut, Meta, MenuTreeListOut
from .dept import Dept, DeptSimpleOut, DeptTreeListOut

View File

@ -0,0 +1,39 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023/10/25 12:19
# @File : dept.py
# @IDE : PyCharm
# @desc : pydantic 模型,用于数据库序列化操作
from pydantic import BaseModel, ConfigDict
from core.data_types import DatetimeStr
class Dept(BaseModel):
name: str
dept_key: str
disabled: bool = False
order: int | None = None
desc: str | None = None
owner: str | None = None
phone: str | None = None
email: str | None = None
parent_id: int | None = None
class DeptSimpleOut(Dept):
model_config = ConfigDict(from_attributes=True)
id: int
create_datetime: DatetimeStr
update_datetime: DatetimeStr
class DeptTreeListOut(DeptSimpleOut):
model_config = ConfigDict(from_attributes=True)
children: list[dict] = []

View File

@ -0,0 +1,66 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/18 22:19
# @File : role.py
# @IDE : PyCharm
# @desc : pydantic 模型,用于数据库序列化操作
from pydantic import BaseModel, ConfigDict
from core.data_types import DatetimeStr
class Menu(BaseModel):
title: str
icon: str | None = None
component: str | None = None
redirect: str | None = None
path: str | None = None
disabled: bool = False
hidden: bool = False
order: int | None = None
perms: str | None = None
parent_id: int | None = None
menu_type: str
alwaysShow: bool | None = True
noCache: bool | None = False
class MenuSimpleOut(Menu):
model_config = ConfigDict(from_attributes=True)
id: int
create_datetime: DatetimeStr
update_datetime: DatetimeStr
class Meta(BaseModel):
title: str
icon: str | None = None
hidden: bool = False
noCache: bool | None = False
breadcrumb: bool | None = True
affix: bool | None = False
noTagsView: bool | None = False
canTo: bool | None = False
alwaysShow: bool | None = True
# 路由展示
class RouterOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
name: str | None = None
component: str | None = None
path: str
redirect: str | None = None
meta: Meta | None = None
order: int | None = None
children: list[dict] = []
class MenuTreeListOut(MenuSimpleOut):
model_config = ConfigDict(from_attributes=True)
children: list[dict] = []

View File

@ -0,0 +1,52 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/18 22:19
# @File : role.py
# @IDE : PyCharm
# @desc : pydantic 模型,用于数据库序列化操作
from pydantic import BaseModel, ConfigDict, Field
from core.data_types import DatetimeStr
from .menu import MenuSimpleOut
from .dept import DeptSimpleOut
class Role(BaseModel):
name: str
disabled: bool = False
order: int | None = None
desc: str | None = None
data_range: int = 4
role_key: str
is_admin: bool = False
class RoleSimpleOut(Role):
model_config = ConfigDict(from_attributes=True)
id: int
create_datetime: DatetimeStr
update_datetime: DatetimeStr
class RoleOut(RoleSimpleOut):
model_config = ConfigDict(from_attributes=True)
menus: list[MenuSimpleOut] = []
depts: list[DeptSimpleOut] = []
class RoleIn(Role):
menu_ids: list[int] = []
dept_ids: list[int] = []
class RoleOptionsOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
label: str = Field(alias='name')
value: int = Field(alias='id')
disabled: bool

View File

@ -0,0 +1,98 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/18 22:19
# @File : user.py
# @IDE : PyCharm
# @desc : pydantic 模型,用于数据库序列化操作
from pydantic import BaseModel, ConfigDict, field_validator
from pydantic_core.core_schema import FieldValidationInfo
from core.data_types import Telephone, DatetimeStr, Email
from .role import RoleSimpleOut
from .dept import DeptSimpleOut
class User(BaseModel):
name: str
telephone: Telephone
email: Email | None = None
nickname: str | None = None
avatar: str | None = None
is_active: bool | None = True
is_staff: bool | None = True
gender: str | None = "0"
is_wx_server_openid: bool | None = False
class UserIn(User):
"""
创建用户
"""
role_ids: list[int] = []
dept_ids: list[int] = []
password: str | None = ""
class UserUpdateBaseInfo(BaseModel):
"""
更新用户基本信息
"""
name: str
telephone: Telephone
email: Email | None = None
nickname: str | None = None
gender: str | None = "0"
class UserUpdate(User):
"""
更新用户详细信息
"""
name: str | None = None
telephone: Telephone
email: Email | None = None
nickname: str | None = None
avatar: str | None = None
is_active: bool | None = True
is_staff: bool | None = False
gender: str | None = "0"
role_ids: list[int] = []
dept_ids: list[int] = []
class UserSimpleOut(User):
model_config = ConfigDict(from_attributes=True)
id: int
update_datetime: DatetimeStr
create_datetime: DatetimeStr
is_reset_password: bool | None = None
last_login: DatetimeStr | None = None
last_ip: str | None = None
class UserPasswordOut(UserSimpleOut):
model_config = ConfigDict(from_attributes=True)
password: str
class UserOut(UserSimpleOut):
model_config = ConfigDict(from_attributes=True)
roles: list[RoleSimpleOut] = []
depts: list[DeptSimpleOut] = []
class ResetPwd(BaseModel):
password: str
password_two: str
@field_validator('password_two')
def check_passwords_match(cls, v, info: FieldValidationInfo):
if 'password' in info.data and v != info.data['password']:
raise ValueError('两次密码不一致!')
return v

View File

@ -0,0 +1,7 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/8/8 11:02
# @File : __init__.py
# @IDE : PyCharm
# @desc : 简要说明

View File

@ -0,0 +1,114 @@
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/24 16:44
# @File : current.py
# @IDE : PyCharm
# @desc : 获取认证后的信息工具
from typing import Annotated
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload
from apps.vadmin.auth.crud import UserDal
from apps.vadmin.auth.models import VadminUser, VadminRole
from core.exception import CustomException
from utils import status
from .validation import AuthValidation
from fastapi import Request, Depends
from application import settings
from core.database import db_getter
from .validation.auth import Auth
class OpenAuth(AuthValidation):
"""
开放认证,无认证也可以访问
认证了以后可以获取到用户信息,无认证则获取不到
"""
async def __call__(
self,
request: Request,
token: Annotated[str, Depends(settings.oauth2_scheme)],
db: AsyncSession = Depends(db_getter)
):
"""
每次调用依赖此类的接口会执行该方法
"""
if not settings.OAUTH_ENABLE:
return Auth(db=db)
try:
telephone, password = self.validate_token(request, token)
user = await UserDal(db).get_data(telephone=telephone, password=password, v_return_none=True)
return await self.validate_user(request, user, db, is_all=True)
except CustomException:
return Auth(db=db)
class AllUserAuth(AuthValidation):
"""
支持所有用户认证
获取用户基本信息
"""
async def __call__(
self,
request: Request,
token: str = Depends(settings.oauth2_scheme),
db: AsyncSession = Depends(db_getter)
):
"""
每次调用依赖此类的接口会执行该方法
"""
if not settings.OAUTH_ENABLE:
return Auth(db=db)
telephone, password = self.validate_token(request, token)
user = await UserDal(db).get_data(telephone=telephone, password=password, v_return_none=True)
return await self.validate_user(request, user, db, is_all=True)
class FullAdminAuth(AuthValidation):
"""
只支持员工用户认证
获取员工用户完整信息
如果有权限,那么会验证该用户是否包括权限列表中的其中一个权限
"""
def __init__(self, permissions: list[str] | None = None):
if permissions:
self.permissions = set(permissions)
else:
self.permissions = None
async def __call__(
self,
request: Request,
token: str = Depends(settings.oauth2_scheme),
db: AsyncSession = Depends(db_getter)
) -> Auth:
"""
每次调用依赖此类的接口会执行该方法
"""
if not settings.OAUTH_ENABLE:
return Auth(db=db)
telephone, password = self.validate_token(request, token)
options = [
joinedload(VadminUser.roles).subqueryload(VadminRole.menus),
joinedload(VadminUser.roles).subqueryload(VadminRole.depts),
joinedload(VadminUser.depts)
]
user = await UserDal(db).get_data(
telephone=telephone,
password=password,
v_return_none=True,
v_options=options,
is_staff=True
)
result = await self.validate_user(request, user, db, is_all=False)
permissions = self.get_user_permissions(user)
if permissions != {'*.*.*'} and self.permissions:
if not (self.permissions & permissions):
raise CustomException(msg="无权限操作", code=status.HTTP_403_FORBIDDEN)
return result

View File

@ -0,0 +1,180 @@
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/24 16:44
# @File : views.py
# @IDE : PyCharm
# @desc : 安全认证视图
"""
JWT 表示 「JSON Web Tokens」。https://jwt.io/
它是一个将 JSON 对象编码为密集且没有空格的长字符串的标准。
通过这种方式,你可以创建一个有效期为 1 周的令牌。然后当用户第二天使用令牌重新访问时,你知道该用户仍然处于登入状态。
一周后令牌将会过期,用户将不会通过认证,必须再次登录才能获得一个新令牌。
我们需要安装 python-jose 以在 Python 中生成和校验 JWT 令牌pip install python-jose[cryptography]
PassLib 是一个用于处理哈希密码的很棒的 Python 包。它支持许多安全哈希算法以及配合算法使用的实用程序。
推荐的算法是 「Bcrypt」pip install passlib[bcrypt]
"""
from datetime import timedelta
from redis.asyncio import Redis
from fastapi import APIRouter, Depends, Request, Body
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession
from core.database import db_getter, redis_getter
from core.exception import CustomException
from utils import status
from utils.response import SuccessResponse, ErrorResponse
from application import settings
from .login_manage import LoginManage
from .validation import LoginForm, WXLoginForm
from apps.vadmin.record.models import VadminLoginRecord
from apps.vadmin.auth.crud import MenuDal, UserDal
from apps.vadmin.auth.models import VadminUser
from .current import FullAdminAuth
from .validation.auth import Auth
from utils.wx.oauth import WXOAuth
import jwt
app = APIRouter()
@app.post("/api/login", summary="API 手机号密码登录", description="Swagger API 文档登录认证")
async def api_login_for_access_token(
request: Request,
data: OAuth2PasswordRequestForm = Depends(),
db: AsyncSession = Depends(db_getter)
):
user = await UserDal(db).get_data(telephone=data.username, v_return_none=True)
error_code = status.HTTP_401_UNAUTHORIZED
if not user:
raise CustomException(status_code=error_code, code=error_code, msg="该手机号不存在")
result = VadminUser.verify_password(data.password, user.password)
if not result:
raise CustomException(status_code=error_code, code=error_code, msg="手机号或密码错误")
if not user.is_active:
raise CustomException(status_code=error_code, code=error_code, msg="此手机号已被冻结")
elif not user.is_staff:
raise CustomException(status_code=error_code, code=error_code, msg="此手机号无权限")
access_token = LoginManage.create_token({"sub": user.telephone, "password": user.password})
record = LoginForm(platform='2', method='0', telephone=data.username, password=data.password)
resp = {"access_token": access_token, "token_type": "bearer"}
await VadminLoginRecord.create_login_record(db, record, True, request, resp)
return resp
@app.post("/login", summary="手机号密码登录", description="员工登录通道限制最多输错次数达到最大值后将is_active=False")
async def login_for_access_token(
request: Request,
data: LoginForm,
manage: LoginManage = Depends(),
db: AsyncSession = Depends(db_getter)
):
try:
result = await manage.password_login(data, db, request)
if not result.status:
raise ValueError(result.msg)
access_token = LoginManage.create_token(
{"sub": result.user.telephone, "is_refresh": False, "password": result.user.password}
)
expires = timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
refresh_token = LoginManage.create_token(
payload={"sub": result.user.telephone, "is_refresh": True, "password": result.user.password},
expires=expires
)
resp = {
"access_token": access_token,
"refresh_token": refresh_token,
"token_type": "bearer",
"is_reset_password": result.user.is_reset_password,
"is_wx_server_openid": result.user.is_wx_server_openid
}
await VadminLoginRecord.create_login_record(db, data, True, request, resp)
return SuccessResponse(resp)
except ValueError as e:
await VadminLoginRecord.create_login_record(db, data, False, request, {"message": str(e)})
return ErrorResponse(msg=str(e))
@app.post("/wx/login", summary="微信服务端一键登录", description="员工登录通道")
async def wx_login_for_access_token(
request: Request,
data: WXLoginForm,
db: AsyncSession = Depends(db_getter),
rd: Redis = Depends(redis_getter)
):
try:
if data.platform != "1" or data.method != "2":
raise ValueError("无效参数")
wx = WXOAuth(rd, 0)
telephone = await wx.parsing_phone_number(data.code)
if not telephone:
raise ValueError("无效Code")
data.telephone = telephone
user = await UserDal(db).get_data(telephone=telephone, v_return_none=True)
if not user:
raise ValueError("手机号不存在")
elif not user.is_active:
raise ValueError("手机号已被冻结")
except ValueError as e:
await VadminLoginRecord.create_login_record(db, data, False, request, {"message": str(e)})
return ErrorResponse(msg=str(e))
# 更新登录时间
await UserDal(db).update_login_info(user, request.client.host)
# 登录成功创建 token
access_token = LoginManage.create_token({"sub": user.telephone, "is_refresh": False, "password": user.password})
expires = timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
refresh_token = LoginManage.create_token(
payload={"sub": user.telephone, "is_refresh": True, "password": user.password},
expires=expires
)
resp = {
"access_token": access_token,
"refresh_token": refresh_token,
"token_type": "bearer",
"is_reset_password": user.is_reset_password,
"is_wx_server_openid": user.is_wx_server_openid
}
await VadminLoginRecord.create_login_record(db, data, True, request, resp)
return SuccessResponse(resp)
@app.get("/getMenuList", summary="获取当前用户菜单树")
async def get_menu_list(auth: Auth = Depends(FullAdminAuth())):
return SuccessResponse(await MenuDal(auth.db).get_routers(auth.user))
@app.post("/token/refresh", summary="刷新Token")
async def token_refresh(refresh: str = Body(..., title="刷新Token")):
error_code = status.HTTP_401_UNAUTHORIZED
try:
payload = jwt.decode(refresh, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
telephone: str = payload.get("sub")
is_refresh: bool = payload.get("is_refresh")
password: str = payload.get("password")
if not telephone or not is_refresh or not password:
return ErrorResponse("未认证,请您重新登录", code=error_code, status=error_code)
except jwt.exceptions.InvalidSignatureError:
return ErrorResponse("无效认证,请您重新登录", code=error_code, status=error_code)
except jwt.exceptions.ExpiredSignatureError:
return ErrorResponse("登录已超时,请您重新登录", code=error_code, status=error_code)
access_token = LoginManage.create_token({"sub": telephone, "is_refresh": False, "password": password})
expires = timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
refresh_token = LoginManage.create_token(
payload={"sub": telephone, "is_refresh": True, "password": password},
expires=expires
)
resp = {
"access_token": access_token,
"refresh_token": refresh_token,
"token_type": "bearer"
}
return SuccessResponse(resp)

View File

@ -0,0 +1,62 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/8/8 11:02
# @File : auth_util.py
# @IDE : PyCharm
# @desc : 简要说明
from datetime import datetime, timedelta
from fastapi import Request
from application import settings
import jwt
from apps.vadmin.auth import models
from core.database import redis_getter
from utils.sms.code import CodeSMS
from .validation import LoginValidation, LoginForm, LoginResult
class LoginManage:
"""
登录认证工具
"""
@LoginValidation
async def password_login(self, data: LoginForm, user: models.VadminUser, **kwargs) -> LoginResult:
"""
验证用户密码
"""
result = models.VadminUser.verify_password(data.password, user.password)
if result:
return LoginResult(status=True, msg="验证成功")
return LoginResult(status=False, msg="手机号或密码错误")
@LoginValidation
async def sms_login(self, data: LoginForm, request: Request, **kwargs) -> LoginResult:
"""
验证用户短信验证码
"""
rd = redis_getter(request)
sms = CodeSMS(data.telephone, rd)
result = await sms.check_sms_code(data.password)
if result:
return LoginResult(status=True, msg="验证成功")
return LoginResult(status=False, msg="验证码错误")
@staticmethod
def create_token(payload: dict, expires: timedelta = None):
"""
创建一个生成新的访问令牌的工具函数。
pyjwthttps://github.com/jpadilla/pyjwt/blob/master/docs/usage.rst
jwt 博客https://geek-docs.com/python/python-tutorial/j_python-jwt.html
#TODO 传入的时间为UTC时间datetime.datetime类型但是在解码时获取到的是本机时间的时间戳
"""
if expires:
expire = datetime.utcnow() + expires
else:
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
payload.update({"exp": expire})
encoded_jwt = jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
return encoded_jwt

View File

@ -0,0 +1,10 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/11/9 10:14
# @File : __init__.py.py
# @IDE : PyCharm
# @desc : 简要说明
from .auth import Auth, AuthValidation
from .login import LoginValidation, LoginForm, LoginResult, WXLoginForm

View File

@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/24 16:44
# @File : auth.py
# @IDE : PyCharm
# @desc : 用户凭证验证装饰器
from fastapi import Request
import jwt
from pydantic import BaseModel
from application import settings
from sqlalchemy.ext.asyncio import AsyncSession
from apps.vadmin.auth.models import VadminUser
from core.exception import CustomException
from utils import status
from datetime import timedelta, datetime
from apps.vadmin.auth.crud import UserDal
class Auth(BaseModel):
user: VadminUser = None
db: AsyncSession
data_range: int | None = None
dept_ids: list | None = []
class Config:
# 接收任意类型
arbitrary_types_allowed = True
class AuthValidation:
"""
用于用户每次调用接口时验证用户提交的token是否正确并从token中获取用户信息
"""
# status_code = 401 时表示强制要求重新登录因账号已冻结账号已过期手机号码错误刷新token无效等问题导致
# 只有 code = 401 时,表示 token 过期,要求刷新 token
# 只有 code = 错误值时,只是报错,不重新登陆
error_code = status.HTTP_401_UNAUTHORIZED
warning_code = status.HTTP_ERROR
# status_code = 403 时,表示强制要求重新登录,因无系统权限,而进入到系统访问等问题导致
@classmethod
def validate_token(cls, request: Request, token: str | None) -> tuple[str, bool]:
"""
验证用户 token
"""
if not token:
raise CustomException(
msg="请您先登录!",
code=status.HTTP_403_FORBIDDEN,
status_code=status.HTTP_403_FORBIDDEN
)
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
telephone: str = payload.get("sub")
exp: int = payload.get("exp")
is_refresh: bool = payload.get("is_refresh")
password: bool = payload.get("password")
if not telephone or is_refresh or not password:
raise CustomException(
msg="未认证,请您重新登录",
code=status.HTTP_403_FORBIDDEN,
status_code=status.HTTP_403_FORBIDDEN
)
# 计算当前时间 + 缓冲时间是否大于等于 JWT 过期时间
buffer_time = (datetime.now() + timedelta(minutes=settings.ACCESS_TOKEN_CACHE_MINUTES)).timestamp()
# print("过期时间", exp, datetime.fromtimestamp(exp))
# print("当前时间", buffer_time, datetime.fromtimestamp(buffer_time))
# print("剩余时间", exp - buffer_time)
if buffer_time >= exp:
request.scope["if-refresh"] = 1
else:
request.scope["if-refresh"] = 0
except (jwt.exceptions.InvalidSignatureError, jwt.exceptions.DecodeError):
raise CustomException(
msg="无效认证,请您重新登录",
code=status.HTTP_403_FORBIDDEN,
status_code=status.HTTP_403_FORBIDDEN
)
except jwt.exceptions.ExpiredSignatureError:
raise CustomException(msg="认证已失效,请您重新登录", code=cls.error_code, status_code=cls.error_code)
return telephone, password
@classmethod
async def validate_user(cls, request: Request, user: VadminUser, db: AsyncSession, is_all: bool = True) -> Auth:
"""
验证用户信息
:param request:
:param user:
:param db:
:param is_all: 是否所有人访问,不加权限
:return:
"""
if user is None:
raise CustomException(msg="未认证,请您重新登陆", code=cls.error_code, status_code=cls.error_code)
elif not user.is_active:
raise CustomException(msg="用户已被冻结!", code=cls.error_code, status_code=cls.error_code)
request.scope["telephone"] = user.telephone
request.scope["user_id"] = user.id
request.scope["user_name"] = user.name
try:
request.scope["body"] = await request.body()
except RuntimeError:
request.scope["body"] = "获取失败"
if is_all:
return Auth(user=user, db=db)
data_range, dept_ids = await cls.get_user_data_range(user, db)
return Auth(user=user, db=db, data_range=data_range, dept_ids=dept_ids)
@classmethod
def get_user_permissions(cls, user: VadminUser) -> set:
"""
获取员工用户所有权限列表
:param user: 用户实例
:return:
"""
if user.is_admin():
return {'*.*.*'}
permissions = set()
for role_obj in user.roles:
for menu in role_obj.menus:
if menu.perms and not menu.disabled:
permissions.add(menu.perms)
return permissions
@classmethod
async def get_user_data_range(cls, user: VadminUser, db: AsyncSession) -> tuple:
"""
获取用户数据范围
0 仅本人数据权限 create_user_id 查询
1 本部门数据权限 部门 id 左连接查询
2 本部门及以下数据权限 部门 id 左连接查询
3 自定义数据权限 部门 id 左连接查询
4 全部数据权限 无
:param user:
:param db:
:return:
"""
if user.is_admin():
return 4, ["*"]
data_range = max([i.data_range for i in user.roles])
dept_ids = set()
if data_range == 0:
pass
elif data_range == 1:
for dept in user.depts:
dept_ids.add(dept.id)
elif data_range == 2:
# 递归获取部门列表
dept_ids = await UserDal(db).recursion_get_dept_ids(user)
elif data_range == 3:
for role_obj in user.roles:
for dept in role_obj.depts:
dept_ids.add(dept.id)
elif data_range == 4:
dept_ids.add("*")
return data_range, list(dept_ids)

View File

@ -0,0 +1,92 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/11/9 10:15
# @File : login.py
# @IDE : PyCharm
# @desc : 登录验证装饰器
from fastapi import Request
from pydantic import BaseModel, field_validator
from sqlalchemy.ext.asyncio import AsyncSession
from application.settings import DEFAULT_AUTH_ERROR_MAX_NUMBER, DEMO, REDIS_DB_ENABLE
from apps.vadmin.auth import crud, schemas
from core.database import redis_getter
from core.validator import vali_telephone
from utils.count import Count
class LoginForm(BaseModel):
telephone: str
password: str
method: str = '0' # 认证方式0密码登录1短信登录2微信一键登录
platform: str = '0' # 登录平台0PC端管理系统1移动端管理系统
# 重用验证器https://docs.pydantic.dev/dev-v2/usage/validators/#reuse-validators
normalize_telephone = field_validator('telephone')(vali_telephone)
class WXLoginForm(BaseModel):
telephone: str | None = None
code: str
method: str = '2' # 认证方式0密码登录1短信登录2微信一键登录
platform: str = '1' # 登录平台0PC端管理系统1移动端管理系统
class LoginResult(BaseModel):
status: bool | None = False
user: schemas.UserPasswordOut | None = None
msg: str | None = None
class Config:
arbitrary_types_allowed = True
class LoginValidation:
"""
验证用户登录时提交的数据是否有效
"""
def __init__(self, func):
self.func = func
async def __call__(self, data: LoginForm, db: AsyncSession, request: Request) -> LoginResult:
self.result = LoginResult()
if data.platform not in ["0", "1"] or data.method not in ["0", "1"]:
self.result.msg = "无效参数"
return self.result
user = await crud.UserDal(db).get_data(telephone=data.telephone, v_return_none=True)
if not user:
self.result.msg = "该手机号不存在!"
return self.result
result = await self.func(self, data=data, user=user, request=request)
if REDIS_DB_ENABLE:
count_key = f"{data.telephone}_password_auth" if data.method == '0' else f"{data.telephone}_sms_auth"
count = Count(redis_getter(request), count_key)
else:
count = None
if not result.status:
self.result.msg = result.msg
if not DEMO and count:
number = await count.add(ex=86400)
if number >= DEFAULT_AUTH_ERROR_MAX_NUMBER:
await count.reset()
# 如果等于最大次数,那么就将用户 is_active=False
user.is_active = False
await db.flush()
elif not user.is_active:
self.result.msg = "此手机号已被冻结!"
elif data.platform in ["0", "1"] and not user.is_staff:
self.result.msg = "此手机号无权限!"
else:
if not DEMO and count:
await count.delete()
self.result.msg = "OK"
self.result.status = True
self.result.user = schemas.UserPasswordOut.model_validate(user)
await crud.UserDal(db).update_login_info(user, request.client.host)
return self.result

307
apps/vadmin/auth/views.py Normal file
View File

@ -0,0 +1,307 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/2/24 17:02
# @File : views.py
# @IDE : PyCharm
# @desc : 简要说明
from redis.asyncio import Redis
from fastapi import APIRouter, Depends, Body, UploadFile, Request
from sqlalchemy.orm import joinedload
from core.database import redis_getter
from utils.response import SuccessResponse, ErrorResponse
from . import schemas, crud, models
from core.dependencies import IdList
from apps.vadmin.auth.utils.current import AllUserAuth, FullAdminAuth, OpenAuth
from apps.vadmin.auth.utils.validation.auth import Auth
from .params import UserParams, RoleParams, DeptParams
app = APIRouter()
###########################################################
# 接口测试
###########################################################
@app.get("/test", summary="接口测试")
async def test(auth: Auth = Depends(OpenAuth())):
return SuccessResponse(await crud.TestDal(auth.db).relationship_where_operations_has())
###########################################################
# 用户管理
###########################################################
@app.get("/users", summary="获取用户列表")
async def get_users(
params: UserParams = Depends(),
auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.list"]))
):
model = models.VadminUser
options = [joinedload(model.roles), joinedload(model.depts)]
schema = schemas.UserOut
datas, count = await crud.UserDal(auth.db).get_datas(
**params.dict(),
v_options=options,
v_schema=schema,
v_return_count=True
)
return SuccessResponse(datas, count=count)
@app.post("/users", summary="创建用户")
async def create_user(data: schemas.UserIn, auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.create"]))):
return SuccessResponse(await crud.UserDal(auth.db).create_data(data=data))
@app.delete("/users", summary="批量删除用户", description="软删除,删除后清空所关联的角色")
async def delete_users(ids: IdList = Depends(), auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.delete"]))):
if auth.user.id in ids.ids:
return ErrorResponse("不能删除当前登录用户")
elif 1 in ids.ids:
return ErrorResponse("不能删除超级管理员用户")
await crud.UserDal(auth.db).delete_datas(ids=ids.ids, v_soft=True, is_active=False)
return SuccessResponse("删除成功")
@app.put("/users/{data_id}", summary="更新用户信息")
async def put_user(
data_id: int,
data: schemas.UserUpdate,
auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.update"]))
):
return SuccessResponse(await crud.UserDal(auth.db).put_data(data_id, data))
@app.get("/users/{data_id}", summary="获取用户信息")
async def get_user(
data_id: int,
auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.view", "auth.user.update"]))
):
model = models.VadminUser
options = [joinedload(model.roles), joinedload(model.depts)]
schema = schemas.UserOut
return SuccessResponse(await crud.UserDal(auth.db).get_data(data_id, v_options=options, v_schema=schema))
@app.post("/user/current/reset/password", summary="重置当前用户密码")
async def user_current_reset_password(data: schemas.ResetPwd, auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.UserDal(auth.db).reset_current_password(auth.user, data))
@app.post("/user/current/update/info", summary="更新当前用户基本信息")
async def post_user_current_update_info(data: schemas.UserUpdateBaseInfo, auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.UserDal(auth.db).update_current_info(auth.user, data))
@app.post("/user/current/update/avatar", summary="更新当前用户头像")
async def post_user_current_update_avatar(file: UploadFile, auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.UserDal(auth.db).update_current_avatar(auth.user, file))
@app.get("/user/admin/current/info", summary="获取当前管理员信息")
async def get_user_admin_current_info(auth: Auth = Depends(FullAdminAuth())):
result = schemas.UserOut.model_validate(auth.user).model_dump()
result["permissions"] = list(FullAdminAuth.get_user_permissions(auth.user))
return SuccessResponse(result)
@app.post("/user/export/query/list/to/excel", summary="导出用户查询列表为excel")
async def post_user_export_query_list(
header: list = Body(..., title="表头与对应字段"),
params: UserParams = Depends(),
auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.export"]))
):
return SuccessResponse(await crud.UserDal(auth.db).export_query_list(header, params))
@app.get("/user/download/import/template", summary="下载最新批量导入用户模板")
async def get_user_download_new_import_template(auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.UserDal(auth.db).download_import_template())
@app.post("/import/users", summary="批量导入用户")
async def post_import_users(file: UploadFile, auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.import"]))):
return SuccessResponse(await crud.UserDal(auth.db).import_users(file))
@app.post("/users/init/password/send/sms", summary="初始化所选用户密码并发送通知短信")
async def post_users_init_password(
request: Request,
ids: IdList = Depends(),
auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.reset"])),
rd: Redis = Depends(redis_getter)
):
return SuccessResponse(await crud.UserDal(auth.db).init_password_send_sms(ids.ids, rd))
@app.post("/users/init/password/send/email", summary="初始化所选用户密码并发送通知邮件")
async def post_users_init_password_send_email(
request: Request,
ids: IdList = Depends(),
auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.reset"])),
rd: Redis = Depends(redis_getter)
):
return SuccessResponse(await crud.UserDal(auth.db).init_password_send_email(ids.ids, rd))
@app.put("/users/wx/server/openid", summary="更新当前用户服务端微信平台openid")
async def put_user_wx_server_openid(code: str, auth: Auth = Depends(AllUserAuth()), rd: Redis = Depends(redis_getter)):
result = await crud.UserDal(auth.db).update_wx_server_openid(code, auth.user, rd)
return SuccessResponse(result)
###########################################################
# 角色管理
###########################################################
@app.get("/roles", summary="获取角色列表")
async def get_roles(
params: RoleParams = Depends(),
auth: Auth = Depends(FullAdminAuth(permissions=["auth.role.list"]))
):
datas, count = await crud.RoleDal(auth.db).get_datas(**params.dict(), v_return_count=True)
return SuccessResponse(datas, count=count)
@app.post("/roles", summary="创建角色信息")
async def create_role(role: schemas.RoleIn, auth: Auth = Depends(FullAdminAuth(permissions=["auth.role.create"]))):
return SuccessResponse(await crud.RoleDal(auth.db).create_data(data=role))
@app.delete("/roles", summary="批量删除角色", description="硬删除, 如果存在用户关联则无法删除")
async def delete_roles(ids: IdList = Depends(), auth: Auth = Depends(FullAdminAuth(permissions=["auth.role.delete"]))):
if 1 in ids.ids:
return ErrorResponse("不能删除管理员角色")
await crud.RoleDal(auth.db).delete_datas(ids.ids, v_soft=False)
return SuccessResponse("删除成功")
@app.put("/roles/{data_id}", summary="更新角色信息")
async def put_role(
data_id: int,
data: schemas.RoleIn,
auth: Auth = Depends(FullAdminAuth(permissions=["auth.role.update"]))
):
if 1 == data_id:
return ErrorResponse("不能修改管理员角色")
return SuccessResponse(await crud.RoleDal(auth.db).put_data(data_id, data))
@app.get("/roles/options", summary="获取角色选择项")
async def get_role_options(auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.create", "auth.user.update"]))):
return SuccessResponse(await crud.RoleDal(auth.db).get_select_datas())
@app.get("/roles/{data_id}", summary="获取角色信息")
async def get_role(
data_id: int,
auth: Auth = Depends(FullAdminAuth(permissions=["auth.role.view", "auth.role.update"]))
):
model = models.VadminRole
options = [joinedload(model.menus), joinedload(model.depts)]
schema = schemas.RoleOut
return SuccessResponse(await crud.RoleDal(auth.db).get_data(data_id, v_options=options, v_schema=schema))
###########################################################
# 菜单管理
###########################################################
@app.get("/menus", summary="获取菜单列表")
async def get_menus(auth: Auth = Depends(FullAdminAuth(permissions=["auth.menu.list"]))):
datas = await crud.MenuDal(auth.db).get_tree_list(mode=1)
return SuccessResponse(datas)
@app.get("/menus/tree/options", summary="获取菜单树选择项,添加/修改菜单时使用")
async def get_menus_options(auth: Auth = Depends(FullAdminAuth(permissions=["auth.menu.create", "auth.menu.update"]))):
datas = await crud.MenuDal(auth.db).get_tree_list(mode=2)
return SuccessResponse(datas)
@app.get("/menus/role/tree/options", summary="获取菜单列表树信息,角色权限使用")
async def get_menus_treeselect(
auth: Auth = Depends(FullAdminAuth(permissions=["auth.role.create", "auth.role.update"]))
):
return SuccessResponse(await crud.MenuDal(auth.db).get_tree_list(mode=3))
@app.post("/menus", summary="创建菜单信息")
async def create_menu(menu: schemas.Menu, auth: Auth = Depends(FullAdminAuth(permissions=["auth.menu.create"]))):
if menu.parent_id:
menu.alwaysShow = False
return SuccessResponse(await crud.MenuDal(auth.db).create_data(data=menu))
@app.delete("/menus", summary="批量删除菜单", description="硬删除, 如果存在角色关联则无法删除")
async def delete_menus(ids: IdList = Depends(), auth: Auth = Depends(FullAdminAuth(permissions=["auth.menu.delete"]))):
await crud.MenuDal(auth.db).delete_datas(ids.ids, v_soft=False)
return SuccessResponse("删除成功")
@app.put("/menus/{data_id}", summary="更新菜单信息")
async def put_menus(
data_id: int,
data: schemas.Menu, auth: Auth = Depends(FullAdminAuth(permissions=["auth.menu.update"]))
):
return SuccessResponse(await crud.MenuDal(auth.db).put_data(data_id, data))
@app.get("/menus/{data_id}", summary="获取菜单信息")
async def get_menus(
data_id: int,
auth: Auth = Depends(FullAdminAuth(permissions=["auth.menu.view", "auth.menu.update"]))
):
schema = schemas.MenuSimpleOut
return SuccessResponse(await crud.MenuDal(auth.db).get_data(data_id, v_schema=schema))
@app.get("/role/menus/tree/{role_id}", summary="获取菜单列表树信息以及角色菜单权限ID角色权限使用")
async def get_role_menu_tree(
role_id: int,
auth: Auth = Depends(FullAdminAuth(permissions=["auth.role.create", "auth.role.update"]))
):
tree_data = await crud.MenuDal(auth.db).get_tree_list(mode=3)
role_menu_tree = await crud.RoleDal(auth.db).get_role_menu_tree(role_id)
return SuccessResponse({"role_menu_tree": role_menu_tree, "menus": tree_data})
###########################################################
# 部门管理
###########################################################
@app.get("/depts", summary="获取部门列表")
async def get_depts(
params: DeptParams = Depends(),
auth: Auth = Depends(FullAdminAuth())
):
datas = await crud.DeptDal(auth.db).get_tree_list(1)
return SuccessResponse(datas)
@app.get("/dept/tree/options", summary="获取部门树选择项,添加/修改部门时使用")
async def get_dept_options(auth: Auth = Depends(FullAdminAuth())):
datas = await crud.DeptDal(auth.db).get_tree_list(mode=2)
return SuccessResponse(datas)
@app.get("/dept/user/tree/options", summary="获取部门树选择项,添加/修改用户时使用")
async def get_dept_treeselect(auth: Auth = Depends(FullAdminAuth())):
return SuccessResponse(await crud.DeptDal(auth.db).get_tree_list(mode=3))
@app.post("/depts", summary="创建部门信息")
async def create_dept(data: schemas.Dept, auth: Auth = Depends(FullAdminAuth())):
return SuccessResponse(await crud.DeptDal(auth.db).create_data(data=data))
@app.delete("/depts", summary="批量删除部门", description="硬删除, 如果存在用户关联则无法删除")
async def delete_depts(ids: IdList = Depends(), auth: Auth = Depends(FullAdminAuth())):
await crud.DeptDal(auth.db).delete_datas(ids.ids, v_soft=False)
return SuccessResponse("删除成功")
@app.put("/depts/{data_id}", summary="更新部门信息")
async def put_dept(
data_id: int,
data: schemas.Dept,
auth: Auth = Depends(FullAdminAuth())
):
return SuccessResponse(await crud.DeptDal(auth.db).put_data(data_id, data))