diff --git a/app/api/deps.py b/app/api/deps.py index acebb04e..a9760d14 100644 --- a/app/api/deps.py +++ b/app/api/deps.py @@ -39,7 +39,7 @@ The cookie path restrictions prevent cross-context cookie leakage: import logging from datetime import UTC -from fastapi import Cookie, Depends, Request +from fastapi import Cookie, Depends, HTTPException, Request from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from sqlalchemy.orm import Session @@ -73,6 +73,19 @@ logger = logging.getLogger(__name__) # ============================================================================ +async def get_resolved_store_code(request: Request) -> str: + """Get store code from path parameter (path-based) or middleware (subdomain/custom domain).""" + # Path parameter from double-mount prefix (/store/{store_code}/...) + store_code = request.path_params.get("store_code") + if store_code: + return store_code + # Middleware-resolved store (subdomain or custom domain) + store = getattr(request.state, "store", None) + if store: + return store.store_code + raise HTTPException(status_code=404, detail="Store not found") + + def _get_token_from_request( credentials: HTTPAuthorizationCredentials | None, cookie_value: str | None, diff --git a/app/modules/analytics/routes/pages/store.py b/app/modules/analytics/routes/pages/store.py index 2f7f5ddb..ba96233d 100644 --- a/app/modules/analytics/routes/pages/store.py +++ b/app/modules/analytics/routes/pages/store.py @@ -7,11 +7,15 @@ Store pages for analytics dashboard. import logging -from fastapi import APIRouter, Depends, Path, Request +from fastapi import APIRouter, Depends, Request from fastapi.responses import HTMLResponse from sqlalchemy.orm import Session -from app.api.deps import get_current_store_from_cookie_or_header, get_db +from app.api.deps import ( + get_current_store_from_cookie_or_header, + get_db, + get_resolved_store_code, +) from app.modules.core.services.platform_settings_service import ( platform_settings_service, # MOD-004 - shared platform service ) @@ -73,11 +77,11 @@ def get_store_context( @router.get( - "/{store_code}/analytics", response_class=HTMLResponse, include_in_schema=False + "/analytics", response_class=HTMLResponse, include_in_schema=False ) async def store_analytics_page( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): diff --git a/app/modules/billing/routes/pages/store.py b/app/modules/billing/routes/pages/store.py index b5e4a555..b3dc16f6 100644 --- a/app/modules/billing/routes/pages/store.py +++ b/app/modules/billing/routes/pages/store.py @@ -7,11 +7,15 @@ Store pages for billing management: - Invoices """ -from fastapi import APIRouter, Depends, Path, Request +from fastapi import APIRouter, Depends, Request from fastapi.responses import HTMLResponse from sqlalchemy.orm import Session -from app.api.deps import get_current_store_from_cookie_or_header, get_db +from app.api.deps import ( + get_current_store_from_cookie_or_header, + get_db, + get_resolved_store_code, +) from app.modules.core.utils.page_context import get_store_context from app.modules.tenancy.models import User from app.templates_config import templates @@ -25,11 +29,11 @@ router = APIRouter() @router.get( - "/{store_code}/billing", response_class=HTMLResponse, include_in_schema=False + "/billing", response_class=HTMLResponse, include_in_schema=False ) async def store_billing_page( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): @@ -44,11 +48,11 @@ async def store_billing_page( @router.get( - "/{store_code}/invoices", response_class=HTMLResponse, include_in_schema=False + "/invoices", response_class=HTMLResponse, include_in_schema=False ) async def store_invoices_page( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): diff --git a/app/modules/billing/static/shared/js/feature-store.js b/app/modules/billing/static/shared/js/feature-store.js index 305b1abb..d566135a 100644 --- a/app/modules/billing/static/shared/js/feature-store.js +++ b/app/modules/billing/static/shared/js/feature-store.js @@ -178,10 +178,11 @@ }, /** - * Get store code from URL + * Get store code from server-rendered value or URL fallback * @returns {string|null} */ getStoreCode() { + if (window.STORE_CODE) return window.STORE_CODE; const path = window.location.pathname; const segments = path.split('/').filter(Boolean); // Direct: /store/{code}/... diff --git a/app/modules/billing/static/shared/js/upgrade-prompts.js b/app/modules/billing/static/shared/js/upgrade-prompts.js index b6910888..a81a7d79 100644 --- a/app/modules/billing/static/shared/js/upgrade-prompts.js +++ b/app/modules/billing/static/shared/js/upgrade-prompts.js @@ -139,9 +139,10 @@ }, /** - * Get store code from URL + * Get store code from server-rendered value or URL fallback */ getStoreCode() { + if (window.STORE_CODE) return window.STORE_CODE; const path = window.location.pathname; const segments = path.split('/').filter(Boolean); if (segments[0] === 'store' && segments[1]) { diff --git a/app/modules/catalog/routes/pages/store.py b/app/modules/catalog/routes/pages/store.py index 082c23f7..8b911a56 100644 --- a/app/modules/catalog/routes/pages/store.py +++ b/app/modules/catalog/routes/pages/store.py @@ -7,11 +7,15 @@ Store pages for product management: - Product create """ -from fastapi import APIRouter, Depends, Path, Request +from fastapi import APIRouter, Depends, Request from fastapi.responses import HTMLResponse from sqlalchemy.orm import Session -from app.api.deps import get_current_store_from_cookie_or_header, get_db +from app.api.deps import ( + get_current_store_from_cookie_or_header, + get_db, + get_resolved_store_code, +) from app.modules.core.utils.page_context import get_store_context from app.modules.tenancy.models import User from app.templates_config import templates @@ -25,11 +29,11 @@ router = APIRouter() @router.get( - "/{store_code}/products", response_class=HTMLResponse, include_in_schema=False + "/products", response_class=HTMLResponse, include_in_schema=False ) async def store_products_page( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): @@ -44,13 +48,13 @@ async def store_products_page( @router.get( - "/{store_code}/products/create", + "/products/create", response_class=HTMLResponse, include_in_schema=False, ) async def store_product_create_page( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): diff --git a/app/modules/cms/routes/pages/store.py b/app/modules/cms/routes/pages/store.py index 100864f3..c685ea07 100644 --- a/app/modules/cms/routes/pages/store.py +++ b/app/modules/cms/routes/pages/store.py @@ -11,7 +11,11 @@ from fastapi import APIRouter, Depends, HTTPException, Path, Request from fastapi.responses import HTMLResponse from sqlalchemy.orm import Session -from app.api.deps import get_current_store_from_cookie_or_header, get_db +from app.api.deps import ( + get_current_store_from_cookie_or_header, + get_db, + get_resolved_store_code, +) from app.modules.cms.services import content_page_service from app.modules.core.services.platform_settings_service import ( platform_settings_service, # MOD-004 - shared platform service @@ -83,11 +87,11 @@ def get_store_context( @router.get( - "/{store_code}/content-pages", response_class=HTMLResponse, include_in_schema=False + "/content-pages", response_class=HTMLResponse, include_in_schema=False ) async def store_content_pages_list( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): @@ -102,13 +106,13 @@ async def store_content_pages_list( @router.get( - "/{store_code}/content-pages/create", + "/content-pages/create", response_class=HTMLResponse, include_in_schema=False, ) async def store_content_page_create( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): @@ -122,13 +126,13 @@ async def store_content_page_create( @router.get( - "/{store_code}/content-pages/{page_id}/edit", + "/content-pages/{page_id}/edit", response_class=HTMLResponse, include_in_schema=False, ) async def store_content_page_edit( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), page_id: int = Path(..., description="Content page ID"), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), @@ -148,11 +152,11 @@ async def store_content_page_edit( @router.get( - "/{store_code}/{slug}", response_class=HTMLResponse, include_in_schema=False + "/{slug}", response_class=HTMLResponse, include_in_schema=False ) async def store_content_page( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), slug: str = Path(..., description="Content page slug"), db: Session = Depends(get_db), ): diff --git a/app/modules/core/routes/pages/store.py b/app/modules/core/routes/pages/store.py index 84575bd0..a7e3011a 100644 --- a/app/modules/core/routes/pages/store.py +++ b/app/modules/core/routes/pages/store.py @@ -8,11 +8,15 @@ Store pages for core functionality: - Notifications """ -from fastapi import APIRouter, Depends, Path, Request +from fastapi import APIRouter, Depends, Request from fastapi.responses import HTMLResponse from sqlalchemy.orm import Session -from app.api.deps import get_current_store_from_cookie_or_header, get_db +from app.api.deps import ( + get_current_store_from_cookie_or_header, + get_db, + get_resolved_store_code, +) from app.modules.core.utils.page_context import get_store_context from app.modules.tenancy.models import User from app.templates_config import templates @@ -26,11 +30,11 @@ router = APIRouter() @router.get( - "/{store_code}/dashboard", response_class=HTMLResponse, include_in_schema=False + "/dashboard", response_class=HTMLResponse, include_in_schema=False ) async def store_dashboard_page( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): @@ -55,11 +59,11 @@ async def store_dashboard_page( @router.get( - "/{store_code}/media", response_class=HTMLResponse, include_in_schema=False + "/media", response_class=HTMLResponse, include_in_schema=False ) async def store_media_page( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): @@ -79,13 +83,13 @@ async def store_media_page( @router.get( - "/{store_code}/notifications", + "/notifications", response_class=HTMLResponse, include_in_schema=False, ) async def store_notifications_page( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): diff --git a/app/modules/core/static/store/js/init-alpine.js b/app/modules/core/static/store/js/init-alpine.js index 81dadf4b..5de4df15 100644 --- a/app/modules/core/static/store/js/init-alpine.js +++ b/app/modules/core/static/store/js/init-alpine.js @@ -74,8 +74,9 @@ function data() { ? segments[segments.length - 2] : last; - // Get store code from URL - if (segments[0] === 'store' && segments[1]) { + // Get store code from server-rendered value or URL fallback + this.storeCode = window.STORE_CODE || null; + if (!this.storeCode && segments[0] === 'store' && segments[1]) { this.storeCode = segments[1]; } @@ -281,14 +282,18 @@ function emailSettingsWarning() { storeCode: null, async init() { - // Get store code from URL - const path = window.location.pathname; - const segments = path.split('/').filter(Boolean); - if (segments[0] === 'store' && segments[1]) { - this.storeCode = segments[1]; + // Get store code from server-rendered value or URL fallback + this.storeCode = window.STORE_CODE || null; + if (!this.storeCode) { + const path = window.location.pathname; + const segments = path.split('/').filter(Boolean); + if (segments[0] === 'store' && segments[1]) { + this.storeCode = segments[1]; + } } // Skip if we're on the settings page (to avoid showing banner on config page) + const path = window.location.pathname; if (path.includes('/settings')) { this.loading = false; return; diff --git a/app/modules/customers/routes/api/storefront.py b/app/modules/customers/routes/api/storefront.py index e9b08396..5923af5f 100644 --- a/app/modules/customers/routes/api/storefront.py +++ b/app/modules/customers/routes/api/storefront.py @@ -262,7 +262,7 @@ def forgot_password(request: Request, email: str, db: Session = Depends(get_db)) scheme = "https" if should_use_secure_cookies() else "http" host = request.headers.get("host", "localhost") - reset_link = f"{scheme}://{host}/shop/account/reset-password?token={plaintext_token}" + reset_link = f"{scheme}://{host}/account/reset-password?token={plaintext_token}" email_service = EmailService(db) email_service.send_template( diff --git a/app/modules/customers/routes/pages/store.py b/app/modules/customers/routes/pages/store.py index bd811f20..e5776738 100644 --- a/app/modules/customers/routes/pages/store.py +++ b/app/modules/customers/routes/pages/store.py @@ -6,11 +6,15 @@ Store pages for customer management: - Customers list """ -from fastapi import APIRouter, Depends, Path, Request +from fastapi import APIRouter, Depends, Request from fastapi.responses import HTMLResponse from sqlalchemy.orm import Session -from app.api.deps import get_current_store_from_cookie_or_header, get_db +from app.api.deps import ( + get_current_store_from_cookie_or_header, + get_db, + get_resolved_store_code, +) from app.modules.core.utils.page_context import get_store_context from app.modules.tenancy.models import User from app.templates_config import templates @@ -24,11 +28,11 @@ router = APIRouter() @router.get( - "/{store_code}/customers", response_class=HTMLResponse, include_in_schema=False + "/customers", response_class=HTMLResponse, include_in_schema=False ) async def store_customers_page( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): diff --git a/app/modules/inventory/routes/pages/store.py b/app/modules/inventory/routes/pages/store.py index 42c37388..1efbb791 100644 --- a/app/modules/inventory/routes/pages/store.py +++ b/app/modules/inventory/routes/pages/store.py @@ -6,11 +6,15 @@ Store pages for inventory management: - Inventory list """ -from fastapi import APIRouter, Depends, Path, Request +from fastapi import APIRouter, Depends, Request from fastapi.responses import HTMLResponse from sqlalchemy.orm import Session -from app.api.deps import get_current_store_from_cookie_or_header, get_db +from app.api.deps import ( + get_current_store_from_cookie_or_header, + get_db, + get_resolved_store_code, +) from app.modules.core.utils.page_context import get_store_context from app.modules.tenancy.models import User from app.templates_config import templates @@ -24,11 +28,11 @@ router = APIRouter() @router.get( - "/{store_code}/inventory", response_class=HTMLResponse, include_in_schema=False + "/inventory", response_class=HTMLResponse, include_in_schema=False ) async def store_inventory_page( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): diff --git a/app/modules/loyalty/routes/pages/store.py b/app/modules/loyalty/routes/pages/store.py index af0fec08..6e024596 100644 --- a/app/modules/loyalty/routes/pages/store.py +++ b/app/modules/loyalty/routes/pages/store.py @@ -8,7 +8,7 @@ Store pages for: - Program settings - Stats dashboard -Routes follow the standard store convention: /{store_code}/loyalty/... +Routes follow the standard store convention: /loyalty/... so they match the menu URLs in definition.py. """ @@ -18,7 +18,11 @@ from fastapi import APIRouter, Depends, Path, Request from fastapi.responses import HTMLResponse, RedirectResponse from sqlalchemy.orm import Session -from app.api.deps import get_current_store_from_cookie_or_header, get_db +from app.api.deps import ( + get_current_store_from_cookie_or_header, + get_db, + get_resolved_store_code, +) from app.modules.core.services.platform_settings_service import ( platform_settings_service, ) @@ -86,12 +90,12 @@ def get_store_context( @router.get( - "/{store_code}/loyalty", + "/loyalty", response_class=RedirectResponse, include_in_schema=False, ) async def store_loyalty_root( - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), ): """ @@ -110,13 +114,13 @@ async def store_loyalty_root( @router.get( - "/{store_code}/loyalty/terminal", + "/loyalty/terminal", response_class=HTMLResponse, include_in_schema=False, ) async def store_loyalty_terminal( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): @@ -136,13 +140,13 @@ async def store_loyalty_terminal( @router.get( - "/{store_code}/loyalty/cards", + "/loyalty/cards", response_class=HTMLResponse, include_in_schema=False, ) async def store_loyalty_cards( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): @@ -157,13 +161,13 @@ async def store_loyalty_cards( @router.get( - "/{store_code}/loyalty/cards/{card_id}", + "/loyalty/cards/{card_id}", response_class=HTMLResponse, include_in_schema=False, ) async def store_loyalty_card_detail( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), card_id: int = Path(..., description="Loyalty card ID"), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), @@ -184,13 +188,13 @@ async def store_loyalty_card_detail( @router.get( - "/{store_code}/loyalty/stats", + "/loyalty/stats", response_class=HTMLResponse, include_in_schema=False, ) async def store_loyalty_stats( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): @@ -210,13 +214,13 @@ async def store_loyalty_stats( @router.get( - "/{store_code}/loyalty/enroll", + "/loyalty/enroll", response_class=HTMLResponse, include_in_schema=False, ) async def store_loyalty_enroll( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): diff --git a/app/modules/marketplace/routes/pages/store.py b/app/modules/marketplace/routes/pages/store.py index 7e675d0d..82d94a0b 100644 --- a/app/modules/marketplace/routes/pages/store.py +++ b/app/modules/marketplace/routes/pages/store.py @@ -8,11 +8,15 @@ Store pages for marketplace management: - Letzshop integration """ -from fastapi import APIRouter, Depends, Path, Request +from fastapi import APIRouter, Depends, Request from fastapi.responses import HTMLResponse, RedirectResponse from sqlalchemy.orm import Session -from app.api.deps import get_current_store_from_cookie_or_header, get_db +from app.api.deps import ( + get_current_store_from_cookie_or_header, + get_db, + get_resolved_store_code, +) from app.modules.core.utils.page_context import get_store_context from app.modules.marketplace.services.onboarding_service import OnboardingService from app.modules.service import module_service @@ -29,11 +33,11 @@ router = APIRouter() @router.get( - "/{store_code}/onboarding", response_class=HTMLResponse, include_in_schema=False + "/onboarding", response_class=HTMLResponse, include_in_schema=False ) async def store_onboarding_page( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): @@ -80,11 +84,11 @@ async def store_onboarding_page( @router.get( - "/{store_code}/marketplace", response_class=HTMLResponse, include_in_schema=False + "/marketplace", response_class=HTMLResponse, include_in_schema=False ) async def store_marketplace_page( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): @@ -112,11 +116,11 @@ async def store_marketplace_page( @router.get( - "/{store_code}/letzshop", response_class=HTMLResponse, include_in_schema=False + "/letzshop", response_class=HTMLResponse, include_in_schema=False ) async def store_letzshop_page( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): diff --git a/app/modules/marketplace/static/store/js/onboarding.js b/app/modules/marketplace/static/store/js/onboarding.js index 0140271f..533ee902 100644 --- a/app/modules/marketplace/static/store/js/onboarding.js +++ b/app/modules/marketplace/static/store/js/onboarding.js @@ -615,10 +615,10 @@ function storeOnboarding(initialLang = 'en') { async handleLogout() { onboardingLog.info('Logging out from onboarding...'); - // Get store code from URL + // Get store code from server-rendered value or URL fallback const path = window.location.pathname; const segments = path.split('/').filter(Boolean); - const storeCode = segments[0] === 'store' && segments[1] ? segments[1] : ''; + const storeCode = window.STORE_CODE || (segments[0] === 'store' && segments[1] ? segments[1] : ''); try { // Call logout API diff --git a/app/modules/messaging/routes/pages/store.py b/app/modules/messaging/routes/pages/store.py index 0d59ffb5..6ed71d4e 100644 --- a/app/modules/messaging/routes/pages/store.py +++ b/app/modules/messaging/routes/pages/store.py @@ -12,7 +12,11 @@ from fastapi import APIRouter, Depends, Path, Request from fastapi.responses import HTMLResponse from sqlalchemy.orm import Session -from app.api.deps import get_current_store_from_cookie_or_header, get_db +from app.api.deps import ( + get_current_store_from_cookie_or_header, + get_db, + get_resolved_store_code, +) from app.modules.core.utils.page_context import get_store_context from app.modules.tenancy.models import User from app.templates_config import templates @@ -26,11 +30,11 @@ router = APIRouter() @router.get( - "/{store_code}/messages", response_class=HTMLResponse, include_in_schema=False + "/messages", response_class=HTMLResponse, include_in_schema=False ) async def store_messages_page( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): @@ -45,13 +49,13 @@ async def store_messages_page( @router.get( - "/{store_code}/messages/{conversation_id}", + "/messages/{conversation_id}", response_class=HTMLResponse, include_in_schema=False, ) async def store_message_detail_page( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), conversation_id: int = Path(..., description="Conversation ID"), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), @@ -74,13 +78,13 @@ async def store_message_detail_page( @router.get( - "/{store_code}/email-templates", + "/email-templates", response_class=HTMLResponse, include_in_schema=False, ) async def store_email_templates_page( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): diff --git a/app/modules/orders/routes/pages/store.py b/app/modules/orders/routes/pages/store.py index 0634ece2..76862cc4 100644 --- a/app/modules/orders/routes/pages/store.py +++ b/app/modules/orders/routes/pages/store.py @@ -11,7 +11,11 @@ from fastapi import APIRouter, Depends, Path, Request from fastapi.responses import HTMLResponse from sqlalchemy.orm import Session -from app.api.deps import get_current_store_from_cookie_or_header, get_db +from app.api.deps import ( + get_current_store_from_cookie_or_header, + get_db, + get_resolved_store_code, +) from app.modules.core.utils.page_context import get_store_context from app.modules.tenancy.models import User from app.templates_config import templates @@ -25,11 +29,11 @@ router = APIRouter() @router.get( - "/{store_code}/orders", response_class=HTMLResponse, include_in_schema=False + "/orders", response_class=HTMLResponse, include_in_schema=False ) async def store_orders_page( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): @@ -44,13 +48,13 @@ async def store_orders_page( @router.get( - "/{store_code}/orders/{order_id}", + "/orders/{order_id}", response_class=HTMLResponse, include_in_schema=False, ) async def store_order_detail_page( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), order_id: int = Path(..., description="Order ID"), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), diff --git a/app/modules/tenancy/routes/pages/store.py b/app/modules/tenancy/routes/pages/store.py index da7d74fd..bbc04eaf 100644 --- a/app/modules/tenancy/routes/pages/store.py +++ b/app/modules/tenancy/routes/pages/store.py @@ -10,7 +10,7 @@ Store pages for authentication and account management: - Settings """ -from fastapi import APIRouter, Depends, Path, Request +from fastapi import APIRouter, Depends, Request from fastapi.responses import HTMLResponse, RedirectResponse from sqlalchemy.orm import Session @@ -18,6 +18,7 @@ from app.api.deps import ( get_current_store_from_cookie_or_header, get_current_store_optional, get_db, + get_resolved_store_code, ) from app.modules.core.utils.page_context import get_store_context from app.modules.tenancy.models import User @@ -31,24 +32,13 @@ router = APIRouter() # ============================================================================ -@router.get("/{store_code}", response_class=RedirectResponse, include_in_schema=False) -async def store_root_no_slash(store_code: str = Path(..., description="Store code")): - """ - Redirect /store/{code} (no trailing slash) to login page. - Handles requests without trailing slash. - """ - return RedirectResponse(url=f"/store/{store_code}/login", status_code=302) - - -@router.get( - "/{store_code}/", response_class=RedirectResponse, include_in_schema=False -) +@router.get("/", response_class=RedirectResponse, include_in_schema=False) async def store_root( - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User | None = Depends(get_current_store_optional), ): """ - Redirect /store/{code}/ based on authentication status. + Redirect /store/ or /store/{code}/ based on authentication status. - Authenticated store users -> /store/{code}/dashboard - Unauthenticated users -> /store/{code}/login @@ -62,11 +52,11 @@ async def store_root( @router.get( - "/{store_code}/login", response_class=HTMLResponse, include_in_schema=False + "/login", response_class=HTMLResponse, include_in_schema=False ) async def store_login_page( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User | None = Depends(get_current_store_optional), ): """ @@ -74,11 +64,6 @@ async def store_login_page( If user is already authenticated as store, redirect to dashboard. Otherwise, show login form. - - JavaScript will: - - Load store info via API - - Handle login form submission - - Redirect to dashboard on success """ if current_user: return RedirectResponse( @@ -100,11 +85,11 @@ async def store_login_page( @router.get( - "/{store_code}/team", response_class=HTMLResponse, include_in_schema=False + "/team", response_class=HTMLResponse, include_in_schema=False ) async def store_team_page( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): @@ -119,11 +104,11 @@ async def store_team_page( @router.get( - "/{store_code}/profile", response_class=HTMLResponse, include_in_schema=False + "/profile", response_class=HTMLResponse, include_in_schema=False ) async def store_profile_page( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): @@ -138,11 +123,11 @@ async def store_profile_page( @router.get( - "/{store_code}/settings", response_class=HTMLResponse, include_in_schema=False + "/settings", response_class=HTMLResponse, include_in_schema=False ) async def store_settings_page( request: Request, - store_code: str = Path(..., description="Store code"), + store_code: str = Depends(get_resolved_store_code), current_user: User = Depends(get_current_store_from_cookie_or_header), db: Session = Depends(get_db), ): diff --git a/app/modules/tenancy/static/admin/js/store-detail.js b/app/modules/tenancy/static/admin/js/store-detail.js index d30ad8a3..9b49fdff 100644 --- a/app/modules/tenancy/static/admin/js/store-detail.js +++ b/app/modules/tenancy/static/admin/js/store-detail.js @@ -20,6 +20,13 @@ function adminStoreDetail() { showDeleteStoreModal: false, showDeleteStoreFinalModal: false, + // Domain management state + domains: [], + domainsLoading: false, + showAddDomainForm: false, + domainSaving: false, + newDomain: { domain: '', platform_id: '' }, + // Initialize async init() { // Load i18n translations @@ -42,9 +49,12 @@ function adminStoreDetail() { this.storeCode = match[1]; detailLog.info('Viewing store:', this.storeCode); await this.loadStore(); - // Load subscription after store is loaded + // Load subscription and domains after store is loaded if (this.store?.id) { - await this.loadSubscriptions(); + await Promise.all([ + this.loadSubscriptions(), + this.loadDomains(), + ]); } } else { detailLog.error('No store code in URL'); @@ -180,6 +190,90 @@ function adminStoreDetail() { } }, + // ==================================================================== + // DOMAIN MANAGEMENT + // ==================================================================== + + async loadDomains() { + if (!this.store?.id) return; + this.domainsLoading = true; + try { + const url = `/admin/stores/${this.store.id}/domains`; + const response = await apiClient.get(url); + this.domains = response.domains || []; + detailLog.info('Domains loaded:', this.domains.length); + } catch (error) { + if (error.status === 404) { + this.domains = []; + } else { + detailLog.warn('Failed to load domains:', error.message); + } + } finally { + this.domainsLoading = false; + } + }, + + async addDomain() { + if (!this.newDomain.domain || this.domainSaving) return; + this.domainSaving = true; + try { + const payload = { domain: this.newDomain.domain }; + if (this.newDomain.platform_id) { + payload.platform_id = parseInt(this.newDomain.platform_id); + } + await apiClient.post(`/admin/stores/${this.store.id}/domains`, payload); + Utils.showToast('Domain added successfully', 'success'); + this.showAddDomainForm = false; + this.newDomain = { domain: '', platform_id: '' }; + await this.loadDomains(); + } catch (error) { + Utils.showToast(error.message || 'Failed to add domain', 'error'); + } finally { + this.domainSaving = false; + } + }, + + async verifyDomain(domainId) { + try { + const response = await apiClient.post(`/admin/stores/domains/${domainId}/verify`); + Utils.showToast(response.message || 'Domain verified!', 'success'); + await this.loadDomains(); + } catch (error) { + Utils.showToast(error.message || 'Verification failed — check DNS records', 'error'); + } + }, + + async toggleDomainActive(domainId, activate) { + try { + await apiClient.put(`/admin/stores/domains/${domainId}`, { is_active: activate }); + Utils.showToast(activate ? 'Domain activated' : 'Domain deactivated', 'success'); + await this.loadDomains(); + } catch (error) { + Utils.showToast(error.message || 'Failed to update domain', 'error'); + } + }, + + async setDomainPrimary(domainId) { + try { + await apiClient.put(`/admin/stores/domains/${domainId}`, { is_primary: true }); + Utils.showToast('Domain set as primary', 'success'); + await this.loadDomains(); + } catch (error) { + Utils.showToast(error.message || 'Failed to set primary domain', 'error'); + } + }, + + async deleteDomain(domainId, domainName) { + if (!confirm(`Delete domain "${domainName}"? This cannot be undone.`)) return; + try { + await apiClient.delete(`/admin/stores/domains/${domainId}`); + Utils.showToast('Domain deleted', 'success'); + await this.loadDomains(); + } catch (error) { + Utils.showToast(error.message || 'Failed to delete domain', 'error'); + } + }, + // Refresh store data async refresh() { detailLog.info('=== STORE REFRESH TRIGGERED ==='); diff --git a/app/modules/tenancy/templates/tenancy/admin/store-detail.html b/app/modules/tenancy/templates/tenancy/admin/store-detail.html index 0fad2509..12f88191 100644 --- a/app/modules/tenancy/templates/tenancy/admin/store-detail.html +++ b/app/modules/tenancy/templates/tenancy/admin/store-detail.html @@ -356,6 +356,142 @@ + +
+
+

+ Custom Domains +

+ +
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+

Loading domains...

+
+ + +
+ +
+ + +
+

No custom domains configured.

+
+
+

diff --git a/app/templates/store/base.html b/app/templates/store/base.html index 851a5628..73e26d98 100644 --- a/app/templates/store/base.html +++ b/app/templates/store/base.html @@ -56,6 +56,7 @@