from __future__ import annotations from datetime import datetime from typing import Any from passlib.context import CryptContext from sqlalchemy import ( JSON, BigInteger, Boolean, Column, DateTime, Float, ForeignKey, Integer, String, Table, Text, UniqueConstraint, ) from sqlalchemy.orm import Mapped, mapped_column, relationship from iti.db import AuditMixin, Base, IdMixin, TimestampMixin from iti_system.enums import GenderEnum, LogType, MenuTypeEnum, StatusEnum pwd_context = CryptContext(schemes=["argon2"], deprecated="auto") sys_user_role = Table( "sys_user_role", Base.metadata, Column("user_id", String(36), ForeignKey("sys_user.id", ondelete="CASCADE"), primary_key=True), Column("role_id", String(36), ForeignKey("sys_role.id", ondelete="CASCADE"), primary_key=True), ) sys_role_menu = Table( "sys_role_menu", Base.metadata, Column("role_id", String(36), ForeignKey("sys_role.id", ondelete="CASCADE"), primary_key=True), Column("menu_id", String(36), ForeignKey("sys_menu.id", ondelete="CASCADE"), primary_key=True), ) sys_user_dept = Table( "sys_user_dept", Base.metadata, Column("user_id", String(36), ForeignKey("sys_user.id", ondelete="CASCADE"), primary_key=True), Column("dept_id", String(36), ForeignKey("sys_dept.id", ondelete="CASCADE"), primary_key=True), ) class User(IdMixin, TimestampMixin, AuditMixin, Base): __tablename__ = "sys_user" username: Mapped[str] = mapped_column(String(64), unique=True, index=True) phone: Mapped[str | None] = mapped_column(String(32), unique=True, nullable=True) email: Mapped[str | None] = mapped_column(String(255), unique=True, nullable=True) password_hash: Mapped[str] = mapped_column(String(255)) realname: Mapped[str | None] = mapped_column(String(64), nullable=True) desc: Mapped[str | None] = mapped_column(Text, nullable=True) avatar: Mapped[str | None] = mapped_column(String(512), nullable=True) gender: Mapped[str] = mapped_column(String(16), default=GenderEnum.SECURE.value) status: Mapped[str] = mapped_column(String(16), default=StatusEnum.ENABLED.value) roles: Mapped[list["Role"]] = relationship( secondary=sys_user_role, back_populates="users", lazy="selectin", ) depts: Mapped[list["SysDept"]] = relationship( secondary=sys_user_dept, back_populates="users", lazy="selectin", ) attributes: Mapped[list["SysUserAttribute"]] = relationship( back_populates="user", lazy="selectin", cascade="all, delete-orphan", ) def set_password(self, value: str) -> None: self.password_hash = pwd_context.hash(value) def check_password(self, value: str) -> bool: return pwd_context.verify(value, self.password_hash) @property def permissions(self) -> list[str]: codes: set[str] = set() for role in self.roles: for menu in role.menus: if menu.status == StatusEnum.ENABLED.value and menu.auth_code: codes.add(menu.auth_code) return sorted(codes) @property def role_codes(self) -> list[str]: return [role.code for role in self.roles] @property def dept_ids(self) -> list[str]: return [dept.id for dept in self.depts] @property def attribute_map(self) -> dict[str, dict[str, Any]]: result: dict[str, dict[str, Any]] = {} for item in self.attributes: result.setdefault(item.attr_group, {})[item.attr_key] = item.typed_value return result class Role(IdMixin, TimestampMixin, AuditMixin, Base): __tablename__ = "sys_role" name: Mapped[str] = mapped_column(String(64)) code: Mapped[str] = mapped_column(String(64), unique=True, index=True) desc: Mapped[str | None] = mapped_column(Text, nullable=True) sort: Mapped[int] = mapped_column(Integer, default=0) status: Mapped[str] = mapped_column(String(16), default=StatusEnum.ENABLED.value) users: Mapped[list[User]] = relationship( secondary=sys_user_role, back_populates="roles", lazy="selectin", ) menus: Mapped[list["SysMenu"]] = relationship( secondary=sys_role_menu, back_populates="roles", lazy="selectin", ) @property def permissions(self) -> list[str]: return [menu.id for menu in self.menus] class SysMenu(IdMixin, TimestampMixin, AuditMixin, Base): __tablename__ = "sys_menu" name: Mapped[str] = mapped_column(String(255), unique=True) type: Mapped[str] = mapped_column(String(32), default=MenuTypeEnum.MENU.value) path: Mapped[str | None] = mapped_column(String(255), nullable=True) component: Mapped[str | None] = mapped_column(String(255), nullable=True) redirect: Mapped[str | None] = mapped_column(String(255), nullable=True) sort: Mapped[int] = mapped_column(Integer, default=0) auth_code: Mapped[str | None] = mapped_column(String(128), index=True, nullable=True) meta: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) status: Mapped[str] = mapped_column(String(16), default=StatusEnum.ENABLED.value) parent_id: Mapped[str | None] = mapped_column( String(36), ForeignKey("sys_menu.id", ondelete="SET NULL"), nullable=True, ) parent: Mapped["SysMenu | None"] = relationship(remote_side="SysMenu.id", back_populates="children") children: Mapped[list["SysMenu"]] = relationship(back_populates="parent", lazy="selectin") roles: Mapped[list[Role]] = relationship( secondary=sys_role_menu, back_populates="menus", lazy="selectin", ) class SysDept(IdMixin, TimestampMixin, AuditMixin, Base): __tablename__ = "sys_dept" name: Mapped[str] = mapped_column(String(255)) parent_id: Mapped[str | None] = mapped_column( String(36), ForeignKey("sys_dept.id", ondelete="SET NULL"), nullable=True, ) desc: Mapped[str | None] = mapped_column(Text, nullable=True) sort: Mapped[int] = mapped_column(Integer, default=0) leader_id: Mapped[str | None] = mapped_column(String(36), nullable=True) status: Mapped[str] = mapped_column(String(16), default=StatusEnum.ENABLED.value) parent: Mapped["SysDept | None"] = relationship(remote_side="SysDept.id", back_populates="children") children: Mapped[list["SysDept"]] = relationship(back_populates="parent", lazy="selectin") users: Mapped[list[User]] = relationship( secondary=sys_user_dept, back_populates="depts", lazy="selectin", ) class SysConfig(IdMixin, TimestampMixin, AuditMixin, Base): __tablename__ = "sys_config" __table_args__ = (UniqueConstraint("type", "code", name="uk_sys_config_type_code"),) type: Mapped[str] = mapped_column(String(64), default="SYSTEM") name: Mapped[str] = mapped_column(String(255)) code: Mapped[str] = mapped_column(String(128), index=True) value: Mapped[str | None] = mapped_column(Text, nullable=True) desc: Mapped[str | None] = mapped_column(Text, nullable=True) sort: Mapped[int] = mapped_column(Integer, default=0) status: Mapped[str] = mapped_column(String(16), default=StatusEnum.ENABLED.value) class SysDictType(IdMixin, TimestampMixin, AuditMixin, Base): __tablename__ = "sys_dict_type" type_name: Mapped[str] = mapped_column(String(255)) type_code: Mapped[str] = mapped_column(String(128), unique=True, index=True) desc: Mapped[str | None] = mapped_column(Text, nullable=True) sort: Mapped[int] = mapped_column(Integer, default=0) status: Mapped[str] = mapped_column(String(16), default=StatusEnum.ENABLED.value) data_list: Mapped[list["SysDictData"]] = relationship( back_populates="type", lazy="selectin", cascade="all, delete-orphan", primaryjoin="SysDictType.type_code == foreign(SysDictData.type_code)", ) class SysDictData(IdMixin, TimestampMixin, AuditMixin, Base): __tablename__ = "sys_dict_data" __table_args__ = (UniqueConstraint("type_code", "code", name="uk_sys_dict_data_type_code_code"),) type_code: Mapped[str] = mapped_column(String(128), index=True) label: Mapped[str] = mapped_column(String(255)) code: Mapped[str] = mapped_column(String(128)) value: Mapped[str | None] = mapped_column(Text, nullable=True) desc: Mapped[str | None] = mapped_column(Text, nullable=True) sort: Mapped[int] = mapped_column(Integer, default=0) status: Mapped[str] = mapped_column(String(16), default=StatusEnum.ENABLED.value) type: Mapped[SysDictType | None] = relationship( back_populates="data_list", primaryjoin="foreign(SysDictData.type_code) == SysDictType.type_code", ) class SysFile(IdMixin, TimestampMixin, AuditMixin, Base): __tablename__ = "sys_file" filename: Mapped[str] = mapped_column(String(255)) file_key: Mapped[str] = mapped_column(String(512), unique=True, index=True) file_hash: Mapped[str | None] = mapped_column(String(128), index=True, nullable=True) mime_type: Mapped[str | None] = mapped_column(String(128), nullable=True) file_size: Mapped[int] = mapped_column(BigInteger, default=0) extension: Mapped[str | None] = mapped_column(String(32), nullable=True) storage_type: Mapped[str] = mapped_column(String(32), default="local") storage_info: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) directory_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True) metadata_: Mapped[dict[str, Any] | None] = mapped_column("metadata", JSON, nullable=True) is_deleted: Mapped[bool] = mapped_column(Boolean, default=False) deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) deleted_by: Mapped[str | None] = mapped_column(String(36), nullable=True) share_code: Mapped[str | None] = mapped_column(String(64), unique=True, nullable=True) share_password: Mapped[str | None] = mapped_column(String(64), nullable=True) share_expire_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) share_count: Mapped[int] = mapped_column(Integer, default=0) status: Mapped[str] = mapped_column(String(16), default=StatusEnum.ENABLED.value) class SysLog(IdMixin, TimestampMixin, Base): __tablename__ = "sys_log" name: Mapped[str | None] = mapped_column(String(100), nullable=True) method: Mapped[str | None] = mapped_column(String(10), nullable=True) user_id: Mapped[str | None] = mapped_column(String(36), index=True, nullable=True) path: Mapped[str | None] = mapped_column(String(255), nullable=True) ip: Mapped[str | None] = mapped_column(String(255), nullable=True) user_agent: Mapped[str | None] = mapped_column(Text, nullable=True) headers: Mapped[str | None] = mapped_column(Text, nullable=True) query_params: Mapped[str | None] = mapped_column(Text, nullable=True) body_params: Mapped[str | None] = mapped_column(Text, nullable=True) execution_time: Mapped[float | None] = mapped_column(Float, nullable=True) response: Mapped[str | None] = mapped_column(Text, nullable=True) exception: Mapped[str | None] = mapped_column(Text, nullable=True) success: Mapped[bool | None] = mapped_column(Boolean, nullable=True) desc: Mapped[str | None] = mapped_column(Text, nullable=True) type: Mapped[str] = mapped_column(String(32), default=LogType.OPERATION.value) class SysUserAttribute(IdMixin, TimestampMixin, AuditMixin, Base): __tablename__ = "sys_user_attribute" __table_args__ = ( UniqueConstraint("user_id", "attr_group", "attr_key", name="uk_user_group_key"), ) user_id: Mapped[str] = mapped_column(String(36), ForeignKey("sys_user.id", ondelete="CASCADE"), index=True) attr_group: Mapped[str] = mapped_column(String(64), index=True) attr_key: Mapped[str] = mapped_column(String(128)) attr_value: Mapped[str | None] = mapped_column(Text, nullable=True) attr_type: Mapped[str] = mapped_column(String(32), default="string") description: Mapped[str | None] = mapped_column(String(255), nullable=True) sort: Mapped[int] = mapped_column(Integer, default=0) user: Mapped[User] = relationship(back_populates="attributes") @property def typed_value(self) -> Any: if self.attr_value is None: return None if self.attr_type == "int": return int(self.attr_value) if self.attr_type == "float": return float(self.attr_value) if self.attr_type == "bool": return self.attr_value.lower() in {"true", "1", "yes", "on"} if self.attr_type == "json": import json return json.loads(self.attr_value) if self.attr_type == "encrypted": return "******" return self.attr_value def set_typed_value(self, value: Any) -> None: if value is None: self.attr_value = None elif self.attr_type == "json": import json self.attr_value = json.dumps(value, ensure_ascii=False) elif self.attr_type == "bool": self.attr_value = "true" if value else "false" else: self.attr_value = str(value) __all__ = [ "Role", "SysConfig", "SysDept", "SysDictData", "SysDictType", "SysFile", "SysLog", "SysMenu", "SysUserAttribute", "User", "sys_role_menu", "sys_user_dept", "sys_user_role", ]