docs: update routing docs and seed script for production routing changes
Some checks failed
CI / ruff (push) Successful in 9s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled

Reflect the production routing refactor (ce5b54f): document store dashboard
double-mounting, per-platform subdomain overrides via StorePlatform.custom_subdomain,
get_resolved_store_code dependency, and /merchants/ reserved path. Update seed
script to populate custom_subdomain and StoreDomain.platform_id for demo data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 11:44:43 +01:00
parent ce5b54f27b
commit d480b59df4
5 changed files with 238 additions and 51 deletions

View File

@@ -93,14 +93,18 @@ INFO Response: 200 for GET /admin/dashboard (0.143s)
**What it does**: **What it does**:
- Detects store from: - Detects store from:
- Custom domain (e.g., `customdomain.com`) - Custom domain (e.g., `customdomain.com`) — via `StoreDomain` lookup (optionally scoped to a platform via `StoreDomain.platform_id`)
- Subdomain (e.g., `store1.platform.com`) - Subdomain with two-step lookup:
1. `StorePlatform.custom_subdomain` — per-platform subdomain overrides (e.g., `wizatech-rewards.rewardflow.lu`)
2. `Store.subdomain` — standard subdomain fallback (e.g., `wizatech.omsflow.lu`)
- Path prefix (e.g., `/store/store1/` or `/stores/store1/`) - Path prefix (e.g., `/store/store1/` or `/stores/store1/`)
- Queries database to find store by domain or code - Queries database to find store by domain or code
- Injects store object into `request.state.store` - Injects store object and platform into `request.state.store`
- Extracts "clean path" (path without store prefix) - Extracts "clean path" (path without store prefix)
- Sets `request.state.clean_path` for routing - Sets `request.state.clean_path` for routing
**Reserved paths**: When a storefront domain is detected, certain paths are excluded from the `/storefront/` rewrite: `/store/`, `/admin/`, `/api/`, `/static/`, `/storefront/`, `/health`, `/docs`, `/redoc`, `/media/`, `/assets/`, and `/merchants/`. This ensures that staff dashboards, APIs, and merchant-level routes are not incorrectly routed to storefront pages.
**Example**: **Example**:
``` ```
Request: https://orion.platform.com/storefront/products Request: https://orion.platform.com/storefront/products

View File

@@ -82,21 +82,27 @@ logger.info(f"Request: GET /storefront/products from 192.168.1.100")
- Queries database for store - Queries database for store
- Extracts clean path - Extracts clean path
**Example Processing** (Subdomain Mode): **Example Processing** (Subdomain Mode — Two-Step Lookup):
```python ```python
# Input # Input
host = "orion.platform.com" host = "wizatech-rewards.rewardflow.lu"
path = "/storefront/products" path = "/storefront/products"
# Detection logic # Detection logic — two-step subdomain lookup
if host != settings.platform_domain: subdomain = host.split('.')[0] # "wizatech-rewards"
# Subdomain detected
store_code = host.split('.')[0] # "orion"
# Query database # Step 1: Check per-platform custom subdomain (StorePlatform.custom_subdomain)
store_platform = db.query(StorePlatform).filter(
StorePlatform.custom_subdomain == subdomain
).first()
if store_platform:
store = store_platform.store
else:
# Step 2: Fall back to standard Store.subdomain
store = db.query(Store).filter( store = db.query(Store).filter(
Store.code == store_code Store.subdomain == subdomain
).first() ).first()
# Set request state # Set request state
@@ -116,23 +122,32 @@ request.state.clean_path = "/storefront/products"
**What happens**: **What happens**:
- FastAPI matches the request path against registered routes - FastAPI matches the request path against registered routes
- For path-based development mode, routes are registered with two prefixes: - Both storefront and store dashboard routes are registered with two prefixes each:
- `/storefront/*` for subdomain/custom domain - Storefront: `/storefront/*` and `/storefront/{store_code}/*`
- `/stores/{store_code}/storefront/*` for path-based development - Store dashboard: `/store/*` and `/store/{store_code}/*`
**Example** (Path-Based Mode): **Example** (Path-Based Mode):
```python ```python
# In main.py - Double router mounting # In main.py - Double router mounting for storefront
app.include_router(storefront_pages.router, prefix="/storefront") app.include_router(storefront_pages.router, prefix="/storefront")
app.include_router(storefront_pages.router, prefix="/stores/{store_code}/storefront") app.include_router(storefront_pages.router, prefix="/storefront/{store_code}")
# Request: /stores/ORION/storefront/products # In main.py - Double router mounting for store dashboard
# Matches: Second router (/stores/{store_code}/storefront) app.include_router(store_pages.router, prefix="/store")
# Route: @router.get("/products") app.include_router(store_pages.router, prefix="/store/{store_code}")
# store_code available as path parameter = "ORION"
# Storefront request: /storefront/ACME/products
# Matches: Second storefront router (/storefront/{store_code})
# store_code = "ACME"
# Store dashboard request: /store/ACME/dashboard
# Matches: Second store router (/store/{store_code})
# store_code = "ACME"
``` ```
Route handlers use the `get_resolved_store_code` dependency to transparently resolve the store code from either the path parameter (dev mode) or middleware state (production subdomain/custom domain).
**Note:** Previous implementations used `PathRewriteMiddleware` to rewrite paths. This has been replaced with FastAPI's native routing via double router mounting. **Note:** Previous implementations used `PathRewriteMiddleware` to rewrite paths. This has been replaced with FastAPI's native routing via double router mounting.
**Request State After**: No changes to state, but internal path updated **Request State After**: No changes to state, but internal path updated

View File

@@ -13,11 +13,16 @@ There are three ways depending on the deployment mode:
https://STORE_SUBDOMAIN.platform-domain.lu/ https://STORE_SUBDOMAIN.platform-domain.lu/
https://STORE_SUBDOMAIN.platform-domain.lu/products https://STORE_SUBDOMAIN.platform-domain.lu/products
https://STORE_SUBDOMAIN.platform-domain.lu/cart https://STORE_SUBDOMAIN.platform-domain.lu/cart
https://STORE_SUBDOMAIN.platform-domain.lu/store/dashboard (staff)
Example: Example:
https://acme.omsflow.lu/ https://acme.omsflow.lu/
https://acme.omsflow.lu/products https://acme.omsflow.lu/products
https://techpro.rewardflow.lu/account/dashboard https://techpro.rewardflow.lu/account/dashboard
https://acme.omsflow.lu/store/dashboard (staff)
Per-platform subdomain override:
https://wizatech-rewards.rewardflow.lu/ (same store as wizatech.omsflow.lu)
``` ```
### 2. **CUSTOM DOMAIN MODE** (Production - Premium) ### 2. **CUSTOM DOMAIN MODE** (Production - Premium)
@@ -227,7 +232,9 @@ Request arrives
1. Customer visits `https://acme.omsflow.lu/products` 1. Customer visits `https://acme.omsflow.lu/products`
2. `PlatformContextMiddleware` detects subdomain `"acme"`, resolves platform from root domain `omsflow.lu` 2. `PlatformContextMiddleware` detects subdomain `"acme"`, resolves platform from root domain `omsflow.lu`
3. Middleware rewrites path: `/products``/storefront/products` (internal) 3. Middleware rewrites path: `/products``/storefront/products` (internal)
4. `store_context_middleware` detects subdomain, queries: `SELECT * FROM stores WHERE subdomain = 'acme'` 4. `store_context_middleware` performs two-step subdomain lookup:
- First: `SELECT * FROM store_platforms WHERE custom_subdomain = 'acme'` (per-platform override)
- Fallback: `SELECT * FROM stores WHERE subdomain = 'acme'` (standard subdomain)
5. Sets `request.state.store = Store(ACME Store)` 5. Sets `request.state.store = Store(ACME Store)`
6. `frontend_type_middleware` detects STOREFRONT from `/storefront` path prefix 6. `frontend_type_middleware` detects STOREFRONT from `/storefront` path prefix
7. `theme_context_middleware` loads ACME's theme 7. `theme_context_middleware` loads ACME's theme
@@ -310,6 +317,7 @@ id | store_id | domain | is_active | is_verified
### Subdomain/Custom Domain (PRODUCTION) ### Subdomain/Custom Domain (PRODUCTION)
``` ```
Storefront (customer-facing):
https://acme.omsflow.lu/ → Homepage https://acme.omsflow.lu/ → Homepage
https://acme.omsflow.lu/products → Product Catalog https://acme.omsflow.lu/products → Product Catalog
https://acme.omsflow.lu/products/123 → Product Detail https://acme.omsflow.lu/products/123 → Product Detail
@@ -320,7 +328,16 @@ https://acme.omsflow.lu/search?q=laptop → Search Results
https://acme.omsflow.lu/account/login → Customer Login https://acme.omsflow.lu/account/login → Customer Login
https://acme.omsflow.lu/account/dashboard → Account Dashboard (Auth Required) https://acme.omsflow.lu/account/dashboard → Account Dashboard (Auth Required)
https://acme.omsflow.lu/account/orders → Order History (Auth Required) https://acme.omsflow.lu/account/orders → Order History (Auth Required)
Store Dashboard (staff):
https://acme.omsflow.lu/store/dashboard → Staff Dashboard (Auth Required) https://acme.omsflow.lu/store/dashboard → Staff Dashboard (Auth Required)
https://acme.omsflow.lu/store/products → Manage Products
https://acme.omsflow.lu/store/orders → Manage Orders
https://acme.omsflow.lu/store/login → Staff Login
Per-platform subdomain override:
https://wizatech-rewards.rewardflow.lu/ → Same store as wizatech.omsflow.lu
https://wizatech-rewards.rewardflow.lu/store/dashboard → Staff dashboard on loyalty platform
``` ```
Note: In production, the root path `/` is the storefront. The `PlatformContextMiddleware` Note: In production, the root path `/` is the storefront. The `PlatformContextMiddleware`
@@ -524,28 +541,46 @@ In Jinja2 template:
**Current Solution: Double Router Mounting + Path Rewriting** **Current Solution: Double Router Mounting + Path Rewriting**
The application handles routing by registering storefront routes **twice** with different prefixes: The application handles routing by registering both storefront and store dashboard routes **twice** with different prefixes:
```python ```python
# In main.py # In main.py — Storefront routes (customer-facing)
app.include_router(storefront_pages.router, prefix="/storefront") app.include_router(storefront_pages.router, prefix="/storefront")
app.include_router(storefront_pages.router, prefix="/storefront/{store_code}") app.include_router(storefront_pages.router, prefix="/storefront/{store_code}")
# In main.py — Store dashboard routes (staff management)
app.include_router(store_pages.router, prefix="/store")
app.include_router(store_pages.router, prefix="/store/{store_code}")
``` ```
**How This Works:** **How This Works:**
1. **For Subdomain/Custom Domain Mode (Production):** 1. **For Subdomain/Custom Domain Mode (Production):**
- URL: `https://acme.omsflow.lu/products` - Storefront: `https://acme.omsflow.lu/products` → path rewritten to `/storefront/products` → matches first storefront mount
- `PlatformContextMiddleware` detects subdomain, rewrites path: `/products``/storefront/products` - Dashboard: `https://acme.omsflow.lu/store/dashboard` → matches first store mount at `/store`
- Matches: First router with `/storefront` prefix - Store resolved by middleware via `request.state.store`
- Route: `@router.get("/products")` → Full path: `/storefront/products`
2. **For Path-Based Development Mode:** 2. **For Path-Based Development Mode:**
- URL: `http://localhost:8000/platforms/oms/storefront/ACME/products` - Storefront: `http://localhost:8000/platforms/oms/storefront/ACME/products` → matches second storefront mount at `/storefront/{store_code}`
- Platform middleware strips `/platforms/oms/` prefix, sets platform context - Dashboard: `http://localhost:8000/platforms/oms/store/ACME/dashboard` → matches second store mount at `/store/{store_code}`
- Matches: Second router with `/storefront/{store_code}` prefix - `store_code` available as path parameter
- Route: `@router.get("/products")` → Full path: `/storefront/{store_code}/products`
- Bonus: `store_code` available as path parameter! ### `get_resolved_store_code` Dependency
Route handlers use the `get_resolved_store_code` dependency to transparently obtain the store code regardless of deployment mode:
```python
async def get_resolved_store_code(request: Request) -> str:
# 1. Path parameter from double-mount (/store/{store_code}/...)
store_code = request.path_params.get("store_code")
if store_code:
return store_code
# 2. 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")
```
**Benefits:** **Benefits:**
- ✅ Clean separation: `/storefront/` = customer, `/store/` = staff - ✅ Clean separation: `/storefront/` = customer, `/store/` = staff
@@ -553,6 +588,43 @@ app.include_router(storefront_pages.router, prefix="/storefront/{store_code}")
- ✅ No `/storefront/` prefix visible to production customers - ✅ No `/storefront/` prefix visible to production customers
- ✅ Internal path rewriting handled by ASGI middleware - ✅ Internal path rewriting handled by ASGI middleware
- ✅ Both deployment modes supported cleanly - ✅ Both deployment modes supported cleanly
-`get_resolved_store_code` abstracts store resolution for handlers
---
## Per-Platform Subdomain Overrides
Stores that are active on multiple platforms can have a **custom subdomain** per platform via `StorePlatform.custom_subdomain`. This allows a single store to appear under different subdomains on different platform domains.
### How It Works
```
Store: WizaTech (subdomain: "wizatech")
├── OMS platform → wizatech.omsflow.lu (uses Store.subdomain)
└── Loyalty platform → wizatech-rewards.rewardflow.lu (uses StorePlatform.custom_subdomain)
```
**Database:**
```sql
-- stores table
id | store_code | subdomain
1 | WIZATECH | wizatech
-- store_platforms table
id | store_id | platform_id | custom_subdomain
1 | 1 | 1 (oms) | NULL -- uses store.subdomain = "wizatech"
2 | 1 | 2 (loyalty) | wizatech-rewards -- overrides to "wizatech-rewards"
```
**Resolution order** (in `store_context_middleware`):
1. Check `StorePlatform.custom_subdomain` for a match on the current platform
2. Fall back to `Store.subdomain` for the standard lookup
### Use Cases
- **Brand differentiation**: A store selling electronics via OMS and running a loyalty program wants different branding per platform
- **Subdomain conflicts**: Two unrelated stores might use the same subdomain on different platforms — custom subdomains resolve the collision
- **Marketing**: Platform-specific landing URLs for campaigns (e.g., `wizatech-rewards.rewardflow.lu` for loyalty-specific promotions)
--- ---
@@ -592,5 +664,5 @@ Set-Cookie: customer_token=eyJ...; Path=/storefront; HttpOnly; SameSite=Lax
--- ---
Generated: January 30, 2026 Generated: February 26, 2026
Orion Version: Current Development Orion Version: Current Development

View File

@@ -140,11 +140,19 @@ This runs:
### Stores Created ### Stores Created
| Code | Name | Subdomain | Theme | Custom Domain | | Code | Name | Subdomain | Theme | Custom Domain | Notes |
|------|------|-----------|-------|---------------| |------|------|-----------|-------|---------------|-------|
| ORION | Orion | orion | modern | orion.shop | | WIZATECH | WizaTech | wizatech | modern | wizatech.shop (OMS) | `custom_subdomain="wizatech-rewards"` on Loyalty `StorePlatform` |
| FASHIONHUB | Fashion Hub | fashionhub | vibrant | fashionhub.store | | WIZAGADGETS | WizaGadgets | wizagadgets | modern | (none) | |
| BOOKSTORE | The Book Store | bookstore | classic | (none) | | WIZAHOME | WizaHome | wizahome | classic | (none) | |
| FASHIONHUB | Fashion Hub | fashionhub | vibrant | fashionhub.store (Loyalty) | |
| FASHIONOUTLET | Fashion Outlet | fashionoutlet | vibrant | (none) | |
| BOOKSTORE | The Book Store | bookstore | classic | (none) | |
| BOOKDIGITAL | BookWorld Digital | bookdigital | modern | (none) | |
**Per-platform subdomain demo:** WizaTech has both OMS and Loyalty subscriptions. On the Loyalty platform, its `StorePlatform.custom_subdomain` is set to `"wizatech-rewards"`, meaning `wizatech-rewards.rewardflow.lu` resolves to the same store as `wizatech.omsflow.lu`.
**Multi-platform custom domains:** `StoreDomain` records now include a `platform_id` linking the domain to a specific platform. For example, `wizatech.shop` is linked to the OMS platform and `fashionhub.store` is linked to the Loyalty platform.
### Products Created ### Products Created
@@ -217,15 +225,15 @@ python scripts/seed_database.py [--reset] [--minimal]
- Username: `admin` - Username: `admin`
- Password: `admin123` - Password: `admin123`
### Store Storefronts ### Store Storefronts (Development)
- ORION: `http://localhost:8000/storefront/ORION` - WIZATECH (OMS): `http://localhost:8000/platforms/oms/storefront/WIZATECH/`
- FASHIONHUB: `http://localhost:8000/storefront/FASHIONHUB` - WIZATECH (Loyalty): `http://localhost:8000/platforms/loyalty/storefront/WIZATECH/`
- BOOKSTORE: `http://localhost:8000/storefront/BOOKSTORE` - FASHIONHUB: `http://localhost:8000/platforms/loyalty/storefront/FASHIONHUB/`
- BOOKSTORE: `http://localhost:8000/platforms/oms/storefront/BOOKSTORE/`
### Theme Editors ### Store Dashboards (Development)
- ORION Theme: `http://localhost:8000/admin/stores/ORION/theme` - WIZATECH: `http://localhost:8000/platforms/oms/store/WIZATECH/dashboard`
- FASHIONHUB Theme: `http://localhost:8000/admin/stores/FASHIONHUB/theme` - FASHIONHUB: `http://localhost:8000/platforms/loyalty/store/FASHIONHUB/dashboard`
- BOOKSTORE Theme: `http://localhost:8000/admin/stores/BOOKSTORE/theme`
## Example Output ## Example Output

View File

@@ -152,6 +152,10 @@ DEMO_STORES = [
"description": "Premium electronics and gadgets marketplace", "description": "Premium electronics and gadgets marketplace",
"theme_preset": "modern", "theme_preset": "modern",
"custom_domain": "wizatech.shop", "custom_domain": "wizatech.shop",
"custom_domain_platform": "oms", # Link domain to OMS platform
"platform_subdomains": { # Per-platform subdomain overrides
"loyalty": "wizatech-rewards", # wizatech-rewards.rewardflow.lu
},
}, },
{ {
"merchant_index": 0, # WizaCorp "merchant_index": 0, # WizaCorp
@@ -179,6 +183,7 @@ DEMO_STORES = [
"description": "Trendy clothing and accessories", "description": "Trendy clothing and accessories",
"theme_preset": "vibrant", "theme_preset": "vibrant",
"custom_domain": "fashionhub.store", "custom_domain": "fashionhub.store",
"custom_domain_platform": "loyalty", # Link domain to Loyalty platform
}, },
{ {
"merchant_index": 1, # Fashion Group "merchant_index": 1, # Fashion Group
@@ -799,12 +804,28 @@ def create_demo_stores(
) )
).all() ).all()
# Build platform code→id lookup for this store's custom subdomain config
from app.modules.tenancy.models import Platform
platform_code_map = {}
if store_data.get("platform_subdomains") or store_data.get("custom_domain_platform"):
platform_rows = db.execute(select(Platform.id, Platform.code)).all()
platform_code_map = {code: pid for pid, code in platform_rows}
for i, (platform_id,) in enumerate(merchant_subs): for i, (platform_id,) in enumerate(merchant_subs):
# Per-platform subdomain override for multi-platform stores
# Config uses platform codes; resolve to IDs
custom_sub = None
for pcode, subdomain_val in store_data.get("platform_subdomains", {}).items():
if platform_code_map.get(pcode) == platform_id:
custom_sub = subdomain_val
break
sp = StorePlatform( sp = StorePlatform(
store_id=store.id, store_id=store.id,
platform_id=platform_id, platform_id=platform_id,
is_active=True, is_active=True,
is_primary=(i == 0), is_primary=(i == 0),
custom_subdomain=custom_sub,
) )
db.add(sp) db.add(sp)
@@ -814,6 +835,9 @@ def create_demo_stores(
f" Linked to {len(merchant_subs)} platform(s): " f" Linked to {len(merchant_subs)} platform(s): "
f"{[pid for (pid,) in merchant_subs]}" f"{[pid for (pid,) in merchant_subs]}"
) )
# Report custom subdomains if any
for pcode, subdomain_val in store_data.get("platform_subdomains", {}).items():
print_success(f" Custom subdomain on {pcode}: {subdomain_val}")
# Owner relationship is via Merchant.owner_user_id — no StoreUser needed # Owner relationship is via Merchant.owner_user_id — no StoreUser needed
@@ -839,11 +863,15 @@ def create_demo_stores(
# Create custom domain if specified # Create custom domain if specified
if store_data.get("custom_domain"): if store_data.get("custom_domain"):
# Resolve platform_id from platform code (if specified)
domain_platform_code = store_data.get("custom_domain_platform")
domain_platform_id = platform_code_map.get(domain_platform_code) if domain_platform_code else None
domain = StoreDomain( domain = StoreDomain(
store_id=store.id, store_id=store.id,
domain=store_data[ domain=store_data[
"custom_domain" "custom_domain"
], # ✅ Field is 'domain', not 'domain_name' ], # ✅ Field is 'domain', not 'domain_name'
platform_id=domain_platform_id,
is_verified=True, # Auto-verified for demo is_verified=True, # Auto-verified for demo
is_primary=True, is_primary=True,
verification_token=None, verification_token=None,
@@ -1290,6 +1318,20 @@ def print_summary(db: Session):
print(f"\n {store.name} ({store.store_code})") print(f"\n {store.name} ({store.store_code})")
print(f" Subdomain: {store.subdomain}.{settings.platform_domain}") print(f" Subdomain: {store.subdomain}.{settings.platform_domain}")
# Show per-platform custom subdomains
from app.modules.tenancy.models import Platform
store_platforms = (
db.query(StorePlatform, Platform)
.join(Platform, Platform.id == StorePlatform.platform_id)
.filter(StorePlatform.store_id == store.id)
.all()
)
for sp, platform in store_platforms:
if sp.custom_subdomain:
pdomain = getattr(platform, "domain", platform.code)
print(f" [{platform.code}] Custom subdomain: {sp.custom_subdomain}.{pdomain}")
# Query custom domains separately # Query custom domains separately
custom_domain = ( custom_domain = (
db.query(StoreDomain) db.query(StoreDomain)
@@ -1305,7 +1347,12 @@ def print_summary(db: Session):
or getattr(custom_domain, "name", None) or getattr(custom_domain, "name", None)
) )
if domain_value: if domain_value:
print(f" Custom: {domain_value}") platform_note = ""
if custom_domain.platform_id:
dp = db.query(Platform).filter(Platform.id == custom_domain.platform_id).first()
if dp:
platform_note = f" (linked to {dp.code})"
print(f" Custom: {domain_value}{platform_note}")
print(f" Status: {'✓ Active' if store.is_active else '✗ Inactive'}") print(f" Status: {'✓ Active' if store.is_active else '✗ Inactive'}")
@@ -1375,6 +1422,25 @@ def print_summary(db: Session):
port = settings.api_port port = settings.api_port
base = f"http://localhost:{port}" base = f"http://localhost:{port}"
# Build store→platform details (including custom subdomains) for production URLs
from app.modules.tenancy.models import Platform
store_platform_details: dict[int, list[dict]] = {}
sp_detail_rows = db.execute(
select(
StorePlatform.store_id,
Platform.code,
Platform.domain,
StorePlatform.custom_subdomain,
).join(Platform, Platform.id == StorePlatform.platform_id)
.where(StorePlatform.is_active == True) # noqa: E712
.order_by(StorePlatform.store_id, StorePlatform.is_primary.desc())
).all()
for store_id, pcode, pdomain, custom_sub in sp_detail_rows:
store_platform_details.setdefault(store_id, []).append({
"code": pcode, "domain": pdomain, "custom_subdomain": custom_sub,
})
print("\n🏪 Store Access (Development):") print("\n🏪 Store Access (Development):")
print("" * 70) print("" * 70)
for store in stores: for store in stores:
@@ -1388,6 +1454,28 @@ def print_summary(db: Session):
print(f" [{pc}] Staff login: {base}/platforms/{pc}/store/{store.store_code}/login") print(f" [{pc}] Staff login: {base}/platforms/{pc}/store/{store.store_code}/login")
else: else:
print(" (!) No platform assigned") print(" (!) No platform assigned")
print("\n🌐 Store Access (Production-style):")
print("" * 70)
for store in stores:
details = store_platform_details.get(store.id, [])
print(f" {store.name} ({store.store_code}):")
for d in details:
subdomain = d["custom_subdomain"] or store.subdomain
pdomain = d["domain"] or f"{d['code']}.example.com"
suffix = " (custom subdomain)" if d["custom_subdomain"] else ""
print(f" [{d['code']}] Storefront: https://{subdomain}.{pdomain}/{suffix}")
print(f" [{d['code']}] Dashboard: https://{subdomain}.{pdomain}/store/dashboard")
# Show custom domain if any
custom_domain = (
db.query(StoreDomain)
.filter(StoreDomain.store_id == store.id, StoreDomain.is_active == True)
.first()
)
if custom_domain:
dv = getattr(custom_domain, "domain", None)
if dv:
print(f" [custom] Storefront: https://{dv}/")
print() print()
print("⚠️ ALL DEMO CREDENTIALS ARE INSECURE - For development only!") print("⚠️ ALL DEMO CREDENTIALS ARE INSECURE - For development only!")