refactor(arch): move auth schemas to tenancy module and add cross-module service methods
Some checks failed
Some checks failed
Move all auth schemas (UserContext, UserLogin, LoginResponse, etc.) from legacy models/schema/auth.py to app/modules/tenancy/schemas/auth.py per MOD-019. Update 84 import sites across 14 modules. Legacy file now re-exports for backwards compatibility. Add missing tenancy service methods for cross-module consumers: - merchant_service.get_merchant_by_owner_id() - merchant_service.get_merchant_count_for_owner() - admin_service.get_user_by_id() (public, was private-only) - platform_service.get_active_store_count() Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -21,9 +21,7 @@ from app.modules.core.services.auth_service import auth_service
|
||||
from app.modules.tenancy.exceptions import (
|
||||
InvalidCredentialsException,
|
||||
)
|
||||
from app.modules.tenancy.services.admin_platform_service import admin_platform_service
|
||||
from middleware.auth import AuthManager
|
||||
from models.schema.auth import (
|
||||
from app.modules.tenancy.schemas.auth import (
|
||||
LoginResponse,
|
||||
LogoutResponse,
|
||||
PlatformSelectResponse,
|
||||
@@ -31,6 +29,8 @@ from models.schema.auth import (
|
||||
UserLogin,
|
||||
UserResponse,
|
||||
)
|
||||
from app.modules.tenancy.services.admin_platform_service import admin_platform_service
|
||||
from middleware.auth import AuthManager
|
||||
|
||||
admin_auth_router = APIRouter(prefix="/auth")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -16,6 +16,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.core.database import get_db
|
||||
from app.modules.tenancy.schemas.auth import UserContext
|
||||
from app.modules.tenancy.schemas.merchant_domain import (
|
||||
MerchantDomainCreate,
|
||||
MerchantDomainDeletionResponse,
|
||||
@@ -30,7 +31,6 @@ from app.modules.tenancy.schemas.store_domain import (
|
||||
from app.modules.tenancy.services.merchant_domain_service import (
|
||||
merchant_domain_service,
|
||||
)
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
admin_merchant_domains_router = APIRouter(prefix="/merchants")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -16,6 +16,7 @@ from app.modules.tenancy.exceptions import (
|
||||
ConfirmationRequiredException,
|
||||
MerchantHasStoresException,
|
||||
)
|
||||
from app.modules.tenancy.schemas.auth import UserContext
|
||||
from app.modules.tenancy.schemas.merchant import (
|
||||
MerchantCreate,
|
||||
MerchantCreateResponse,
|
||||
@@ -27,7 +28,6 @@ from app.modules.tenancy.schemas.merchant import (
|
||||
MerchantUpdate,
|
||||
)
|
||||
from app.modules.tenancy.services.merchant_service import merchant_service
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
admin_merchants_router = APIRouter(prefix="/merchants")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -21,8 +21,8 @@ from app.api.deps import get_current_super_admin, get_db
|
||||
from app.exceptions import ValidationException
|
||||
from app.modules.registry import MODULES
|
||||
from app.modules.service import module_service
|
||||
from app.modules.tenancy.schemas.auth import UserContext
|
||||
from app.modules.tenancy.services.platform_service import platform_service
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/module-config")
|
||||
|
||||
@@ -21,8 +21,8 @@ from sqlalchemy.orm import Session
|
||||
from app.api.deps import get_current_super_admin, get_db
|
||||
from app.modules.registry import MODULES, get_core_module_codes
|
||||
from app.modules.service import module_service
|
||||
from app.modules.tenancy.schemas.auth import UserContext
|
||||
from app.modules.tenancy.services.platform_service import platform_service
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/modules")
|
||||
|
||||
@@ -15,8 +15,7 @@ from sqlalchemy.orm import Session
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.core.database import get_db
|
||||
from app.modules.core.services.stats_aggregator import stats_aggregator
|
||||
from app.modules.tenancy.services.admin_service import admin_service
|
||||
from models.schema.auth import (
|
||||
from app.modules.tenancy.schemas.auth import (
|
||||
OwnedMerchantSummary,
|
||||
StoreMembershipSummary,
|
||||
UserContext,
|
||||
@@ -29,6 +28,7 @@ from models.schema.auth import (
|
||||
UserStatusToggleResponse,
|
||||
UserUpdate,
|
||||
)
|
||||
from app.modules.tenancy.services.admin_service import admin_service
|
||||
|
||||
admin_platform_users_router = APIRouter(prefix="/users")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -22,8 +22,8 @@ from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_from_cookie_or_header, get_db
|
||||
from app.modules.tenancy.schemas.auth import UserContext
|
||||
from app.modules.tenancy.services.platform_service import platform_service
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
admin_platforms_router = APIRouter(prefix="/platforms")
|
||||
|
||||
@@ -16,6 +16,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.core.database import get_db
|
||||
from app.modules.tenancy.schemas.auth import UserContext
|
||||
from app.modules.tenancy.schemas.store_domain import (
|
||||
DomainDeletionResponse,
|
||||
DomainVerificationInstructions,
|
||||
@@ -27,7 +28,6 @@ from app.modules.tenancy.schemas.store_domain import (
|
||||
)
|
||||
from app.modules.tenancy.services.store_domain_service import store_domain_service
|
||||
from app.modules.tenancy.services.store_service import store_service
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
admin_store_domains_router = APIRouter(prefix="/stores")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -21,6 +21,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.core.database import get_db
|
||||
from app.modules.tenancy.schemas.auth import UserContext
|
||||
from app.modules.tenancy.schemas.team import (
|
||||
PermissionCatalogResponse,
|
||||
RoleCreate,
|
||||
@@ -33,7 +34,6 @@ from app.modules.tenancy.services.permission_discovery_service import (
|
||||
)
|
||||
from app.modules.tenancy.services.store_team_service import store_team_service
|
||||
from app.utils.i18n import translate
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
admin_store_roles_router = APIRouter(prefix="/store-roles")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -16,6 +16,7 @@ from sqlalchemy.orm import Session
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.core.database import get_db
|
||||
from app.modules.tenancy.exceptions import ConfirmationRequiredException
|
||||
from app.modules.tenancy.schemas.auth import UserContext
|
||||
from app.modules.tenancy.schemas.store import (
|
||||
StoreCreate,
|
||||
StoreCreateResponse,
|
||||
@@ -26,7 +27,6 @@ from app.modules.tenancy.schemas.store import (
|
||||
)
|
||||
from app.modules.tenancy.services.admin_service import admin_service
|
||||
from app.modules.tenancy.services.store_service import store_service
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
admin_stores_router = APIRouter(prefix="/stores")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -24,8 +24,8 @@ from app.exceptions import ValidationException
|
||||
from app.modules.tenancy.models import (
|
||||
User, # API-007 - Internal helper uses User model
|
||||
)
|
||||
from app.modules.tenancy.schemas.auth import UserContext
|
||||
from app.modules.tenancy.services.admin_platform_service import admin_platform_service
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
admin_users_router = APIRouter(prefix="/admin-users")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -21,8 +21,8 @@ from app.modules.tenancy.schemas import (
|
||||
MerchantPortalProfileUpdate,
|
||||
MerchantPortalStoreListResponse,
|
||||
)
|
||||
from app.modules.tenancy.schemas.auth import UserContext
|
||||
from app.modules.tenancy.services.merchant_service import merchant_service
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
from .email_verification import email_verification_api_router
|
||||
from .merchant_auth import merchant_auth_router
|
||||
|
||||
@@ -22,14 +22,14 @@ from app.modules.core.services.auth_service import auth_service
|
||||
from app.modules.tenancy.models.user_password_reset_token import (
|
||||
UserPasswordResetToken, # noqa: API-007
|
||||
)
|
||||
from app.modules.tenancy.services.user_auth_service import user_auth_service
|
||||
from models.schema.auth import (
|
||||
from app.modules.tenancy.schemas.auth import (
|
||||
LoginResponse,
|
||||
LogoutResponse,
|
||||
UserContext,
|
||||
UserLogin,
|
||||
UserResponse,
|
||||
)
|
||||
from app.modules.tenancy.services.user_auth_service import user_auth_service
|
||||
|
||||
merchant_auth_router = APIRouter(prefix="/auth")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -26,10 +26,15 @@ from app.modules.tenancy.exceptions import InvalidCredentialsException
|
||||
from app.modules.tenancy.models.user_password_reset_token import (
|
||||
UserPasswordResetToken, # noqa: API-007
|
||||
)
|
||||
from app.modules.tenancy.schemas.auth import (
|
||||
LogoutResponse,
|
||||
StoreUserResponse,
|
||||
UserContext,
|
||||
UserLogin,
|
||||
)
|
||||
from app.modules.tenancy.services.user_auth_service import user_auth_service
|
||||
from middleware.platform_context import get_current_platform
|
||||
from middleware.store_context import get_current_store
|
||||
from models.schema.auth import LogoutResponse, StoreUserResponse, UserContext, UserLogin
|
||||
|
||||
store_auth_router = APIRouter(prefix="/auth")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -13,9 +13,9 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_store_api
|
||||
from app.core.database import get_db
|
||||
from app.modules.tenancy.schemas.auth import UserContext
|
||||
from app.modules.tenancy.schemas.store import StoreResponse, StoreUpdate
|
||||
from app.modules.tenancy.services.store_service import store_service
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
store_profile_router = APIRouter(prefix="/profile")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -22,6 +22,7 @@ from app.api.deps import (
|
||||
require_store_permission,
|
||||
)
|
||||
from app.core.database import get_db
|
||||
from app.modules.tenancy.schemas.auth import UserContext
|
||||
from app.modules.tenancy.schemas.team import (
|
||||
BulkRemoveRequest,
|
||||
BulkRemoveResponse,
|
||||
@@ -48,7 +49,6 @@ from app.modules.tenancy.services.permission_discovery_service import (
|
||||
)
|
||||
from app.modules.tenancy.services.store_team_service import store_team_service
|
||||
from app.utils.i18n import translate
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
store_team_router = APIRouter(prefix="/team")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -16,8 +16,8 @@ from sqlalchemy.orm import Session
|
||||
from app.api.deps import get_current_merchant_from_cookie_or_header, get_db
|
||||
from app.modules.core.utils.page_context import get_context_for_frontend
|
||||
from app.modules.enums import FrontendType
|
||||
from app.modules.tenancy.schemas.auth import UserContext
|
||||
from app.templates_config import templates
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ Tenancy module Pydantic schemas.
|
||||
Request/response schemas for platform, merchant, store, admin user, and team management.
|
||||
"""
|
||||
|
||||
# Merchant schemas
|
||||
# Auth schemas
|
||||
# Admin schemas
|
||||
from app.modules.tenancy.schemas.admin import (
|
||||
AdminAuditLogFilters,
|
||||
@@ -49,6 +49,27 @@ from app.modules.tenancy.schemas.admin import (
|
||||
RowsPerPageUpdateResponse,
|
||||
SystemHealthResponse,
|
||||
)
|
||||
from app.modules.tenancy.schemas.auth import (
|
||||
LoginResponse,
|
||||
LogoutResponse,
|
||||
OwnedMerchantSummary,
|
||||
PasswordResetRequestResponse,
|
||||
PasswordResetResponse,
|
||||
PlatformSelectResponse,
|
||||
StoreMembershipSummary,
|
||||
StoreUserResponse,
|
||||
UserContext,
|
||||
UserCreate,
|
||||
UserDeleteResponse,
|
||||
UserDetailResponse,
|
||||
UserListResponse,
|
||||
UserLogin,
|
||||
UserResponse,
|
||||
UserSearchItem,
|
||||
UserSearchResponse,
|
||||
UserStatusToggleResponse,
|
||||
UserUpdate,
|
||||
)
|
||||
from app.modules.tenancy.schemas.merchant import (
|
||||
MerchantBase,
|
||||
MerchantCreate,
|
||||
@@ -112,6 +133,26 @@ from app.modules.tenancy.schemas.team import (
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Auth
|
||||
"LoginResponse",
|
||||
"LogoutResponse",
|
||||
"OwnedMerchantSummary",
|
||||
"PasswordResetRequestResponse",
|
||||
"PasswordResetResponse",
|
||||
"PlatformSelectResponse",
|
||||
"StoreMembershipSummary",
|
||||
"StoreUserResponse",
|
||||
"UserContext",
|
||||
"UserCreate",
|
||||
"UserDeleteResponse",
|
||||
"UserDetailResponse",
|
||||
"UserListResponse",
|
||||
"UserLogin",
|
||||
"UserResponse",
|
||||
"UserSearchItem",
|
||||
"UserSearchResponse",
|
||||
"UserStatusToggleResponse",
|
||||
"UserUpdate",
|
||||
# Merchant
|
||||
"MerchantBase",
|
||||
"MerchantCreate",
|
||||
|
||||
343
app/modules/tenancy/schemas/auth.py
Normal file
343
app/modules/tenancy/schemas/auth.py
Normal file
@@ -0,0 +1,343 @@
|
||||
# app/modules/tenancy/schemas/auth.py
|
||||
"""
|
||||
Authentication and user context schemas.
|
||||
|
||||
UserContext is the primary schema for dependency injection in API endpoints,
|
||||
replacing direct use of the User database model in routes.
|
||||
|
||||
Migrated from models/schema/auth.py per MOD-019 / MOD-025.
|
||||
"""
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
|
||||
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
email_or_username: str = Field(..., description="Username or email address")
|
||||
password: str = Field(..., description="Password")
|
||||
store_code: str | None = Field(
|
||||
None, description="Optional store code for context"
|
||||
)
|
||||
|
||||
@field_validator("email_or_username")
|
||||
@classmethod
|
||||
def validate_email_or_username(cls, v):
|
||||
return v.strip()
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: int
|
||||
email: str
|
||||
username: str
|
||||
role: str
|
||||
is_active: bool
|
||||
preferred_language: str | None = None
|
||||
last_login: datetime | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class LoginResponse(BaseModel):
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
expires_in: int
|
||||
user: UserResponse
|
||||
|
||||
|
||||
class PlatformSelectResponse(BaseModel):
|
||||
"""Response for platform selection (no user data - client already has it)."""
|
||||
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
expires_in: int
|
||||
platform_id: int
|
||||
platform_code: str
|
||||
|
||||
|
||||
class OwnedMerchantSummary(BaseModel):
|
||||
"""Summary of a merchant owned by a user."""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
is_active: bool
|
||||
store_count: int
|
||||
|
||||
|
||||
class StoreMembershipSummary(BaseModel):
|
||||
"""Summary of a user's store membership."""
|
||||
|
||||
store_id: int
|
||||
store_code: str
|
||||
store_name: str
|
||||
role: str
|
||||
is_active: bool
|
||||
|
||||
|
||||
class UserDetailResponse(UserResponse):
|
||||
"""Extended user response with additional details."""
|
||||
|
||||
first_name: str | None = None
|
||||
last_name: str | None = None
|
||||
full_name: str | None = None
|
||||
is_email_verified: bool = False
|
||||
owned_merchants_count: int = 0
|
||||
store_memberships_count: int = 0
|
||||
owned_merchants: list[OwnedMerchantSummary] = []
|
||||
store_memberships: list[StoreMembershipSummary] = []
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
"""Schema for updating user information."""
|
||||
|
||||
username: str | None = Field(None, min_length=3, max_length=50)
|
||||
email: EmailStr | None = None
|
||||
first_name: str | None = Field(None, max_length=100)
|
||||
last_name: str | None = Field(None, max_length=100)
|
||||
role: str | None = Field(None, pattern="^(super_admin|platform_admin|merchant_owner|store_member)$")
|
||||
is_active: bool | None = None
|
||||
is_email_verified: bool | None = None
|
||||
preferred_language: str | None = Field(
|
||||
None, description="Preferred language (en, fr, de, lb)"
|
||||
)
|
||||
|
||||
@field_validator("username")
|
||||
@classmethod
|
||||
def validate_username(cls, v):
|
||||
if v and not re.match(r"^[a-zA-Z0-9_]+$", v):
|
||||
raise ValueError(
|
||||
"Username must contain only letters, numbers, or underscores"
|
||||
)
|
||||
return v.lower().strip() if v else v
|
||||
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
"""Schema for creating a new user (admin only)."""
|
||||
|
||||
email: EmailStr = Field(..., description="Valid email address")
|
||||
username: str = Field(..., min_length=3, max_length=50)
|
||||
password: str = Field(..., min_length=6, description="Password")
|
||||
first_name: str | None = Field(None, max_length=100)
|
||||
last_name: str | None = Field(None, max_length=100)
|
||||
role: str = Field(default="store_member", pattern="^(super_admin|platform_admin|merchant_owner|store_member)$")
|
||||
preferred_language: str | None = Field(
|
||||
None, description="Preferred language (en, fr, de, lb)"
|
||||
)
|
||||
|
||||
@field_validator("username")
|
||||
@classmethod
|
||||
def validate_username(cls, v):
|
||||
if not re.match(r"^[a-zA-Z0-9_]+$", v):
|
||||
raise ValueError(
|
||||
"Username must contain only letters, numbers, or underscores"
|
||||
)
|
||||
return v.lower().strip()
|
||||
|
||||
|
||||
class UserListResponse(BaseModel):
|
||||
"""Schema for paginated user list."""
|
||||
|
||||
items: list[UserResponse]
|
||||
total: int
|
||||
page: int
|
||||
per_page: int
|
||||
pages: int
|
||||
|
||||
|
||||
class UserSearchItem(BaseModel):
|
||||
"""Schema for a single user search result."""
|
||||
|
||||
id: int
|
||||
username: str
|
||||
email: str
|
||||
is_active: bool
|
||||
|
||||
|
||||
class UserSearchResponse(BaseModel):
|
||||
"""Schema for user search results."""
|
||||
|
||||
users: list[UserSearchItem]
|
||||
|
||||
|
||||
class UserStatusToggleResponse(BaseModel):
|
||||
"""Schema for user status toggle response."""
|
||||
|
||||
message: str
|
||||
is_active: bool
|
||||
|
||||
|
||||
class UserDeleteResponse(BaseModel):
|
||||
"""Schema for user delete response."""
|
||||
|
||||
message: str
|
||||
|
||||
|
||||
class LogoutResponse(BaseModel):
|
||||
"""Schema for logout response."""
|
||||
|
||||
message: str
|
||||
|
||||
|
||||
class PasswordResetRequestResponse(BaseModel):
|
||||
"""Schema for password reset request response."""
|
||||
|
||||
message: str
|
||||
|
||||
|
||||
class PasswordResetResponse(BaseModel):
|
||||
"""Schema for password reset response."""
|
||||
|
||||
message: str
|
||||
|
||||
|
||||
class StoreUserResponse(BaseModel):
|
||||
"""Schema for store user info in auth context."""
|
||||
|
||||
id: int
|
||||
username: str
|
||||
email: str
|
||||
role: str
|
||||
is_active: bool
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class UserContext(BaseModel):
|
||||
"""
|
||||
User context for dependency injection in API endpoints.
|
||||
|
||||
This schema replaces direct use of the User database model in API routes,
|
||||
following the principle that routes should not import database models directly.
|
||||
|
||||
Used by:
|
||||
- get_current_admin_api / get_current_admin_from_cookie_or_header
|
||||
- get_current_store_api / get_current_store_from_cookie_or_header
|
||||
- get_current_super_admin
|
||||
|
||||
For admin users:
|
||||
- is_super_admin indicates full platform access
|
||||
- accessible_platform_ids is None for super admins (all platforms)
|
||||
- accessible_platform_ids is a list for platform admins
|
||||
|
||||
For store users:
|
||||
- token_store_id/code/role come from JWT token
|
||||
- These indicate which store context the user is operating in
|
||||
"""
|
||||
|
||||
# Core user fields
|
||||
id: int
|
||||
email: str
|
||||
username: str
|
||||
role: str # super_admin, platform_admin, merchant_owner, or store_member
|
||||
is_active: bool = True
|
||||
|
||||
# Admin-specific fields
|
||||
accessible_platform_ids: list[int] | None = None # None = all platforms (super admin)
|
||||
|
||||
# Admin platform context (from JWT token after platform selection)
|
||||
token_platform_id: int | None = None
|
||||
token_platform_code: str | None = None
|
||||
|
||||
# Store-specific fields (from JWT token)
|
||||
token_store_id: int | None = None
|
||||
token_store_code: str | None = None
|
||||
token_store_role: str | None = None
|
||||
|
||||
# Optional profile fields
|
||||
first_name: str | None = None
|
||||
last_name: str | None = None
|
||||
preferred_language: str | None = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@property
|
||||
def full_name(self) -> str:
|
||||
"""Returns the full name of the user."""
|
||||
if self.first_name and self.last_name:
|
||||
return f"{self.first_name} {self.last_name}"
|
||||
return self.username
|
||||
|
||||
@property
|
||||
def is_super_admin(self) -> bool:
|
||||
"""Check if user is a super admin."""
|
||||
return self.role == "super_admin"
|
||||
|
||||
@property
|
||||
def is_admin(self) -> bool:
|
||||
"""Check if user is an admin (super_admin or platform_admin)."""
|
||||
return self.role in ("super_admin", "platform_admin")
|
||||
|
||||
@property
|
||||
def is_store_user(self) -> bool:
|
||||
"""Check if user is a store user (merchant_owner or store_member)."""
|
||||
return self.role in ("merchant_owner", "store_member")
|
||||
|
||||
def can_access_platform(self, platform_id: int) -> bool:
|
||||
"""
|
||||
Check if user can access a specific platform.
|
||||
|
||||
Super admins (accessible_platform_ids=None) can access all platforms.
|
||||
Platform admins can only access their assigned platforms.
|
||||
"""
|
||||
if self.is_super_admin:
|
||||
return True
|
||||
if self.accessible_platform_ids is None:
|
||||
return True # Super admin fallback
|
||||
return platform_id in self.accessible_platform_ids
|
||||
|
||||
def get_accessible_platform_ids(self) -> list[int] | None:
|
||||
"""
|
||||
Get list of platform IDs this user can access.
|
||||
|
||||
Returns None for super admins (all platforms accessible).
|
||||
Returns list of platform IDs for platform admins.
|
||||
"""
|
||||
return self.accessible_platform_ids
|
||||
|
||||
@classmethod
|
||||
def from_user(cls, user, include_store_context: bool = True) -> "UserContext":
|
||||
"""
|
||||
Create UserContext from a User database model.
|
||||
|
||||
Args:
|
||||
user: User database model instance
|
||||
include_store_context: Whether to include token_store_* fields
|
||||
|
||||
Returns:
|
||||
UserContext instance
|
||||
"""
|
||||
data = {
|
||||
"id": user.id,
|
||||
"email": user.email,
|
||||
"username": user.username,
|
||||
"role": user.role,
|
||||
"is_active": user.is_active,
|
||||
"first_name": getattr(user, "first_name", None),
|
||||
"last_name": getattr(user, "last_name", None),
|
||||
"preferred_language": getattr(user, "preferred_language", None),
|
||||
}
|
||||
|
||||
# Add admin platform access info
|
||||
if user.is_admin:
|
||||
if user.is_super_admin:
|
||||
data["accessible_platform_ids"] = None # All platforms
|
||||
else:
|
||||
# Get platform IDs from admin_platforms relationship
|
||||
admin_platforms = getattr(user, "admin_platforms", [])
|
||||
data["accessible_platform_ids"] = [
|
||||
ap.platform_id for ap in admin_platforms if ap.is_active
|
||||
]
|
||||
|
||||
# Add platform context from JWT token (for platform admins after selection)
|
||||
data["token_platform_id"] = getattr(user, "token_platform_id", None)
|
||||
data["token_platform_code"] = getattr(user, "token_platform_code", None)
|
||||
|
||||
# Add store context from JWT token if present
|
||||
if include_store_context:
|
||||
data["token_store_id"] = getattr(user, "token_store_id", None)
|
||||
data["token_store_code"] = getattr(user, "token_store_code", None)
|
||||
data["token_store_role"] = getattr(user, "token_store_role", None)
|
||||
|
||||
return cls(**data)
|
||||
@@ -23,7 +23,7 @@ from app.modules.tenancy.exceptions import (
|
||||
UserNotFoundException,
|
||||
)
|
||||
from app.modules.tenancy.models import AdminPlatform, Platform, User
|
||||
from models.schema.auth import UserContext
|
||||
from app.modules.tenancy.schemas.auth import UserContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -786,6 +786,22 @@ class AdminService:
|
||||
# PRIVATE HELPER METHODS
|
||||
# ============================================================================
|
||||
|
||||
def get_user_by_id(self, db: Session, user_id: int) -> User | None:
|
||||
"""
|
||||
Get user by ID.
|
||||
|
||||
Public method for cross-module consumers that need to look up a user.
|
||||
Returns None if not found (does not raise).
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
user_id: User ID
|
||||
|
||||
Returns:
|
||||
User object or None
|
||||
"""
|
||||
return db.query(User).filter(User.id == user_id).first()
|
||||
|
||||
def _get_user_by_id_or_raise(self, db: Session, user_id: int) -> User:
|
||||
"""Get user by ID or raise UserNotFoundException."""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
|
||||
@@ -329,6 +329,49 @@ class MerchantService:
|
||||
|
||||
return merchant, old_owner, new_owner
|
||||
|
||||
def get_merchant_by_owner_id(
|
||||
self, db: Session, owner_user_id: int
|
||||
) -> Merchant | None:
|
||||
"""
|
||||
Get merchant by owner user ID.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
owner_user_id: Owner user ID
|
||||
|
||||
Returns:
|
||||
First active merchant owned by the user, or None
|
||||
"""
|
||||
return (
|
||||
db.query(Merchant)
|
||||
.filter(
|
||||
Merchant.owner_user_id == owner_user_id,
|
||||
Merchant.is_active == True, # noqa: E712
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
def get_merchant_count_for_owner(
|
||||
self, db: Session, owner_user_id: int, active_only: bool = True
|
||||
) -> int:
|
||||
"""
|
||||
Count merchants owned by a user.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
owner_user_id: Owner user ID
|
||||
active_only: Only count active merchants
|
||||
|
||||
Returns:
|
||||
Number of merchants
|
||||
"""
|
||||
query = db.query(func.count(Merchant.id)).filter(
|
||||
Merchant.owner_user_id == owner_user_id
|
||||
)
|
||||
if active_only:
|
||||
query = query.filter(Merchant.is_active == True) # noqa: E712
|
||||
return query.scalar() or 0
|
||||
|
||||
def get_merchant_stores(
|
||||
self, db: Session, merchant_id: int, skip: int = 0, limit: int = 100
|
||||
) -> tuple[list, int]:
|
||||
|
||||
@@ -142,6 +142,31 @@ class PlatformService:
|
||||
or 0
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_active_store_count(db: Session, platform_id: int) -> int:
|
||||
"""
|
||||
Get count of active stores on a platform.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
platform_id: Platform ID
|
||||
|
||||
Returns:
|
||||
Active store count
|
||||
"""
|
||||
from app.modules.tenancy.models import Store
|
||||
|
||||
return (
|
||||
db.query(func.count(StorePlatform.store_id))
|
||||
.join(Store, Store.id == StorePlatform.store_id)
|
||||
.filter(
|
||||
StorePlatform.platform_id == platform_id,
|
||||
Store.is_active == True, # noqa: E712
|
||||
)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_platform_pages_count(db: Session, platform_id: int) -> int:
|
||||
"""
|
||||
|
||||
@@ -15,8 +15,8 @@ import pytest
|
||||
|
||||
from app.api.deps import get_current_merchant_api, get_merchant_for_current_user
|
||||
from app.modules.tenancy.models import Merchant, Store, User
|
||||
from app.modules.tenancy.schemas.auth import UserContext
|
||||
from main import app
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
# ============================================================================
|
||||
# Fixtures
|
||||
|
||||
@@ -16,8 +16,8 @@ import pytest
|
||||
|
||||
from app.api.deps import get_current_store_from_cookie_or_header
|
||||
from app.modules.tenancy.models import Merchant, Role, Store, StoreUser, User
|
||||
from app.modules.tenancy.schemas.auth import UserContext
|
||||
from main import app
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
# ============================================================================
|
||||
# Fixtures
|
||||
|
||||
Reference in New Issue
Block a user