Vendor team member management features

This commit is contained in:
2025-11-14 21:08:57 +01:00
parent af23f5b88f
commit 41439eed09
10 changed files with 1786 additions and 85 deletions

View File

@@ -1,39 +1,56 @@
# models/database/user.py
# models/database/user.py - IMPROVED VERSION
"""
User model with authentication support.
This module defines the User model which includes fields for user details,
authentication information, and relationships to other models such as Vendor and Customer.
"""
from sqlalchemy import Boolean, Column, DateTime, Integer, String
from sqlalchemy.orm import relationship
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
"""
from sqlalchemy import Boolean, Column, DateTime, Integer, String, Enum
from sqlalchemy.orm import relationship
import enum
# Import Base from the central database module instead of creating a new one
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 user in the system."""
"""Represents a platform user (admins and vendors only)."""
__tablename__ = "users" # Name of the table in the database
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True) # Primary key and indexed column for user ID
email = Column(String, unique=True, index=True, nullable=False) # Unique, indexed, non-nullable email column
username = Column(String, unique=True, index=True, nullable=False) # Unique, indexed, non-nullable username column
first_name = Column(String) # Optional first name column
last_name = Column(String) # Optional last name column
hashed_password = Column(String, nullable=False) # Non-nullable hashed password column
role = Column(String, nullable=False, default="user") # Role of the user (default is 'user') //TODO: Change to customer, vendor, admin
is_active = Column(Boolean, default=True, nullable=False) # Active status of the user (default is True)
last_login = Column(DateTime, nullable=True) # Optional last login timestamp column
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)
# Relationships
marketplace_import_jobs = relationship("MarketplaceImportJob",
back_populates="user") # Relationship with import jobs
owned_vendors = relationship("Vendor", back_populates="owner") # Relationship with vendors owned by this user
vendor_memberships = relationship("VendorUser", foreign_keys="[VendorUser.user_id]",
back_populates="user") # Relationship with vendor memberships
marketplace_import_jobs = relationship("MarketplaceImportJob", back_populates="user")
owned_vendors = relationship("Vendor", back_populates="owner")
vendor_memberships = relationship(
"VendorUser",
foreign_keys="[VendorUser.user_id]",
back_populates="user"
)
def __repr__(self):
"""String representation of the User object."""
@@ -45,3 +62,55 @@ class User(Base, TimestampMixin):
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."""
return any(v.id == vendor_id for v in self.owned_vendors)
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
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
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

View File

@@ -5,14 +5,14 @@ Vendor model representing entities that sell products or services.
This module defines the Vendor model along with its relationships to
other models such as User (owner), Product, Customer, Order, and MarketplaceImportJob.
"""
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, Text, JSON
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, Text, JSON, DateTime
from sqlalchemy.orm import relationship
# Import Base from the central database module instead of creating a new one
from app.core.database import Base
from models.database.base import TimestampMixin
from app.core.config import settings
import enum
class Vendor(Base, TimestampMixin):
"""Represents a vendor in the system."""
@@ -177,10 +177,21 @@ class Vendor(Base, TimestampMixin):
return domains
class VendorUser(Base, TimestampMixin):
"""Represents a user's role within a specific vendor."""
class VendorUserType(str, enum.Enum):
"""Types of vendor users."""
OWNER = "owner" # Vendor owner (full access to vendor area)
TEAM_MEMBER = "member" # Team member (role-based access to vendor area)
__tablename__ = "vendor_users" # Name of the table in the database
class VendorUser(Base, TimestampMixin):
"""
Represents a user's membership in a vendor.
- Owner: Created automatically when vendor is created
- Team Member: Invited by owner via email
"""
__tablename__ = "vendor_users"
id = Column(Integer, primary_key=True, index=True)
"""Unique identifier for each VendorUser entry."""
@@ -191,15 +202,23 @@ class VendorUser(Base, TimestampMixin):
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
"""Foreign key linking to the associated User."""
role_id = Column(Integer, ForeignKey("roles.id"), nullable=False)
# Distinguish between owner and team member
user_type = Column(String, nullable=False, default=VendorUserType.TEAM_MEMBER.value)
# Role for team members (NULL for owners - they have all permissions)
role_id = Column(Integer, ForeignKey("roles.id"), nullable=True)
"""Foreign key linking to the associated Role."""
invited_by = Column(Integer, ForeignKey("users.id"))
"""Foreign key linking to the user who invited this VendorUser."""
invitation_token = Column(String, nullable=True, index=True) # For email activation
invitation_sent_at = Column(DateTime, nullable=True)
invitation_accepted_at = Column(DateTime, nullable=True)
is_active = Column(Boolean, default=True, nullable=False)
is_active = Column(Boolean, default=False, nullable=False) # False until invitation accepted
"""Indicates whether the VendorUser role is active."""
# Relationships
vendor = relationship("Vendor", back_populates="vendor_users")
"""Relationship to the Vendor model, representing the associated vendor."""
@@ -216,10 +235,57 @@ class VendorUser(Base, TimestampMixin):
"""Return a string representation of the VendorUser instance.
Returns:
str: A string that includes the vendor_id and user_id of the VendorUser instance.
str: A string that includes the vendor_id, the user_id and the user_type of the VendorUser instance.
"""
return f"<VendorUser(vendor_id={self.vendor_id}, user_id={self.user_id})>"
return f"<VendorUser(vendor_id={self.vendor_id}, user_id={self.user_id}, type={self.user_type})>"
@property
def is_owner(self) -> bool:
"""Check if this is an owner membership."""
return self.user_type == VendorUserType.OWNER.value
@property
def is_team_member(self) -> bool:
"""Check if this is a team member (not owner)."""
return self.user_type == VendorUserType.TEAM_MEMBER.value
@property
def is_invitation_pending(self) -> bool:
"""Check if invitation is still pending."""
return self.invitation_token is not None and self.invitation_accepted_at is None
def has_permission(self, permission: str) -> bool:
"""
Check if user has a specific permission.
Owners always have all permissions.
Team members check their role's permissions.
"""
# Owners have all permissions
if self.is_owner:
return True
# Inactive users have no permissions
if not self.is_active:
return False
# Check role permissions
if self.role and self.role.permissions:
return permission in self.role.permissions
return False
def get_all_permissions(self) -> list:
"""Get all permissions this user has."""
if self.is_owner:
# Return all possible permissions
from app.core.permissions import VendorPermissions
return list(VendorPermissions.__members__.values())
if self.role and self.role.permissions:
return self.role.permissions
return []
class Role(Base, TimestampMixin):
"""Represents a role within a vendor's system."""