Files
orion/models/database/user.py
Samir Boulahtit 53e05dd497 feat: implement super admin and platform admin roles
Add multi-platform admin authorization system with:
- AdminPlatform junction table for admin-platform assignments
- is_super_admin flag on User model for global admin access
- Platform selection flow for platform admins after login
- JWT token updates to include platform context
- New API endpoints for admin user management (super admin only)
- Auth dependencies for super admin and platform access checks

Includes comprehensive test coverage:
- Unit tests for AdminPlatform model and User admin methods
- Unit tests for AdminPlatformService operations
- Integration tests for admin users API endpoints

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 18:44:49 +01:00

191 lines
6.7 KiB
Python

# models/database/user.py - IMPROVED VERSION
"""
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",
)
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]