refactor: complete module-driven architecture migration
This commit completes the migration to a fully module-driven architecture: ## Models Migration - Moved all domain models from models/database/ to their respective modules: - tenancy: User, Admin, Vendor, Company, Platform, VendorDomain, etc. - cms: MediaFile, VendorTheme - messaging: Email, VendorEmailSettings, VendorEmailTemplate - core: AdminMenuConfig - models/database/ now only contains Base and TimestampMixin (infrastructure) ## Schemas Migration - Moved all domain schemas from models/schema/ to their respective modules: - tenancy: company, vendor, admin, team, vendor_domain - cms: media, image, vendor_theme - messaging: email - models/schema/ now only contains base.py and auth.py (infrastructure) ## Routes Migration - Moved admin routes from app/api/v1/admin/ to modules: - menu_config.py -> core module - modules.py -> tenancy module - module_config.py -> tenancy module - app/api/v1/admin/ now only aggregates auto-discovered module routes ## Menu System - Implemented module-driven menu system with MenuDiscoveryService - Extended FrontendType enum: PLATFORM, ADMIN, VENDOR, STOREFRONT - Added MenuItemDefinition and MenuSectionDefinition dataclasses - Each module now defines its own menu items in definition.py - MenuService integrates with MenuDiscoveryService for template rendering ## Documentation - Updated docs/architecture/models-structure.md - Updated docs/architecture/menu-management.md - Updated architecture validation rules for new exceptions ## Architecture Validation - Updated MOD-019 rule to allow base.py in models/schema/ - Created core module exceptions.py and schemas/ directory - All validation errors resolved (only warnings remain) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
202
app/modules/tenancy/models/user.py
Normal file
202
app/modules/tenancy/models/user.py
Normal file
@@ -0,0 +1,202 @@
|
||||
# app/modules/tenancy/models/user.py
|
||||
"""
|
||||
User model with authentication support.
|
||||
|
||||
ROLE CLARIFICATION:
|
||||
- User.role should ONLY contain platform-level roles:
|
||||
* "admin" - Platform administrator (full system access)
|
||||
* "vendor" - Any user who owns or is part of a vendor team
|
||||
|
||||
- Vendor-specific roles (manager, staff, etc.) are stored in VendorUser.role
|
||||
- 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."""
|
||||
|
||||
ADMIN = "admin" # Platform administrator
|
||||
VENDOR = "vendor" # Vendor owner or team member
|
||||
|
||||
|
||||
class User(Base, TimestampMixin):
|
||||
"""Represents a platform user (admins and vendors only)."""
|
||||
|
||||
__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)
|
||||
|
||||
# Platform-level role only (admin or vendor)
|
||||
role = Column(String, nullable=False, default=UserRole.VENDOR.value)
|
||||
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
is_email_verified = Column(Boolean, default=False, nullable=False)
|
||||
last_login = Column(DateTime, nullable=True)
|
||||
|
||||
# Super admin flag (only meaningful when role='admin')
|
||||
# Super admins have access to ALL platforms and global settings
|
||||
# Platform admins (is_super_admin=False) are assigned to specific platforms
|
||||
is_super_admin = Column(Boolean, default=False, nullable=False)
|
||||
|
||||
# Language preference (NULL = use context default: vendor dashboard_language or system default)
|
||||
# Supported: en, fr, de, lb
|
||||
preferred_language = Column(String(5), nullable=True)
|
||||
|
||||
# Relationships
|
||||
marketplace_import_jobs = relationship(
|
||||
"MarketplaceImportJob", back_populates="user"
|
||||
)
|
||||
owned_companies = relationship("Company", back_populates="owner")
|
||||
vendor_memberships = relationship(
|
||||
"VendorUser", foreign_keys="[VendorUser.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"<User(id={self.id}, username='{self.username}', email='{self.email}', role='{self.role}')>"
|
||||
|
||||
@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
|
||||
|
||||
@property
|
||||
def is_admin(self) -> bool:
|
||||
"""Check if user is a platform admin."""
|
||||
return self.role == UserRole.ADMIN.value
|
||||
|
||||
@property
|
||||
def is_vendor(self) -> bool:
|
||||
"""Check if user is a vendor (owner or team member)."""
|
||||
return self.role == UserRole.VENDOR.value
|
||||
|
||||
def is_owner_of(self, vendor_id: int) -> bool:
|
||||
"""
|
||||
Check if user is the owner of a specific vendor.
|
||||
|
||||
Ownership is determined via company ownership:
|
||||
User owns Company -> Company has Vendor -> User owns Vendor
|
||||
"""
|
||||
for company in self.owned_companies:
|
||||
if any(v.id == vendor_id for v in company.vendors):
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_member_of(self, vendor_id: int) -> bool:
|
||||
"""Check if user is a member of a specific vendor (owner or team)."""
|
||||
# Check if owner (via company)
|
||||
if self.is_owner_of(vendor_id):
|
||||
return True
|
||||
# Check if team member
|
||||
return any(
|
||||
vm.vendor_id == vendor_id and vm.is_active for vm in self.vendor_memberships
|
||||
)
|
||||
|
||||
def get_vendor_role(self, vendor_id: int) -> str:
|
||||
"""Get user's role within a specific vendor."""
|
||||
# Check if owner (via company)
|
||||
if self.is_owner_of(vendor_id):
|
||||
return "owner"
|
||||
|
||||
# Check team membership
|
||||
for vm in self.vendor_memberships:
|
||||
if vm.vendor_id == vendor_id and vm.is_active:
|
||||
return vm.role.name if vm.role else "member"
|
||||
|
||||
return None
|
||||
|
||||
def has_vendor_permission(self, vendor_id: int, permission: str) -> bool:
|
||||
"""Check if user has a specific permission in a vendor."""
|
||||
# Owners have all permissions
|
||||
if self.is_owner_of(vendor_id):
|
||||
return True
|
||||
|
||||
# Check team member permissions
|
||||
for vm in self.vendor_memberships:
|
||||
if vm.vendor_id == vendor_id and vm.is_active:
|
||||
if vm.role and permission in vm.role.permissions:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
# =========================================================================
|
||||
# Admin Platform Access Methods
|
||||
# =========================================================================
|
||||
|
||||
@property
|
||||
def is_super_admin_user(self) -> bool:
|
||||
"""Check if user is a super admin (can access all platforms)."""
|
||||
return self.role == UserRole.ADMIN.value and self.is_super_admin
|
||||
|
||||
@property
|
||||
def is_platform_admin(self) -> bool:
|
||||
"""Check if user is a platform admin (access to assigned platforms only)."""
|
||||
return self.role == UserRole.ADMIN.value and not self.is_super_admin
|
||||
|
||||
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"]
|
||||
Reference in New Issue
Block a user