From 4aa6f76e4698d91c2cc048c43394eed402699c19 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Thu, 26 Feb 2026 23:57:04 +0100 Subject: [PATCH] refactor(arch): move auth schemas to tenancy module and add cross-module service methods 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 --- app/api/deps.py | 2 +- app/modules/billing/routes/api/admin.py | 2 +- .../billing/routes/api/admin_features.py | 2 +- app/modules/billing/routes/api/store.py | 2 +- .../billing/routes/api/store_addons.py | 2 +- .../billing/routes/api/store_checkout.py | 2 +- .../billing/routes/api/store_features.py | 2 +- app/modules/billing/routes/api/store_usage.py | 2 +- app/modules/billing/routes/pages/merchant.py | 2 +- .../tests/integration/test_merchant_routes.py | 2 +- app/modules/catalog/routes/api/admin.py | 2 +- app/modules/catalog/routes/api/store.py | 2 +- app/modules/cms/routes/api/admin_images.py | 2 +- app/modules/cms/routes/api/admin_media.py | 2 +- .../cms/routes/api/admin_store_themes.py | 2 +- app/modules/cms/routes/api/store_media.py | 2 +- .../core/routes/api/admin_dashboard.py | 2 +- .../core/routes/api/admin_menu_config.py | 2 +- app/modules/core/routes/api/admin_settings.py | 2 +- app/modules/core/routes/api/merchant_menu.py | 2 +- .../core/routes/api/store_dashboard.py | 2 +- app/modules/core/routes/api/store_menu.py | 2 +- app/modules/core/routes/api/store_settings.py | 2 +- app/modules/core/routes/pages/merchant.py | 2 +- app/modules/core/services/auth_service.py | 2 +- .../test_merchant_dashboard_routes.py | 2 +- .../integration/test_merchant_menu_routes.py | 2 +- .../test_store_dashboard_routes.py | 2 +- app/modules/customers/routes/api/admin.py | 2 +- app/modules/customers/routes/api/store.py | 3 +- .../customers/routes/api/storefront.py | 2 +- app/modules/inventory/routes/api/admin.py | 2 +- app/modules/inventory/routes/api/store.py | 2 +- app/modules/loyalty/routes/pages/merchant.py | 2 +- app/modules/loyalty/tests/conftest.py | 2 +- .../marketplace/routes/api/admin_letzshop.py | 2 +- .../routes/api/admin_marketplace.py | 2 +- .../marketplace/routes/api/admin_products.py | 2 +- .../marketplace/routes/api/store_letzshop.py | 2 +- .../routes/api/store_marketplace.py | 2 +- .../routes/api/store_onboarding.py | 2 +- .../integration/test_store_page_routes.py | 2 +- .../tests/unit/test_store_page_routes.py | 2 +- .../routes/api/admin_email_templates.py | 2 +- .../messaging/routes/api/admin_messages.py | 2 +- .../routes/api/admin_notifications.py | 2 +- .../routes/api/store_email_settings.py | 2 +- .../routes/api/store_email_templates.py | 2 +- .../messaging/routes/api/store_messages.py | 2 +- .../routes/api/store_notifications.py | 2 +- .../monitoring/routes/api/admin_audit.py | 2 +- .../routes/api/admin_code_quality.py | 2 +- .../monitoring/routes/api/admin_logs.py | 2 +- .../routes/api/admin_platform_health.py | 2 +- .../monitoring/routes/api/admin_tasks.py | 2 +- .../monitoring/routes/api/admin_tests.py | 2 +- app/modules/orders/routes/api/admin.py | 2 +- .../orders/routes/api/admin_exceptions.py | 2 +- app/modules/orders/routes/api/store.py | 2 +- .../routes/api/store_customer_orders.py | 2 +- .../orders/routes/api/store_exceptions.py | 2 +- .../orders/routes/api/store_invoices.py | 2 +- app/modules/payments/routes/api/store.py | 2 +- app/modules/tenancy/routes/api/admin_auth.py | 6 +- .../routes/api/admin_merchant_domains.py | 2 +- .../tenancy/routes/api/admin_merchants.py | 2 +- .../tenancy/routes/api/admin_module_config.py | 2 +- .../tenancy/routes/api/admin_modules.py | 2 +- .../routes/api/admin_platform_users.py | 4 +- .../tenancy/routes/api/admin_platforms.py | 2 +- .../tenancy/routes/api/admin_store_domains.py | 2 +- .../tenancy/routes/api/admin_store_roles.py | 2 +- .../tenancy/routes/api/admin_stores.py | 2 +- app/modules/tenancy/routes/api/admin_users.py | 2 +- app/modules/tenancy/routes/api/merchant.py | 2 +- .../tenancy/routes/api/merchant_auth.py | 4 +- app/modules/tenancy/routes/api/store_auth.py | 7 +- .../tenancy/routes/api/store_profile.py | 2 +- app/modules/tenancy/routes/api/store_team.py | 2 +- app/modules/tenancy/routes/pages/merchant.py | 2 +- app/modules/tenancy/schemas/__init__.py | 43 +- app/modules/tenancy/schemas/auth.py | 343 ++++++++++++++++ .../services/admin_platform_service.py | 2 +- app/modules/tenancy/services/admin_service.py | 16 + .../tenancy/services/merchant_service.py | 43 ++ .../tenancy/services/platform_service.py | 25 ++ .../tests/integration/test_merchant_routes.py | 2 +- .../integration/test_store_team_roles_api.py | 2 +- docs/architecture/user-context-pattern.md | 2 +- models/schema/auth.py | 368 ++---------------- tests/unit/api/test_deps.py | 2 +- tests/unit/models/schema/test_auth.py | 2 +- tests/unit/services/test_auth_service.py | 2 +- 93 files changed, 599 insertions(+), 427 deletions(-) create mode 100644 app/modules/tenancy/schemas/auth.py diff --git a/app/api/deps.py b/app/api/deps.py index f913abe9..10e0c290 100644 --- a/app/api/deps.py +++ b/app/api/deps.py @@ -56,10 +56,10 @@ from app.modules.tenancy.exceptions import ( ) from app.modules.tenancy.models import Store from app.modules.tenancy.models import User as UserModel +from app.modules.tenancy.schemas.auth import UserContext from app.modules.tenancy.services.store_service import store_service from middleware.auth import AuthManager from middleware.rate_limiter import RateLimiter -from models.schema.auth import UserContext # Initialize dependencies security = HTTPBearer(auto_error=False) # auto_error=False prevents automatic 403 diff --git a/app/modules/billing/routes/api/admin.py b/app/modules/billing/routes/api/admin.py index cf340e68..adc3a609 100644 --- a/app/modules/billing/routes/api/admin.py +++ b/app/modules/billing/routes/api/admin.py @@ -35,7 +35,7 @@ from app.modules.billing.services import ( subscription_service, ) from app.modules.enums import FrontendType -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext logger = logging.getLogger(__name__) diff --git a/app/modules/billing/routes/api/admin_features.py b/app/modules/billing/routes/api/admin_features.py index fcac5d8d..3583d169 100644 --- a/app/modules/billing/routes/api/admin_features.py +++ b/app/modules/billing/routes/api/admin_features.py @@ -28,7 +28,7 @@ from app.modules.billing.schemas import ( from app.modules.billing.services.feature_aggregator import feature_aggregator from app.modules.billing.services.feature_service import feature_service from app.modules.enums import FrontendType -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext admin_features_router = APIRouter( prefix="/features", diff --git a/app/modules/billing/routes/api/store.py b/app/modules/billing/routes/api/store.py index 5d68a311..bc33f620 100644 --- a/app/modules/billing/routes/api/store.py +++ b/app/modules/billing/routes/api/store.py @@ -23,7 +23,7 @@ from app.modules.billing.schemas.billing import ( ) from app.modules.billing.services import billing_service, subscription_service from app.modules.enums import FrontendType -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext logger = logging.getLogger(__name__) diff --git a/app/modules/billing/routes/api/store_addons.py b/app/modules/billing/routes/api/store_addons.py index 4436d2f8..4f3c7345 100644 --- a/app/modules/billing/routes/api/store_addons.py +++ b/app/modules/billing/routes/api/store_addons.py @@ -22,7 +22,7 @@ from app.core.config import settings from app.core.database import get_db from app.modules.billing.services import billing_service from app.modules.enums import FrontendType -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext store_addons_router = APIRouter( prefix="/addons", diff --git a/app/modules/billing/routes/api/store_checkout.py b/app/modules/billing/routes/api/store_checkout.py index 153aa941..90187080 100644 --- a/app/modules/billing/routes/api/store_checkout.py +++ b/app/modules/billing/routes/api/store_checkout.py @@ -34,7 +34,7 @@ from app.modules.billing.schemas.billing import ( from app.modules.billing.services import billing_service from app.modules.billing.services.subscription_service import subscription_service from app.modules.enums import FrontendType -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext store_checkout_router = APIRouter( dependencies=[Depends(require_module_access("billing", FrontendType.STORE))], diff --git a/app/modules/billing/routes/api/store_features.py b/app/modules/billing/routes/api/store_features.py index e2d9da22..94c66f88 100644 --- a/app/modules/billing/routes/api/store_features.py +++ b/app/modules/billing/routes/api/store_features.py @@ -38,7 +38,7 @@ from app.modules.billing.services.feature_aggregator import feature_aggregator from app.modules.billing.services.feature_service import feature_service from app.modules.billing.services.subscription_service import subscription_service from app.modules.enums import FrontendType -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext store_features_router = APIRouter( prefix="/features", diff --git a/app/modules/billing/routes/api/store_usage.py b/app/modules/billing/routes/api/store_usage.py index f8b47f3a..231ec52c 100644 --- a/app/modules/billing/routes/api/store_usage.py +++ b/app/modules/billing/routes/api/store_usage.py @@ -20,7 +20,7 @@ from app.api.deps import get_current_store_api, require_module_access from app.core.database import get_db from app.modules.billing.services.usage_service import usage_service from app.modules.enums import FrontendType -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext store_usage_router = APIRouter( prefix="/usage", diff --git a/app/modules/billing/routes/pages/merchant.py b/app/modules/billing/routes/pages/merchant.py index 40893846..0be26d60 100644 --- a/app/modules/billing/routes/pages/merchant.py +++ b/app/modules/billing/routes/pages/merchant.py @@ -24,8 +24,8 @@ from app.api.deps import get_current_merchant_from_cookie_or_header from app.core.database import 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 ROUTE_CONFIG = { "prefix": "/billing", diff --git a/app/modules/billing/tests/integration/test_merchant_routes.py b/app/modules/billing/tests/integration/test_merchant_routes.py index 8b29ca87..3631dd24 100644 --- a/app/modules/billing/tests/integration/test_merchant_routes.py +++ b/app/modules/billing/tests/integration/test_merchant_routes.py @@ -23,8 +23,8 @@ from app.modules.billing.models import ( SubscriptionTier, ) from app.modules.tenancy.models import Merchant, Platform, User +from app.modules.tenancy.schemas.auth import UserContext from main import app -from models.schema.auth import UserContext # ============================================================================ # Fixtures diff --git a/app/modules/catalog/routes/api/admin.py b/app/modules/catalog/routes/api/admin.py index b872ae94..1ff3a84b 100644 --- a/app/modules/catalog/routes/api/admin.py +++ b/app/modules/catalog/routes/api/admin.py @@ -32,7 +32,7 @@ from app.modules.catalog.schemas import ( ) from app.modules.catalog.services.store_product_service import store_product_service from app.modules.enums import FrontendType -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext admin_router = APIRouter( prefix="/store-products", diff --git a/app/modules/catalog/routes/api/store.py b/app/modules/catalog/routes/api/store.py index 4b5c6398..a42e60a1 100644 --- a/app/modules/catalog/routes/api/store.py +++ b/app/modules/catalog/routes/api/store.py @@ -30,7 +30,7 @@ from app.modules.catalog.schemas import ( from app.modules.catalog.services.product_service import product_service from app.modules.catalog.services.store_product_service import store_product_service from app.modules.enums import FrontendType -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext store_router = APIRouter( prefix="/products", diff --git a/app/modules/cms/routes/api/admin_images.py b/app/modules/cms/routes/api/admin_images.py index 4478bf29..871b0431 100644 --- a/app/modules/cms/routes/api/admin_images.py +++ b/app/modules/cms/routes/api/admin_images.py @@ -15,7 +15,7 @@ from app.api.deps import get_current_admin_api from app.core.database import get_db from app.modules.cms.schemas.image import ImageStorageStats from app.modules.cms.services.media_service import media_service -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext admin_images_router = APIRouter(prefix="/images") logger = logging.getLogger(__name__) diff --git a/app/modules/cms/routes/api/admin_media.py b/app/modules/cms/routes/api/admin_media.py index 65051508..cff7ea49 100644 --- a/app/modules/cms/routes/api/admin_media.py +++ b/app/modules/cms/routes/api/admin_media.py @@ -19,7 +19,7 @@ from app.modules.cms.schemas.media import ( MediaUploadResponse, ) from app.modules.cms.services.media_service import media_service -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext admin_media_router = APIRouter(prefix="/media") logger = logging.getLogger(__name__) diff --git a/app/modules/cms/routes/api/admin_store_themes.py b/app/modules/cms/routes/api/admin_store_themes.py index aeb0b948..b89f88b9 100644 --- a/app/modules/cms/routes/api/admin_store_themes.py +++ b/app/modules/cms/routes/api/admin_store_themes.py @@ -26,7 +26,7 @@ from app.modules.cms.schemas.store_theme import ( ThemePresetResponse, ) from app.modules.cms.services.store_theme_service import store_theme_service -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext admin_store_themes_router = APIRouter(prefix="/store-themes") logger = logging.getLogger(__name__) diff --git a/app/modules/cms/routes/api/store_media.py b/app/modules/cms/routes/api/store_media.py index 71b4eaaa..b6cf3698 100644 --- a/app/modules/cms/routes/api/store_media.py +++ b/app/modules/cms/routes/api/store_media.py @@ -27,7 +27,7 @@ from app.modules.cms.schemas.media import ( UploadedFileInfo, ) from app.modules.cms.services.media_service import media_service -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext store_media_router = APIRouter(prefix="/media") logger = logging.getLogger(__name__) diff --git a/app/modules/core/routes/api/admin_dashboard.py b/app/modules/core/routes/api/admin_dashboard.py index 49ed9249..682e1967 100644 --- a/app/modules/core/routes/api/admin_dashboard.py +++ b/app/modules/core/routes/api/admin_dashboard.py @@ -31,7 +31,7 @@ from app.modules.core.schemas.dashboard import ( ) from app.modules.core.services.stats_aggregator import stats_aggregator from app.modules.core.services.widget_aggregator import widget_aggregator -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext admin_dashboard_router = APIRouter(prefix="/dashboard") logger = logging.getLogger(__name__) diff --git a/app/modules/core/routes/api/admin_menu_config.py b/app/modules/core/routes/api/admin_menu_config.py index 1f3ca276..518a3d96 100644 --- a/app/modules/core/routes/api/admin_menu_config.py +++ b/app/modules/core/routes/api/admin_menu_config.py @@ -29,9 +29,9 @@ from app.api.deps import ( ) from app.modules.core.services.menu_service import MenuItemConfig, menu_service from app.modules.enums import FrontendType # API-007 - Enum for type safety +from app.modules.tenancy.schemas.auth import UserContext from app.modules.tenancy.services.platform_service import platform_service from app.utils.i18n import DEFAULT_LANGUAGE, translate -from models.schema.auth import UserContext logger = logging.getLogger(__name__) router = APIRouter(prefix="/menu-config") diff --git a/app/modules/core/routes/api/admin_settings.py b/app/modules/core/routes/api/admin_settings.py index b217a510..0d7beb2e 100644 --- a/app/modules/core/routes/api/admin_settings.py +++ b/app/modules/core/routes/api/admin_settings.py @@ -32,7 +32,7 @@ from app.modules.tenancy.schemas.admin import ( RowsPerPageResponse, RowsPerPageUpdateResponse, ) -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext admin_settings_router = APIRouter(prefix="/settings") logger = logging.getLogger(__name__) diff --git a/app/modules/core/routes/api/merchant_menu.py b/app/modules/core/routes/api/merchant_menu.py index 432eba06..b52a9f55 100644 --- a/app/modules/core/routes/api/merchant_menu.py +++ b/app/modules/core/routes/api/merchant_menu.py @@ -20,8 +20,8 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_merchant_from_cookie_or_header, get_db from app.modules.core.services.menu_service import menu_service from app.modules.enums import FrontendType +from app.modules.tenancy.schemas.auth import UserContext from app.utils.i18n import translate -from models.schema.auth import UserContext logger = logging.getLogger(__name__) diff --git a/app/modules/core/routes/api/store_dashboard.py b/app/modules/core/routes/api/store_dashboard.py index ea8edfde..c517e8be 100644 --- a/app/modules/core/routes/api/store_dashboard.py +++ b/app/modules/core/routes/api/store_dashboard.py @@ -26,8 +26,8 @@ from app.modules.core.schemas.dashboard import ( ) from app.modules.core.services.stats_aggregator import stats_aggregator from app.modules.tenancy.exceptions import StoreNotActiveException +from app.modules.tenancy.schemas.auth import UserContext from app.modules.tenancy.services.store_service import store_service -from models.schema.auth import UserContext store_dashboard_router = APIRouter(prefix="/dashboard") logger = logging.getLogger(__name__) diff --git a/app/modules/core/routes/api/store_menu.py b/app/modules/core/routes/api/store_menu.py index 033b8edd..c25fc5f4 100644 --- a/app/modules/core/routes/api/store_menu.py +++ b/app/modules/core/routes/api/store_menu.py @@ -21,9 +21,9 @@ from app.api.deps import get_current_store_api, get_user_permissions from app.core.database import get_db from app.modules.core.services.menu_service import menu_service from app.modules.enums import FrontendType +from app.modules.tenancy.schemas.auth import UserContext from app.modules.tenancy.services.store_service import store_service from app.utils.i18n import translate -from models.schema.auth import UserContext logger = logging.getLogger(__name__) diff --git a/app/modules/core/routes/api/store_settings.py b/app/modules/core/routes/api/store_settings.py index 091cf624..bbb02b9f 100644 --- a/app/modules/core/routes/api/store_settings.py +++ b/app/modules/core/routes/api/store_settings.py @@ -17,8 +17,8 @@ from app.core.database import get_db from app.modules.core.services.platform_settings_service import ( platform_settings_service, ) +from app.modules.tenancy.schemas.auth import UserContext from app.modules.tenancy.services.store_service import store_service -from models.schema.auth import UserContext store_settings_router = APIRouter(prefix="/settings") logger = logging.getLogger(__name__) diff --git a/app/modules/core/routes/pages/merchant.py b/app/modules/core/routes/pages/merchant.py index 5059f752..534b58b6 100644 --- a/app/modules/core/routes/pages/merchant.py +++ b/app/modules/core/routes/pages/merchant.py @@ -22,8 +22,8 @@ from app.api.deps import ( ) 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 ROUTE_CONFIG = { "prefix": "", diff --git a/app/modules/core/services/auth_service.py b/app/modules/core/services/auth_service.py index 9f34de85..faba59b0 100644 --- a/app/modules/core/services/auth_service.py +++ b/app/modules/core/services/auth_service.py @@ -23,8 +23,8 @@ from app.modules.tenancy.exceptions import ( UserNotActiveException, ) from app.modules.tenancy.models import Store, StoreUser, User +from app.modules.tenancy.schemas.auth import UserLogin from middleware.auth import AuthManager -from models.schema.auth import UserLogin logger = logging.getLogger(__name__) diff --git a/app/modules/core/tests/integration/test_merchant_dashboard_routes.py b/app/modules/core/tests/integration/test_merchant_dashboard_routes.py index 5f5bae62..ee7723e8 100644 --- a/app/modules/core/tests/integration/test_merchant_dashboard_routes.py +++ b/app/modules/core/tests/integration/test_merchant_dashboard_routes.py @@ -19,8 +19,8 @@ from app.modules.billing.models import ( ) from app.modules.customers.models.customer import Customer from app.modules.tenancy.models import Merchant, Platform, Store, StoreUser, User +from app.modules.tenancy.schemas.auth import UserContext from main import app -from models.schema.auth import UserContext # ============================================================================ # Fixtures diff --git a/app/modules/core/tests/integration/test_merchant_menu_routes.py b/app/modules/core/tests/integration/test_merchant_menu_routes.py index 8288db2f..2f726744 100644 --- a/app/modules/core/tests/integration/test_merchant_menu_routes.py +++ b/app/modules/core/tests/integration/test_merchant_menu_routes.py @@ -26,8 +26,8 @@ from app.modules.billing.models import ( ) from app.modules.tenancy.models import Merchant, Platform, User from app.modules.tenancy.models.platform_module import PlatformModule +from app.modules.tenancy.schemas.auth import UserContext from main import app -from models.schema.auth import UserContext # ============================================================================ # Fixtures diff --git a/app/modules/core/tests/integration/test_store_dashboard_routes.py b/app/modules/core/tests/integration/test_store_dashboard_routes.py index c5cceda3..8bcc6210 100644 --- a/app/modules/core/tests/integration/test_store_dashboard_routes.py +++ b/app/modules/core/tests/integration/test_store_dashboard_routes.py @@ -19,8 +19,8 @@ import pytest from app.api.deps import get_current_store_from_cookie_or_header from app.modules.tenancy.models import Merchant, Platform, Store, User from app.modules.tenancy.models.store_platform import StorePlatform +from app.modules.tenancy.schemas.auth import UserContext from main import app -from models.schema.auth import UserContext # ============================================================================ # Fixtures diff --git a/app/modules/customers/routes/api/admin.py b/app/modules/customers/routes/api/admin.py index a4919399..b1c0dd9b 100644 --- a/app/modules/customers/routes/api/admin.py +++ b/app/modules/customers/routes/api/admin.py @@ -18,7 +18,7 @@ from app.modules.customers.schemas import ( ) from app.modules.customers.services import admin_customer_service from app.modules.enums import FrontendType -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext # Create module-aware router admin_router = APIRouter( diff --git a/app/modules/customers/routes/api/store.py b/app/modules/customers/routes/api/store.py index 95423d18..08f5bcac 100644 --- a/app/modules/customers/routes/api/store.py +++ b/app/modules/customers/routes/api/store.py @@ -22,7 +22,7 @@ from app.modules.customers.schemas import ( ) from app.modules.customers.services import customer_service from app.modules.enums import FrontendType -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext # Create module-aware router store_router = APIRouter( @@ -150,4 +150,3 @@ def toggle_customer_status( status = "activated" if customer.is_active else "deactivated" return CustomerMessageResponse(message=f"Customer {status} successfully") - diff --git a/app/modules/customers/routes/api/storefront.py b/app/modules/customers/routes/api/storefront.py index 5923af5f..c9bf83d7 100644 --- a/app/modules/customers/routes/api/storefront.py +++ b/app/modules/customers/routes/api/storefront.py @@ -47,7 +47,7 @@ from app.modules.messaging.services.email_service import ( EmailService, # MOD-004 - Core email service ) from app.modules.tenancy.exceptions import StoreNotFoundException -from models.schema.auth import ( +from app.modules.tenancy.schemas.auth import ( LogoutResponse, PasswordResetRequestResponse, PasswordResetResponse, diff --git a/app/modules/inventory/routes/api/admin.py b/app/modules/inventory/routes/api/admin.py index 6ca9f026..c387d0c4 100644 --- a/app/modules/inventory/routes/api/admin.py +++ b/app/modules/inventory/routes/api/admin.py @@ -46,7 +46,7 @@ from app.modules.inventory.services.inventory_service import inventory_service from app.modules.inventory.services.inventory_transaction_service import ( inventory_transaction_service, ) -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext admin_router = APIRouter( prefix="/inventory", diff --git a/app/modules/inventory/routes/api/store.py b/app/modules/inventory/routes/api/store.py index bf954789..960102bb 100644 --- a/app/modules/inventory/routes/api/store.py +++ b/app/modules/inventory/routes/api/store.py @@ -32,7 +32,7 @@ from app.modules.inventory.services.inventory_service import inventory_service from app.modules.inventory.services.inventory_transaction_service import ( inventory_transaction_service, ) -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext store_router = APIRouter( prefix="/inventory", diff --git a/app/modules/loyalty/routes/pages/merchant.py b/app/modules/loyalty/routes/pages/merchant.py index 416ad4b9..9bc6a5b4 100644 --- a/app/modules/loyalty/routes/pages/merchant.py +++ b/app/modules/loyalty/routes/pages/merchant.py @@ -26,8 +26,8 @@ from app.modules.core.utils.page_context import get_context_for_frontend from app.modules.enums import FrontendType from app.modules.loyalty.services import program_service from app.modules.tenancy.models import Merchant +from app.modules.tenancy.schemas.auth import UserContext from app.templates_config import templates -from models.schema.auth import UserContext logger = logging.getLogger(__name__) diff --git a/app/modules/loyalty/tests/conftest.py b/app/modules/loyalty/tests/conftest.py index 4f032a02..832d6905 100644 --- a/app/modules/loyalty/tests/conftest.py +++ b/app/modules/loyalty/tests/conftest.py @@ -15,8 +15,8 @@ from app.modules.loyalty.models.loyalty_program import LoyaltyType from app.modules.tenancy.models import Merchant, Platform, Store, User from app.modules.tenancy.models.store import StoreUser from app.modules.tenancy.models.store_platform import StorePlatform +from app.modules.tenancy.schemas.auth import UserContext from main import app -from models.schema.auth import UserContext @pytest.fixture diff --git a/app/modules/marketplace/routes/api/admin_letzshop.py b/app/modules/marketplace/routes/api/admin_letzshop.py index ece06c30..2d62fe3e 100644 --- a/app/modules/marketplace/routes/api/admin_letzshop.py +++ b/app/modules/marketplace/routes/api/admin_letzshop.py @@ -64,7 +64,7 @@ from app.modules.orders.exceptions import OrderHasUnresolvedExceptionsException from app.modules.orders.services.order_item_exception_service import ( order_item_exception_service, ) -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext admin_letzshop_router = APIRouter( prefix="/letzshop", diff --git a/app/modules/marketplace/routes/api/admin_marketplace.py b/app/modules/marketplace/routes/api/admin_marketplace.py index f13a1fa1..7b9473f5 100644 --- a/app/modules/marketplace/routes/api/admin_marketplace.py +++ b/app/modules/marketplace/routes/api/admin_marketplace.py @@ -27,8 +27,8 @@ from app.modules.marketplace.schemas import ( from app.modules.marketplace.services.marketplace_import_job_service import ( marketplace_import_job_service, ) +from app.modules.tenancy.schemas.auth import UserContext from app.modules.tenancy.services.store_service import store_service -from models.schema.auth import UserContext admin_marketplace_router = APIRouter( prefix="/marketplace-import-jobs", diff --git a/app/modules/marketplace/routes/api/admin_products.py b/app/modules/marketplace/routes/api/admin_products.py index e22fb3f0..9b31b3b9 100644 --- a/app/modules/marketplace/routes/api/admin_products.py +++ b/app/modules/marketplace/routes/api/admin_products.py @@ -24,7 +24,7 @@ from app.modules.enums import FrontendType from app.modules.marketplace.services.marketplace_product_service import ( marketplace_product_service, ) -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext admin_products_router = APIRouter( prefix="/products", diff --git a/app/modules/marketplace/routes/api/store_letzshop.py b/app/modules/marketplace/routes/api/store_letzshop.py index d0d441f9..d7a6e083 100644 --- a/app/modules/marketplace/routes/api/store_letzshop.py +++ b/app/modules/marketplace/routes/api/store_letzshop.py @@ -55,7 +55,7 @@ from app.modules.orders.exceptions import OrderHasUnresolvedExceptionsException from app.modules.orders.services.order_item_exception_service import ( order_item_exception_service, ) -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext store_letzshop_router = APIRouter( prefix="/letzshop", diff --git a/app/modules/marketplace/routes/api/store_marketplace.py b/app/modules/marketplace/routes/api/store_marketplace.py index 826d9953..d353ed4d 100644 --- a/app/modules/marketplace/routes/api/store_marketplace.py +++ b/app/modules/marketplace/routes/api/store_marketplace.py @@ -23,9 +23,9 @@ from app.modules.marketplace.schemas import ( from app.modules.marketplace.services.marketplace_import_job_service import ( marketplace_import_job_service, ) +from app.modules.tenancy.schemas.auth import UserContext from app.modules.tenancy.services.store_service import store_service from middleware.decorators import rate_limit -from models.schema.auth import UserContext store_marketplace_router = APIRouter( prefix="/marketplace", diff --git a/app/modules/marketplace/routes/api/store_onboarding.py b/app/modules/marketplace/routes/api/store_onboarding.py index 13613bab..800005ba 100644 --- a/app/modules/marketplace/routes/api/store_onboarding.py +++ b/app/modules/marketplace/routes/api/store_onboarding.py @@ -38,7 +38,7 @@ from app.modules.marketplace.schemas import ( ProductImportConfigResponse, ) from app.modules.marketplace.services.onboarding_service import OnboardingService -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext store_onboarding_router = APIRouter( prefix="/onboarding", diff --git a/app/modules/marketplace/tests/integration/test_store_page_routes.py b/app/modules/marketplace/tests/integration/test_store_page_routes.py index be166927..7197cb6c 100644 --- a/app/modules/marketplace/tests/integration/test_store_page_routes.py +++ b/app/modules/marketplace/tests/integration/test_store_page_routes.py @@ -25,8 +25,8 @@ from app.modules.marketplace.models import OnboardingStatus, StoreOnboarding from app.modules.tenancy.models import Merchant, Platform, Store, User from app.modules.tenancy.models.platform_module import PlatformModule from app.modules.tenancy.models.store_platform import StorePlatform +from app.modules.tenancy.schemas.auth import UserContext from main import app -from models.schema.auth import UserContext # ============================================================================ # Fixtures diff --git a/app/modules/marketplace/tests/unit/test_store_page_routes.py b/app/modules/marketplace/tests/unit/test_store_page_routes.py index 2e68125f..d86a1c10 100644 --- a/app/modules/marketplace/tests/unit/test_store_page_routes.py +++ b/app/modules/marketplace/tests/unit/test_store_page_routes.py @@ -19,7 +19,7 @@ from app.modules.marketplace.routes.pages.store import ( store_marketplace_page, store_onboarding_page, ) -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext def _make_user_context(store_id: int = 1, store_code: str = "teststore") -> UserContext: diff --git a/app/modules/messaging/routes/api/admin_email_templates.py b/app/modules/messaging/routes/api/admin_email_templates.py index cc5e6dde..b87fae1a 100644 --- a/app/modules/messaging/routes/api/admin_email_templates.py +++ b/app/modules/messaging/routes/api/admin_email_templates.py @@ -21,7 +21,7 @@ from app.api.deps import get_current_admin_api from app.core.database import get_db from app.modules.messaging.services.email_service import EmailService from app.modules.messaging.services.email_template_service import EmailTemplateService -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext admin_email_templates_router = APIRouter(prefix="/email-templates") logger = logging.getLogger(__name__) diff --git a/app/modules/messaging/routes/api/admin_messages.py b/app/modules/messaging/routes/api/admin_messages.py index 5343c5f5..6ed0440d 100644 --- a/app/modules/messaging/routes/api/admin_messages.py +++ b/app/modules/messaging/routes/api/admin_messages.py @@ -47,7 +47,7 @@ from app.modules.messaging.services.message_attachment_service import ( message_attachment_service, ) from app.modules.messaging.services.messaging_service import messaging_service -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext admin_messages_router = APIRouter(prefix="/messages") logger = logging.getLogger(__name__) diff --git a/app/modules/messaging/routes/api/admin_notifications.py b/app/modules/messaging/routes/api/admin_notifications.py index ff632f3c..8ad33820 100644 --- a/app/modules/messaging/routes/api/admin_notifications.py +++ b/app/modules/messaging/routes/api/admin_notifications.py @@ -33,7 +33,7 @@ from app.modules.tenancy.schemas.admin import ( PlatformAlertResolve, PlatformAlertResponse, ) -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext admin_notifications_router = APIRouter(prefix="/notifications") logger = logging.getLogger(__name__) diff --git a/app/modules/messaging/routes/api/store_email_settings.py b/app/modules/messaging/routes/api/store_email_settings.py index bb47af13..09200179 100644 --- a/app/modules/messaging/routes/api/store_email_settings.py +++ b/app/modules/messaging/routes/api/store_email_settings.py @@ -24,7 +24,7 @@ from app.modules.billing.services.subscription_service import subscription_servi from app.modules.messaging.services.store_email_settings_service import ( store_email_settings_service, ) -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext store_email_settings_router = APIRouter(prefix="/email-settings") logger = logging.getLogger(__name__) diff --git a/app/modules/messaging/routes/api/store_email_templates.py b/app/modules/messaging/routes/api/store_email_templates.py index 76d31fb7..b590dcb3 100644 --- a/app/modules/messaging/routes/api/store_email_templates.py +++ b/app/modules/messaging/routes/api/store_email_templates.py @@ -19,8 +19,8 @@ from app.api.deps import get_current_store_api from app.core.database import get_db from app.modules.messaging.services.email_service import EmailService from app.modules.messaging.services.email_template_service import EmailTemplateService +from app.modules.tenancy.schemas.auth import UserContext from app.modules.tenancy.services.store_service import store_service -from models.schema.auth import UserContext store_email_templates_router = APIRouter(prefix="/email-templates") logger = logging.getLogger(__name__) diff --git a/app/modules/messaging/routes/api/store_messages.py b/app/modules/messaging/routes/api/store_messages.py index 0ea1045a..1bcdb8a1 100644 --- a/app/modules/messaging/routes/api/store_messages.py +++ b/app/modules/messaging/routes/api/store_messages.py @@ -49,7 +49,7 @@ from app.modules.messaging.services.message_attachment_service import ( message_attachment_service, ) from app.modules.messaging.services.messaging_service import messaging_service -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext store_messages_router = APIRouter(prefix="/messages") logger = logging.getLogger(__name__) diff --git a/app/modules/messaging/routes/api/store_notifications.py b/app/modules/messaging/routes/api/store_notifications.py index a36b60b8..de3b19ff 100644 --- a/app/modules/messaging/routes/api/store_notifications.py +++ b/app/modules/messaging/routes/api/store_notifications.py @@ -23,8 +23,8 @@ from app.modules.messaging.schemas import ( TestNotificationRequest, UnreadCountResponse, ) +from app.modules.tenancy.schemas.auth import UserContext from app.modules.tenancy.services.store_service import store_service -from models.schema.auth import UserContext store_notifications_router = APIRouter(prefix="/notifications") logger = logging.getLogger(__name__) diff --git a/app/modules/monitoring/routes/api/admin_audit.py b/app/modules/monitoring/routes/api/admin_audit.py index febf7f5a..59481a21 100644 --- a/app/modules/monitoring/routes/api/admin_audit.py +++ b/app/modules/monitoring/routes/api/admin_audit.py @@ -22,7 +22,7 @@ from app.modules.tenancy.schemas.admin import ( AdminAuditLogListResponse, AdminAuditLogResponse, ) -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext admin_audit_router = APIRouter(prefix="/audit") logger = logging.getLogger(__name__) diff --git a/app/modules/monitoring/routes/api/admin_code_quality.py b/app/modules/monitoring/routes/api/admin_code_quality.py index af201446..50bd4b2a 100644 --- a/app/modules/monitoring/routes/api/admin_code_quality.py +++ b/app/modules/monitoring/routes/api/admin_code_quality.py @@ -24,7 +24,7 @@ from app.modules.monitoring.exceptions import ( ScanNotFoundException, ViolationNotFoundException, ) -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext admin_code_quality_router = APIRouter(prefix="/code-quality") diff --git a/app/modules/monitoring/routes/api/admin_logs.py b/app/modules/monitoring/routes/api/admin_logs.py index 74f27aa1..cce37e77 100644 --- a/app/modules/monitoring/routes/api/admin_logs.py +++ b/app/modules/monitoring/routes/api/admin_logs.py @@ -35,7 +35,7 @@ from app.modules.tenancy.schemas.admin import ( LogSettingsUpdateResponse, LogStatistics, ) -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext admin_logs_router = APIRouter(prefix="/logs") logger = logging.getLogger(__name__) diff --git a/app/modules/monitoring/routes/api/admin_platform_health.py b/app/modules/monitoring/routes/api/admin_platform_health.py index 92937f7d..cb0f0fb9 100644 --- a/app/modules/monitoring/routes/api/admin_platform_health.py +++ b/app/modules/monitoring/routes/api/admin_platform_health.py @@ -19,7 +19,7 @@ from app.core.database import get_db from app.modules.monitoring.services.platform_health_service import ( platform_health_service, ) -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext admin_platform_health_router = APIRouter(prefix="/platform") logger = logging.getLogger(__name__) diff --git a/app/modules/monitoring/routes/api/admin_tasks.py b/app/modules/monitoring/routes/api/admin_tasks.py index 4c3590d3..56149c63 100644 --- a/app/modules/monitoring/routes/api/admin_tasks.py +++ b/app/modules/monitoring/routes/api/admin_tasks.py @@ -15,7 +15,7 @@ from app.core.database import get_db from app.modules.monitoring.services.background_tasks_service import ( background_tasks_service, ) -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext admin_tasks_router = APIRouter(prefix="/tasks") diff --git a/app/modules/monitoring/routes/api/admin_tests.py b/app/modules/monitoring/routes/api/admin_tests.py index 1bd115e1..7c681546 100644 --- a/app/modules/monitoring/routes/api/admin_tests.py +++ b/app/modules/monitoring/routes/api/admin_tests.py @@ -11,7 +11,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.dev_tools.services.test_runner_service import test_runner_service -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext admin_tests_router = APIRouter(prefix="/tests") diff --git a/app/modules/orders/routes/api/admin.py b/app/modules/orders/routes/api/admin.py index bcf9cf0c..e06840e5 100644 --- a/app/modules/orders/routes/api/admin.py +++ b/app/modules/orders/routes/api/admin.py @@ -33,7 +33,7 @@ from app.modules.orders.schemas import ( ShippingLabelInfo, ) from app.modules.orders.services.order_service import order_service -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext # Base router for orders _orders_router = APIRouter( diff --git a/app/modules/orders/routes/api/admin_exceptions.py b/app/modules/orders/routes/api/admin_exceptions.py index 3450dd40..346d25d5 100644 --- a/app/modules/orders/routes/api/admin_exceptions.py +++ b/app/modules/orders/routes/api/admin_exceptions.py @@ -29,7 +29,7 @@ from app.modules.orders.schemas import ( from app.modules.orders.services.order_item_exception_service import ( order_item_exception_service, ) -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext logger = logging.getLogger(__name__) diff --git a/app/modules/orders/routes/api/store.py b/app/modules/orders/routes/api/store.py index 5b145e36..e409faa4 100644 --- a/app/modules/orders/routes/api/store.py +++ b/app/modules/orders/routes/api/store.py @@ -25,7 +25,7 @@ from app.modules.orders.schemas import ( ) from app.modules.orders.services.order_inventory_service import order_inventory_service from app.modules.orders.services.order_service import order_service -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext # Base router for orders _orders_router = APIRouter( diff --git a/app/modules/orders/routes/api/store_customer_orders.py b/app/modules/orders/routes/api/store_customer_orders.py index fa3d138b..3216fa6b 100644 --- a/app/modules/orders/routes/api/store_customer_orders.py +++ b/app/modules/orders/routes/api/store_customer_orders.py @@ -21,7 +21,7 @@ from app.core.database import get_db from app.modules.enums import FrontendType from app.modules.orders.services.customer_order_service import customer_order_service from app.modules.orders.services.order_metrics import order_metrics_provider -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext logger = logging.getLogger(__name__) diff --git a/app/modules/orders/routes/api/store_exceptions.py b/app/modules/orders/routes/api/store_exceptions.py index 6a03c2c4..ec68e400 100644 --- a/app/modules/orders/routes/api/store_exceptions.py +++ b/app/modules/orders/routes/api/store_exceptions.py @@ -28,7 +28,7 @@ from app.modules.orders.schemas import ( from app.modules.orders.services.order_item_exception_service import ( order_item_exception_service, ) -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext logger = logging.getLogger(__name__) diff --git a/app/modules/orders/routes/api/store_invoices.py b/app/modules/orders/routes/api/store_invoices.py index d225c77a..00e8b8c2 100644 --- a/app/modules/orders/routes/api/store_invoices.py +++ b/app/modules/orders/routes/api/store_invoices.py @@ -51,7 +51,7 @@ from app.modules.orders.schemas import ( StoreInvoiceSettingsUpdate, ) from app.modules.orders.services.invoice_service import invoice_service -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext store_invoices_router = APIRouter( prefix="/invoices", diff --git a/app/modules/payments/routes/api/store.py b/app/modules/payments/routes/api/store.py index 832b1491..4b49938f 100644 --- a/app/modules/payments/routes/api/store.py +++ b/app/modules/payments/routes/api/store.py @@ -39,8 +39,8 @@ from app.modules.payments.schemas import ( from app.modules.payments.schemas import ( PaymentRefundResponse as RefundResponse, ) +from app.modules.tenancy.schemas.auth import UserContext from app.modules.tenancy.services.store_service import store_service -from models.schema.auth import UserContext store_router = APIRouter( prefix="/payments", diff --git a/app/modules/tenancy/routes/api/admin_auth.py b/app/modules/tenancy/routes/api/admin_auth.py index b09cb13e..bd76b93a 100644 --- a/app/modules/tenancy/routes/api/admin_auth.py +++ b/app/modules/tenancy/routes/api/admin_auth.py @@ -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__) diff --git a/app/modules/tenancy/routes/api/admin_merchant_domains.py b/app/modules/tenancy/routes/api/admin_merchant_domains.py index 9fc476ad..c5e1e812 100644 --- a/app/modules/tenancy/routes/api/admin_merchant_domains.py +++ b/app/modules/tenancy/routes/api/admin_merchant_domains.py @@ -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__) diff --git a/app/modules/tenancy/routes/api/admin_merchants.py b/app/modules/tenancy/routes/api/admin_merchants.py index 06bd9341..0d6b34d2 100644 --- a/app/modules/tenancy/routes/api/admin_merchants.py +++ b/app/modules/tenancy/routes/api/admin_merchants.py @@ -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__) diff --git a/app/modules/tenancy/routes/api/admin_module_config.py b/app/modules/tenancy/routes/api/admin_module_config.py index 61902ded..1957e4f4 100644 --- a/app/modules/tenancy/routes/api/admin_module_config.py +++ b/app/modules/tenancy/routes/api/admin_module_config.py @@ -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") diff --git a/app/modules/tenancy/routes/api/admin_modules.py b/app/modules/tenancy/routes/api/admin_modules.py index c942b79e..4d13efea 100644 --- a/app/modules/tenancy/routes/api/admin_modules.py +++ b/app/modules/tenancy/routes/api/admin_modules.py @@ -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") diff --git a/app/modules/tenancy/routes/api/admin_platform_users.py b/app/modules/tenancy/routes/api/admin_platform_users.py index 8d7b499c..86ae6429 100644 --- a/app/modules/tenancy/routes/api/admin_platform_users.py +++ b/app/modules/tenancy/routes/api/admin_platform_users.py @@ -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__) diff --git a/app/modules/tenancy/routes/api/admin_platforms.py b/app/modules/tenancy/routes/api/admin_platforms.py index 66614627..36930463 100644 --- a/app/modules/tenancy/routes/api/admin_platforms.py +++ b/app/modules/tenancy/routes/api/admin_platforms.py @@ -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") diff --git a/app/modules/tenancy/routes/api/admin_store_domains.py b/app/modules/tenancy/routes/api/admin_store_domains.py index fa6053a7..3f235892 100644 --- a/app/modules/tenancy/routes/api/admin_store_domains.py +++ b/app/modules/tenancy/routes/api/admin_store_domains.py @@ -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__) diff --git a/app/modules/tenancy/routes/api/admin_store_roles.py b/app/modules/tenancy/routes/api/admin_store_roles.py index 57572382..86f22c93 100644 --- a/app/modules/tenancy/routes/api/admin_store_roles.py +++ b/app/modules/tenancy/routes/api/admin_store_roles.py @@ -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__) diff --git a/app/modules/tenancy/routes/api/admin_stores.py b/app/modules/tenancy/routes/api/admin_stores.py index 319ed8f6..05ad2439 100644 --- a/app/modules/tenancy/routes/api/admin_stores.py +++ b/app/modules/tenancy/routes/api/admin_stores.py @@ -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__) diff --git a/app/modules/tenancy/routes/api/admin_users.py b/app/modules/tenancy/routes/api/admin_users.py index aa41cc68..10cf85db 100644 --- a/app/modules/tenancy/routes/api/admin_users.py +++ b/app/modules/tenancy/routes/api/admin_users.py @@ -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__) diff --git a/app/modules/tenancy/routes/api/merchant.py b/app/modules/tenancy/routes/api/merchant.py index a5e0bae4..9df7a1a6 100644 --- a/app/modules/tenancy/routes/api/merchant.py +++ b/app/modules/tenancy/routes/api/merchant.py @@ -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 diff --git a/app/modules/tenancy/routes/api/merchant_auth.py b/app/modules/tenancy/routes/api/merchant_auth.py index f63ea17d..f80b3647 100644 --- a/app/modules/tenancy/routes/api/merchant_auth.py +++ b/app/modules/tenancy/routes/api/merchant_auth.py @@ -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__) diff --git a/app/modules/tenancy/routes/api/store_auth.py b/app/modules/tenancy/routes/api/store_auth.py index f2f5043c..bdd95115 100644 --- a/app/modules/tenancy/routes/api/store_auth.py +++ b/app/modules/tenancy/routes/api/store_auth.py @@ -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__) diff --git a/app/modules/tenancy/routes/api/store_profile.py b/app/modules/tenancy/routes/api/store_profile.py index 545d4990..92a639db 100644 --- a/app/modules/tenancy/routes/api/store_profile.py +++ b/app/modules/tenancy/routes/api/store_profile.py @@ -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__) diff --git a/app/modules/tenancy/routes/api/store_team.py b/app/modules/tenancy/routes/api/store_team.py index 6cf37e8f..4bfc0001 100644 --- a/app/modules/tenancy/routes/api/store_team.py +++ b/app/modules/tenancy/routes/api/store_team.py @@ -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__) diff --git a/app/modules/tenancy/routes/pages/merchant.py b/app/modules/tenancy/routes/pages/merchant.py index 8cc4946c..24f9dfb4 100644 --- a/app/modules/tenancy/routes/pages/merchant.py +++ b/app/modules/tenancy/routes/pages/merchant.py @@ -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() diff --git a/app/modules/tenancy/schemas/__init__.py b/app/modules/tenancy/schemas/__init__.py index c3861189..ee84d26a 100644 --- a/app/modules/tenancy/schemas/__init__.py +++ b/app/modules/tenancy/schemas/__init__.py @@ -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", diff --git a/app/modules/tenancy/schemas/auth.py b/app/modules/tenancy/schemas/auth.py new file mode 100644 index 00000000..97149530 --- /dev/null +++ b/app/modules/tenancy/schemas/auth.py @@ -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) diff --git a/app/modules/tenancy/services/admin_platform_service.py b/app/modules/tenancy/services/admin_platform_service.py index ea153b92..7b27d8f7 100644 --- a/app/modules/tenancy/services/admin_platform_service.py +++ b/app/modules/tenancy/services/admin_platform_service.py @@ -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__) diff --git a/app/modules/tenancy/services/admin_service.py b/app/modules/tenancy/services/admin_service.py index bfc326bb..ad355995 100644 --- a/app/modules/tenancy/services/admin_service.py +++ b/app/modules/tenancy/services/admin_service.py @@ -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() diff --git a/app/modules/tenancy/services/merchant_service.py b/app/modules/tenancy/services/merchant_service.py index 174d3eb4..7ed091bd 100644 --- a/app/modules/tenancy/services/merchant_service.py +++ b/app/modules/tenancy/services/merchant_service.py @@ -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]: diff --git a/app/modules/tenancy/services/platform_service.py b/app/modules/tenancy/services/platform_service.py index f9e47201..b3fdfc29 100644 --- a/app/modules/tenancy/services/platform_service.py +++ b/app/modules/tenancy/services/platform_service.py @@ -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: """ diff --git a/app/modules/tenancy/tests/integration/test_merchant_routes.py b/app/modules/tenancy/tests/integration/test_merchant_routes.py index 1b66fe34..8243c758 100644 --- a/app/modules/tenancy/tests/integration/test_merchant_routes.py +++ b/app/modules/tenancy/tests/integration/test_merchant_routes.py @@ -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 diff --git a/app/modules/tenancy/tests/integration/test_store_team_roles_api.py b/app/modules/tenancy/tests/integration/test_store_team_roles_api.py index 61f6e122..64b91f56 100644 --- a/app/modules/tenancy/tests/integration/test_store_team_roles_api.py +++ b/app/modules/tenancy/tests/integration/test_store_team_roles_api.py @@ -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 diff --git a/docs/architecture/user-context-pattern.md b/docs/architecture/user-context-pattern.md index d6377939..e7cc6a86 100644 --- a/docs/architecture/user-context-pattern.md +++ b/docs/architecture/user-context-pattern.md @@ -17,7 +17,7 @@ Routes should not import database models directly (architecture rule API-007). I ```python # CORRECT: Use UserContext from dependencies -from models.schema.auth import UserContext +from app.modules.tenancy.schemas.auth import UserContext @router.get("/endpoint") def my_endpoint(current_user: UserContext = Depends(get_current_admin_api)): diff --git a/models/schema/auth.py b/models/schema/auth.py index b8976fcb..a171c8f6 100644 --- a/models/schema/auth.py +++ b/models/schema/auth.py @@ -1,334 +1,34 @@ -# auth.py - Keep security-critical validation -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) +# models/schema/auth.py +""" +LEGACY LOCATION — re-exports from canonical location. + +All auth schemas have been moved to app/modules/tenancy/schemas/auth.py +per MOD-019 (schemas belong in their module). + +This file provides backwards compatibility re-exports. +New code should import from: app.modules.tenancy.schemas.auth + +Schemas use Pydantic Field and field_validator for input validation. +""" + +from app.modules.tenancy.schemas.auth import ( # noqa: F401 + LoginResponse, + LogoutResponse, + OwnedMerchantSummary, + PasswordResetRequestResponse, + PasswordResetResponse, + PlatformSelectResponse, + StoreMembershipSummary, + StoreUserResponse, + UserContext, + UserCreate, + UserDeleteResponse, + UserDetailResponse, + UserListResponse, + UserLogin, + UserResponse, + UserSearchItem, + UserSearchResponse, + UserStatusToggleResponse, + UserUpdate, +) diff --git a/tests/unit/api/test_deps.py b/tests/unit/api/test_deps.py index f95b11ee..d9ae3e69 100644 --- a/tests/unit/api/test_deps.py +++ b/tests/unit/api/test_deps.py @@ -52,8 +52,8 @@ from app.modules.tenancy.exceptions import ( UnauthorizedStoreAccessException, ) from app.modules.tenancy.models import User +from app.modules.tenancy.schemas.auth import UserContext from middleware.auth import AuthManager -from models.schema.auth import UserContext # ============================================================================ # Fixtures diff --git a/tests/unit/models/schema/test_auth.py b/tests/unit/models/schema/test_auth.py index fb75f6a0..6a24d9ce 100644 --- a/tests/unit/models/schema/test_auth.py +++ b/tests/unit/models/schema/test_auth.py @@ -4,7 +4,7 @@ import pytest from pydantic import ValidationError -from models.schema.auth import ( +from app.modules.tenancy.schemas.auth import ( UserCreate, UserLogin, UserResponse, diff --git a/tests/unit/services/test_auth_service.py b/tests/unit/services/test_auth_service.py index a0d91a54..5aaab3a4 100644 --- a/tests/unit/services/test_auth_service.py +++ b/tests/unit/services/test_auth_service.py @@ -8,7 +8,7 @@ from app.modules.tenancy.exceptions import ( InvalidCredentialsException, UserNotActiveException, ) -from models.schema.auth import UserLogin +from app.modules.tenancy.schemas.auth import UserLogin @pytest.mark.unit