forked from iti-framework/iTi-Flask
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
181 lines
5.7 KiB
Python
181 lines
5.7 KiB
Python
from marshmallow import post_dump
|
|
from sqlalchemy import distinct, select
|
|
from iti.applications.extensions import db, jwt, cache_simple
|
|
from sqlalchemy.ext.hybrid import hybrid_property
|
|
from werkzeug.security import generate_password_hash, check_password_hash
|
|
import datetime
|
|
from .sys_rel_role_menu import sys_role_menu
|
|
from .sys_rel_user_role import sys_user_role
|
|
from .sys_menu import SysMenu
|
|
from sqlalchemy.orm import joinedload
|
|
from iti.applications.common.enums import GenderEnum, StatusEnum
|
|
from iti.applications.common.utils import BaseSchema
|
|
from apiflask.fields import String, DateTime, Enum, Nested, List
|
|
from iti.applications.common.crud import BaseModelMixin
|
|
from .sys_role import Role
|
|
|
|
|
|
@jwt.user_lookup_loader
|
|
def user_lookup_loader(header, payload):
|
|
"""
|
|
用户查找加载器
|
|
"""
|
|
identity = payload.get("sub", None)
|
|
# 过期时间
|
|
exp = payload.get("exp", None)
|
|
|
|
return load_with_cache(identity=identity, exp=exp)
|
|
|
|
|
|
def load_with_cache(identity, exp):
|
|
if identity is None:
|
|
return None
|
|
cached = cache_simple.get(key=f"user_{identity}")
|
|
if cached is not None:
|
|
return cached
|
|
dbUser = db.session.scalar(
|
|
select(User)
|
|
.filter_by(id=identity)
|
|
.options(joinedload(User.roles).noload(Role.menus), joinedload(User.depts))
|
|
)
|
|
if dbUser is None:
|
|
return None
|
|
dbUser.permissions
|
|
|
|
# 计算需缓存时长
|
|
if exp is None:
|
|
return None
|
|
expTime = datetime.datetime.fromtimestamp(exp)
|
|
now = datetime.datetime.now()
|
|
|
|
cache_simple.set(
|
|
key=f"user_{identity}", value=dbUser, timeout=(expTime - now).seconds
|
|
)
|
|
return dbUser
|
|
|
|
|
|
class User(BaseModelMixin):
|
|
"""
|
|
用户表
|
|
"""
|
|
|
|
__tablename__ = "sys_user"
|
|
username = db.Column(db.String(64), nullable=False, comment="用户名")
|
|
phone = db.Column(db.String(13), nullable=True, comment="手机号")
|
|
email = db.Column(db.String(255), nullable=True, comment="邮箱")
|
|
_password = db.Column("password", db.String(255), nullable=False, comment="密码")
|
|
realname = db.Column(db.String(32), nullable=True, comment="真实姓名")
|
|
desc = db.Column(db.Text, nullable=True, comment="描述")
|
|
avatar = db.Column(db.String(255), nullable=True, comment="头像")
|
|
gender = db.Column(
|
|
db.Enum(GenderEnum, values_callable=lambda x: [e.value for e in x]),
|
|
nullable=False,
|
|
default=GenderEnum.SECURE.value,
|
|
comment="性别",
|
|
)
|
|
status = db.Column(
|
|
db.Enum(StatusEnum, values_callable=lambda x: [e.value for e in x]),
|
|
nullable=False,
|
|
default=StatusEnum.ENABLED.value,
|
|
comment="状态",
|
|
)
|
|
# 关系
|
|
roles = db.relationship(
|
|
"Role",
|
|
secondary="sys_user_role",
|
|
primaryjoin="User.id == sys_user_role.c.user_id",
|
|
secondaryjoin="and_(Role.id == sys_user_role.c.role_id, Role.status == 'enabled')",
|
|
back_populates="users",
|
|
)
|
|
depts = db.relationship(
|
|
"SysDept",
|
|
secondary="sys_user_dept",
|
|
primaryjoin="User.id == sys_user_dept.c.user_id",
|
|
secondaryjoin="and_(SysDept.id == sys_user_dept.c.dept_id, SysDept.status == 'enabled')",
|
|
back_populates="users",
|
|
)
|
|
|
|
@hybrid_property
|
|
def password(self):
|
|
return self._password
|
|
|
|
@password.setter
|
|
def password(self, value):
|
|
if value is not None:
|
|
self._password = generate_password_hash(value, method="pbkdf2:sha256")
|
|
|
|
def check_password(self, value) -> bool:
|
|
return check_password_hash(self._password, value)
|
|
|
|
_permissions = []
|
|
|
|
@hybrid_property
|
|
def permissions(self):
|
|
if len(self._permissions) == 0:
|
|
self._permissions = self.get_permissions()
|
|
return self._permissions
|
|
|
|
@permissions.setter
|
|
def permissions(self, value):
|
|
self._permissions = value
|
|
|
|
def get_permissions(self):
|
|
permissions = db.session.scalars(
|
|
select(distinct(SysMenu.auth_code))
|
|
.join(sys_role_menu, SysMenu.id == sys_role_menu.c.menu_id)
|
|
.join(sys_user_role, sys_role_menu.c.role_id == sys_user_role.c.role_id)
|
|
.filter(
|
|
sys_user_role.c.user_id == self.id,
|
|
SysMenu.status == StatusEnum.ENABLED.value,
|
|
SysMenu.auth_code.isnot(None),
|
|
)
|
|
.order_by(SysMenu.auth_code.asc())
|
|
).all()
|
|
return permissions
|
|
|
|
|
|
class UserSchema(BaseSchema):
|
|
def __init__(self, *args, **kwargs):
|
|
self.roles_type = kwargs.pop("roles_type", "code") # code | id
|
|
super().__init__(*args, **kwargs)
|
|
|
|
id = String()
|
|
username = String()
|
|
phone = String()
|
|
email = String()
|
|
password = String(load_only=True)
|
|
realname = String()
|
|
avatar = String()
|
|
gender = Enum(GenderEnum, by_value=True)
|
|
status = Enum(StatusEnum, by_value=True)
|
|
desc = String()
|
|
created_at = DateTime(data_key="createdAt", format="%Y-%m-%d %H:%M:%S")
|
|
updated_at = DateTime(data_key="updatedAt", format="%Y-%m-%d %H:%M:%S")
|
|
|
|
# 关系
|
|
roles = Nested("RoleSchema", many=True, dump_only=True, exclude=["users"])
|
|
depts = Nested(
|
|
"SysDeptSchema",
|
|
many=True,
|
|
dump_only=True,
|
|
exclude=["users", "children", "parent"],
|
|
)
|
|
permissions = List(String())
|
|
|
|
@post_dump
|
|
def patch_roles(self, data, **kwargs):
|
|
"""
|
|
角色code列表
|
|
"""
|
|
if "roles" in data:
|
|
if self.roles_type == "code":
|
|
role_codes = [role["code"] for role in data["roles"]]
|
|
else:
|
|
role_codes = [role["id"] for role in data["roles"]]
|
|
data["roles"] = role_codes
|
|
# 部门id列表
|
|
if "depts" in data:
|
|
dept_ids = [dept["id"] for dept in data["depts"]]
|
|
data["depts"] = dept_ids
|
|
return data
|