docs: update routing docs and seed script for production routing changes
Some checks failed
Some checks failed
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:
@@ -93,14 +93,18 @@ INFO Response: 200 for GET /admin/dashboard (0.143s)
|
||||
|
||||
**What it does**:
|
||||
- Detects store from:
|
||||
- Custom domain (e.g., `customdomain.com`)
|
||||
- Subdomain (e.g., `store1.platform.com`)
|
||||
- Custom domain (e.g., `customdomain.com`) — via `StoreDomain` lookup (optionally scoped to a platform via `StoreDomain.platform_id`)
|
||||
- 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/`)
|
||||
- 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)
|
||||
- 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**:
|
||||
```
|
||||
Request: https://orion.platform.com/storefront/products
|
||||
|
||||
@@ -82,27 +82,33 @@ logger.info(f"Request: GET /storefront/products from 192.168.1.100")
|
||||
- Queries database for store
|
||||
- Extracts clean path
|
||||
|
||||
**Example Processing** (Subdomain Mode):
|
||||
**Example Processing** (Subdomain Mode — Two-Step Lookup):
|
||||
|
||||
```python
|
||||
# Input
|
||||
host = "orion.platform.com"
|
||||
host = "wizatech-rewards.rewardflow.lu"
|
||||
path = "/storefront/products"
|
||||
|
||||
# Detection logic
|
||||
if host != settings.platform_domain:
|
||||
# Subdomain detected
|
||||
store_code = host.split('.')[0] # "orion"
|
||||
# Detection logic — two-step subdomain lookup
|
||||
subdomain = host.split('.')[0] # "wizatech-rewards"
|
||||
|
||||
# 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.code == store_code
|
||||
Store.subdomain == subdomain
|
||||
).first()
|
||||
|
||||
# Set request state
|
||||
request.state.store = store
|
||||
request.state.store_id = store.id
|
||||
request.state.clean_path = "/storefront/products" # Already clean
|
||||
# Set request state
|
||||
request.state.store = store
|
||||
request.state.store_id = store.id
|
||||
request.state.clean_path = "/storefront/products" # Already clean
|
||||
```
|
||||
|
||||
**Request State After**:
|
||||
@@ -116,23 +122,32 @@ request.state.clean_path = "/storefront/products"
|
||||
|
||||
**What happens**:
|
||||
- FastAPI matches the request path against registered routes
|
||||
- For path-based development mode, routes are registered with two prefixes:
|
||||
- `/storefront/*` for subdomain/custom domain
|
||||
- `/stores/{store_code}/storefront/*` for path-based development
|
||||
- Both storefront and store dashboard routes are registered with two prefixes each:
|
||||
- Storefront: `/storefront/*` and `/storefront/{store_code}/*`
|
||||
- Store dashboard: `/store/*` and `/store/{store_code}/*`
|
||||
|
||||
**Example** (Path-Based Mode):
|
||||
|
||||
```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="/stores/{store_code}/storefront")
|
||||
app.include_router(storefront_pages.router, prefix="/storefront/{store_code}")
|
||||
|
||||
# Request: /stores/ORION/storefront/products
|
||||
# Matches: Second router (/stores/{store_code}/storefront)
|
||||
# Route: @router.get("/products")
|
||||
# store_code available as path parameter = "ORION"
|
||||
# In main.py - Double router mounting for store dashboard
|
||||
app.include_router(store_pages.router, prefix="/store")
|
||||
app.include_router(store_pages.router, prefix="/store/{store_code}")
|
||||
|
||||
# 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.
|
||||
|
||||
**Request State After**: No changes to state, but internal path updated
|
||||
|
||||
@@ -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/products
|
||||
https://STORE_SUBDOMAIN.platform-domain.lu/cart
|
||||
https://STORE_SUBDOMAIN.platform-domain.lu/store/dashboard (staff)
|
||||
|
||||
Example:
|
||||
https://acme.omsflow.lu/
|
||||
https://acme.omsflow.lu/products
|
||||
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)
|
||||
@@ -227,7 +232,9 @@ Request arrives
|
||||
1. Customer visits `https://acme.omsflow.lu/products`
|
||||
2. `PlatformContextMiddleware` detects subdomain `"acme"`, resolves platform from root domain `omsflow.lu`
|
||||
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)`
|
||||
6. `frontend_type_middleware` detects STOREFRONT from `/storefront` path prefix
|
||||
7. `theme_context_middleware` loads ACME's theme
|
||||
@@ -310,6 +317,7 @@ id | store_id | domain | is_active | is_verified
|
||||
|
||||
### Subdomain/Custom Domain (PRODUCTION)
|
||||
```
|
||||
Storefront (customer-facing):
|
||||
https://acme.omsflow.lu/ → Homepage
|
||||
https://acme.omsflow.lu/products → Product Catalog
|
||||
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/dashboard → Account Dashboard (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/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`
|
||||
@@ -524,28 +541,46 @@ In Jinja2 template:
|
||||
|
||||
**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
|
||||
# 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/{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:**
|
||||
|
||||
1. **For Subdomain/Custom Domain Mode (Production):**
|
||||
- URL: `https://acme.omsflow.lu/products`
|
||||
- `PlatformContextMiddleware` detects subdomain, rewrites path: `/products` → `/storefront/products`
|
||||
- Matches: First router with `/storefront` prefix
|
||||
- Route: `@router.get("/products")` → Full path: `/storefront/products`
|
||||
- Storefront: `https://acme.omsflow.lu/products` → path rewritten to `/storefront/products` → matches first storefront mount
|
||||
- Dashboard: `https://acme.omsflow.lu/store/dashboard` → matches first store mount at `/store`
|
||||
- Store resolved by middleware via `request.state.store`
|
||||
|
||||
2. **For Path-Based Development Mode:**
|
||||
- URL: `http://localhost:8000/platforms/oms/storefront/ACME/products`
|
||||
- Platform middleware strips `/platforms/oms/` prefix, sets platform context
|
||||
- Matches: Second router with `/storefront/{store_code}` prefix
|
||||
- Route: `@router.get("/products")` → Full path: `/storefront/{store_code}/products`
|
||||
- Bonus: `store_code` available as path parameter!
|
||||
- Storefront: `http://localhost:8000/platforms/oms/storefront/ACME/products` → matches second storefront mount at `/storefront/{store_code}`
|
||||
- Dashboard: `http://localhost:8000/platforms/oms/store/ACME/dashboard` → matches second store mount at `/store/{store_code}`
|
||||
- `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:**
|
||||
- ✅ 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
|
||||
- ✅ Internal path rewriting handled by ASGI middleware
|
||||
- ✅ 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
|
||||
|
||||
@@ -140,11 +140,19 @@ This runs:
|
||||
|
||||
### Stores Created
|
||||
|
||||
| Code | Name | Subdomain | Theme | Custom Domain |
|
||||
|------|------|-----------|-------|---------------|
|
||||
| ORION | Orion | orion | modern | orion.shop |
|
||||
| FASHIONHUB | Fashion Hub | fashionhub | vibrant | fashionhub.store |
|
||||
| BOOKSTORE | The Book Store | bookstore | classic | (none) |
|
||||
| Code | Name | Subdomain | Theme | Custom Domain | Notes |
|
||||
|------|------|-----------|-------|---------------|-------|
|
||||
| WIZATECH | WizaTech | wizatech | modern | wizatech.shop (OMS) | `custom_subdomain="wizatech-rewards"` on Loyalty `StorePlatform` |
|
||||
| WIZAGADGETS | WizaGadgets | wizagadgets | modern | (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
|
||||
|
||||
@@ -217,15 +225,15 @@ python scripts/seed_database.py [--reset] [--minimal]
|
||||
- Username: `admin`
|
||||
- Password: `admin123`
|
||||
|
||||
### Store Storefronts
|
||||
- ORION: `http://localhost:8000/storefront/ORION`
|
||||
- FASHIONHUB: `http://localhost:8000/storefront/FASHIONHUB`
|
||||
- BOOKSTORE: `http://localhost:8000/storefront/BOOKSTORE`
|
||||
### Store Storefronts (Development)
|
||||
- WIZATECH (OMS): `http://localhost:8000/platforms/oms/storefront/WIZATECH/`
|
||||
- WIZATECH (Loyalty): `http://localhost:8000/platforms/loyalty/storefront/WIZATECH/`
|
||||
- FASHIONHUB: `http://localhost:8000/platforms/loyalty/storefront/FASHIONHUB/`
|
||||
- BOOKSTORE: `http://localhost:8000/platforms/oms/storefront/BOOKSTORE/`
|
||||
|
||||
### Theme Editors
|
||||
- ORION Theme: `http://localhost:8000/admin/stores/ORION/theme`
|
||||
- FASHIONHUB Theme: `http://localhost:8000/admin/stores/FASHIONHUB/theme`
|
||||
- BOOKSTORE Theme: `http://localhost:8000/admin/stores/BOOKSTORE/theme`
|
||||
### Store Dashboards (Development)
|
||||
- WIZATECH: `http://localhost:8000/platforms/oms/store/WIZATECH/dashboard`
|
||||
- FASHIONHUB: `http://localhost:8000/platforms/loyalty/store/FASHIONHUB/dashboard`
|
||||
|
||||
## Example Output
|
||||
|
||||
|
||||
@@ -152,6 +152,10 @@ DEMO_STORES = [
|
||||
"description": "Premium electronics and gadgets marketplace",
|
||||
"theme_preset": "modern",
|
||||
"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
|
||||
@@ -179,6 +183,7 @@ DEMO_STORES = [
|
||||
"description": "Trendy clothing and accessories",
|
||||
"theme_preset": "vibrant",
|
||||
"custom_domain": "fashionhub.store",
|
||||
"custom_domain_platform": "loyalty", # Link domain to Loyalty platform
|
||||
},
|
||||
{
|
||||
"merchant_index": 1, # Fashion Group
|
||||
@@ -799,12 +804,28 @@ def create_demo_stores(
|
||||
)
|
||||
).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):
|
||||
# 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(
|
||||
store_id=store.id,
|
||||
platform_id=platform_id,
|
||||
is_active=True,
|
||||
is_primary=(i == 0),
|
||||
custom_subdomain=custom_sub,
|
||||
)
|
||||
db.add(sp)
|
||||
|
||||
@@ -814,6 +835,9 @@ def create_demo_stores(
|
||||
f" Linked to {len(merchant_subs)} platform(s): "
|
||||
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
|
||||
|
||||
@@ -839,11 +863,15 @@ def create_demo_stores(
|
||||
|
||||
# Create custom domain if specified
|
||||
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(
|
||||
store_id=store.id,
|
||||
domain=store_data[
|
||||
"custom_domain"
|
||||
], # ✅ Field is 'domain', not 'domain_name'
|
||||
platform_id=domain_platform_id,
|
||||
is_verified=True, # Auto-verified for demo
|
||||
is_primary=True,
|
||||
verification_token=None,
|
||||
@@ -1290,6 +1318,20 @@ def print_summary(db: Session):
|
||||
print(f"\n {store.name} ({store.store_code})")
|
||||
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
|
||||
custom_domain = (
|
||||
db.query(StoreDomain)
|
||||
@@ -1305,7 +1347,12 @@ def print_summary(db: Session):
|
||||
or getattr(custom_domain, "name", None)
|
||||
)
|
||||
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'}")
|
||||
|
||||
@@ -1375,6 +1422,25 @@ def print_summary(db: Session):
|
||||
port = settings.api_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("─" * 70)
|
||||
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")
|
||||
else:
|
||||
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("⚠️ ALL DEMO CREDENTIALS ARE INSECURE - For development only!")
|
||||
|
||||
Reference in New Issue
Block a user