# app/modules/tenancy/models/user.py """ User model with authentication support. ROLE SYSTEM (Phase 1 — Consolidated 4-value enum): - User.role contains one of 4 values: * "super_admin" — Platform super administrator (all platforms, all settings) * "platform_admin" — Platform admin scoped to assigned platforms * "merchant_owner" — Owns merchant(s) and all their stores * "store_member" — Team member on specific store(s) - Store-specific granular permissions are in StoreUser.role_id -> Role.permissions - Customers are NOT in the User table — they use the Customer model """ import enum from sqlalchemy import Boolean, Column, DateTime, Integer, String from sqlalchemy.orm import relationship from app.core.database import Base from models.database.base import TimestampMixin class UserRole(str, enum.Enum): """Platform-level user roles.""" SUPER_ADMIN = "super_admin" # Platform super administrator PLATFORM_ADMIN = "platform_admin" # Platform admin (scoped to assigned platforms) MERCHANT_OWNER = "merchant_owner" # Owns merchant(s) and all their stores STORE_MEMBER = "store_member" # Team member on specific store(s) class User(Base, TimestampMixin): """Represents a platform user (admins, merchant owners, and store team).""" __tablename__ = "users" id = Column(Integer, primary_key=True, index=True) email = Column(String, unique=True, index=True, nullable=False) username = Column(String, unique=True, index=True, nullable=False) first_name = Column(String) last_name = Column(String) hashed_password = Column(String, nullable=False) # Consolidated role — one of: super_admin, platform_admin, merchant_owner, store_member role = Column(String, nullable=False, default=UserRole.STORE_MEMBER.value) is_active = Column(Boolean, default=True, nullable=False) is_email_verified = Column(Boolean, default=False, nullable=False) last_login = Column(DateTime, nullable=True) # Language preference (NULL = use context default: store dashboard_language or system default) # Supported: en, fr, de, lb preferred_language = Column(String(5), nullable=True) # Relationships # NOTE: marketplace_import_jobs relationship removed - owned by marketplace module # Use: MarketplaceImportJob.query.filter_by(user_id=user.id) instead owned_merchants = relationship("Merchant", back_populates="owner") store_memberships = relationship( "StoreUser", foreign_keys="[StoreUser.user_id]", back_populates="user" ) # Admin-platform assignments (for platform admins only) # Super admins don't need assignments - they have access to all platforms admin_platforms = relationship( "AdminPlatform", foreign_keys="AdminPlatform.user_id", back_populates="user", cascade="all, delete-orphan", ) # Menu visibility configuration (for super admins only) # Platform admins get menu config from their platform, not user-level menu_configs = relationship( "AdminMenuConfig", foreign_keys="AdminMenuConfig.user_id", back_populates="user", cascade="all, delete-orphan", ) def __repr__(self): """String representation of the User object.""" return f"" @property def full_name(self): """Returns the full name of the user, combining first and last names if available.""" if self.first_name and self.last_name: return f"{self.first_name} {self.last_name}" return self.username # ========================================================================= # Role-checking properties # ========================================================================= @property def is_super_admin(self) -> bool: """Check if user is a super admin.""" return self.role == UserRole.SUPER_ADMIN.value @property def is_admin(self) -> bool: """Check if user is any kind of platform admin (super or scoped).""" return self.role in (UserRole.SUPER_ADMIN.value, UserRole.PLATFORM_ADMIN.value) @property def is_platform_admin(self) -> bool: """Check if user is a scoped platform admin (not super admin).""" return self.role == UserRole.PLATFORM_ADMIN.value @property def is_merchant_owner(self) -> bool: """Check if user is a merchant owner.""" return self.role == UserRole.MERCHANT_OWNER.value @property def is_store_user(self) -> bool: """Check if user is a merchant owner or store member.""" return self.role in (UserRole.MERCHANT_OWNER.value, UserRole.STORE_MEMBER.value) def is_owner_of(self, store_id: int) -> bool: """ Check if user is the owner of a specific store. Ownership is determined via merchant ownership: User owns Merchant -> Merchant has Store -> User owns Store """ for merchant in self.owned_merchants: if any(v.id == store_id for v in merchant.stores): return True return False def is_member_of(self, store_id: int) -> bool: """Check if user is a member of a specific store (owner or team).""" # Check if owner (via merchant) if self.is_owner_of(store_id): return True # Check if team member return any( vm.store_id == store_id and vm.is_active for vm in self.store_memberships ) def get_store_role(self, store_id: int) -> str: """Get user's role within a specific store.""" # Check if owner (via merchant) if self.is_owner_of(store_id): return "owner" # Check team membership for vm in self.store_memberships: if vm.store_id == store_id and vm.is_active: return vm.role.name if vm.role else "member" return None def has_store_permission(self, store_id: int, permission: str) -> bool: """Check if user has a specific permission in a store.""" # Owners have all permissions if self.is_owner_of(store_id): return True # Check team member permissions for vm in self.store_memberships: if vm.store_id == store_id and vm.is_active: if vm.role and permission in vm.role.permissions: return True return False # ========================================================================= # Admin Platform Access Methods # ========================================================================= def can_access_platform(self, platform_id: int) -> bool: """ Check if admin can access a specific platform. - Super admins can access all platforms - Platform admins can only access assigned platforms - Non-admins return False """ if not self.is_admin: return False if self.is_super_admin: return True return any( ap.platform_id == platform_id and ap.is_active for ap in self.admin_platforms ) def get_accessible_platform_ids(self) -> list[int] | None: """ Get list of platform IDs this admin can access. Returns: - None for super admins (means ALL platforms) - List of platform IDs for platform admins - Empty list for non-admins """ if not self.is_admin: return [] if self.is_super_admin: return None # None means ALL platforms return [ap.platform_id for ap in self.admin_platforms if ap.is_active] __all__ = ["User", "UserRole"]